diff --git a/README.md b/README.md index 5821199..a6c0db4 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,13 @@ Keep the file private (`chmod 600`); it holds your API keys. | `Esc` | back / close dialog | | `Ctrl+C` | quit | +## On the TV (LG webOS) + +[webos/](webos/README.md) has the same views as a TV app for LG webOS (2020 models and newer), +driven by the remote: search and Trakt lists, Library with filters, seasons and episodes, release +search and grab, Downloads, Calendar. It is built with your config baked in and installed in the +TV's Developer Mode; see [webos/README.md](webos/README.md). + ## Development `dev/` has a compose file with pinned Radarr, Sonarr and SABnzbd containers plus a fake indexer; diff --git a/dev/screenshots/README.md b/dev/screenshots/README.md index ad165c3..664507f 100644 --- a/dev/screenshots/README.md +++ b/dev/screenshots/README.md @@ -18,7 +18,7 @@ DejaVu fonts; `oxipng` (optional) compresses the PNGs losslessly. It takes about |---|---| | `catalog.py` | The made-up library: movies (with file details) and series (with seasons). | | `posters.sh` | Draws a poster per title into `posters/` (gradient, title, year); run by `take.sh` when `posters/` is missing. | -| `server.py` | The fake services on 127.0.0.1: Radarr 19891, Sonarr 19892, SABnzbd 19893, Trakt 19894 (`take.sh` points arr-tui there with `ARR_TUI_TRAKT_URL`). Library, search, lists, releases, queues, history, calendar, posters. | +| `server.py` | The fake services on 127.0.0.1: Radarr 19891, Sonarr 19892, SABnzbd 19893, Trakt 19894 (`take.sh` points arr-tui there with `ARR_TUI_TRAKT_URL`). Library, search, lists, releases, queues, history, calendar, posters. Like the real services it answers cross-origin requests, and it accepts writes without changing anything, for the webOS app (`webos/`, `npm run demo`). | | `xshot.sh` | Runs a command in Ghostty under Xvfb and follows a scene script. | | `scenes/*.txt` | What to press and when to take a picture: `key`, `type`, `sleep`, `shot `. | diff --git a/dev/screenshots/server.py b/dev/screenshots/server.py index 75918d4..8e5c15a 100644 --- a/dev/screenshots/server.py +++ b/dev/screenshots/server.py @@ -220,6 +220,27 @@ def sab(q): def handler(port): class H(BaseHTTPRequestHandler): def log_message(self, *a): pass + # Like the real services, answer cross-origin requests (the webOS app + # runs in a browser). + def end_headers(self): + self.send_header("Access-Control-Allow-Origin", "*") + super().end_headers() + def do_OPTIONS(self): + self.send_response(204) + self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") + self.send_header("Access-Control-Allow-Headers", self.headers.get("Access-Control-Request-Headers", "*")) + self.end_headers() + # Writes (add, grab, commands, monitoring, removal) are accepted and + # change nothing; an added title comes back with an id. + def do_POST(self): + n = int(self.headers.get("Content-Length") or 0) + body = json.loads(self.rfile.read(n) or b"{}") if n else {} + out = dict(body, id=900) if isinstance(body, dict) and self.path.split("?")[0].endswith(("/movie", "/series")) else {} + b = json.dumps(out).encode() + self.send_response(201 if out else 200); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b) + do_PUT = do_POST + def do_DELETE(self): + self.send_response(200); self.send_header("Content-Length", "0"); self.end_headers() def do_GET(self): u = urllib.parse.urlparse(self.path); q = urllib.parse.parse_qs(u.query); path = u.path if path.startswith("/MediaCover/") or path.startswith("/remote/"): diff --git a/webos/.gitignore b/webos/.gitignore new file mode 100644 index 0000000..e62ff4c --- /dev/null +++ b/webos/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +out/ diff --git a/webos/README.md b/webos/README.md new file mode 100644 index 0000000..3533bdb --- /dev/null +++ b/webos/README.md @@ -0,0 +1,90 @@ +# arr-tui for LG webOS TVs + +The same views as the terminal app, as a TV app driven by the remote: + +- **Movies** and **Series**: search Radarr / Sonarr. With an empty search box you get the Trakt lists + (trending, most watched this week, anticipated, popular, US box office). Add a title, or open + releases and other actions for library titles. +- **Library**: movies and series, each with its own status, title and file-property filters + (quality, language, codec, HDR, audio). A blue dot marks titles that are downloading. A series + opens its seasons and episodes. +- **Downloads**: the SABnzbd queue and history and the Radarr / Sonarr queues. Pause / resume, + delete, retry, remove and blocklist. +- **Calendar**: two weeks of movie releases and airing episodes. +- **Settings** and **Log**. +- **Release search** with approval and rejection reasons, sort, filters and grab. +- **Details pane** with poster, plot and cast from Trakt. + +It is a plain web app (TypeScript, no framework) that talks to Radarr, Sonarr, SABnzbd and Trakt +directly: all four answer cross-origin requests, so the TV needs nothing else running. + +## The remote + +| Button | Does | +|---|---| +| ◀ ▶ ▲ ▼ | move; in a list ▲ ▼ move the selection, at its ends they leave it | +| OK | press; on a title, the menu of what can be done with it | +| Back | close a dialog or page; in a view, back to its tab; on the tabs, leave the app | +| 1–7 | open a view | +| CH ▲ / ▼ | page through a list | +| Red / Green | previous / next Trakt list or calendar week; Library: movies ⇄ series | +| Yellow | filter by file properties (Library); pause / resume SABnzbd (Downloads) | +| Blue | reload | + +The footer shows what OK and the colour buttons do in the current view. With the Magic Remote's +pointer, hovering focuses and clicking presses. In a desktop browser, F1–F4 stand in for the +colour buttons and Escape for Back. + +## Configuration + +The build bakes your arr-tui config into the app: `$ARR_TUI_CONFIG`, else +`~/.config/arr-tui/config.json` (or `node build.mjs --config FILE`). On the TV, **Settings** +changes the addresses, keys, defaults, Trakt client ID and theme. Those changes are kept on the TV +until "Reset to built-in". + +The package then holds your API keys. Keep it private and never publish it. + +Addresses must not redirect. The terminal app follows a redirect from `http://` to `https://`; a +browser does not for cross-origin requests, so enter the final (usually `https://`) address. + +## Install on the TV (Developer Mode) + +1. On the TV, install **Developer Mode** from the LG Content Store and sign in with an LG + developer account (free, [webostv.developer.lge.com](https://webostv.developer.lge.com)). Turn + on **Dev Mode Status** and **Key Server**, and note the IP address and the passphrase. +2. Once, on the computer (Node.js 22 or newer): + + ```sh + cd webos + npm install + npx ares-setup-device -a tv -i "host=TV_IP" -i "port=9922" -i "username=prisoner" + npx ares-novacom --device tv --getkey # asks for the passphrase + ``` + +3. Build, package, install and start: + + ```sh + npm run package # dist/ and out/com.stinnesbeck.arrtui__all.ipk + npm run install-tv + npm run launch + ``` + +A Developer Mode session lasts 50 hours; the Developer Mode app extends it, otherwise the TV removes +developer apps. On a rooted TV with the Homebrew Channel the `.ipk` can be installed without that +limit. + +The app targets webOS 5 (2020 TVs, Chromium 68) and newer. + +## Development + +```sh +npm run typecheck +npm test # unit tests (Node's test runner) +npm run demo # dist/ with the fake services of dev/screenshots +python3 ../dev/screenshots/server.py & # the fake Radarr, Sonarr, SABnzbd and Trakt +CHROMIUM=$(command -v chromium) node dev/drive.mjs SCENE OUTDIR +``` + +`dev/drive.mjs` opens `dist/index.html` in Chromium at 1920×1080, presses remote buttons from a +scene file (`key Down Enter Red`, `type text`, `sleep 1`, `shot name`, `log`) and takes screenshots. +The page can also be opened in any desktop browser (`dist/index.html`). diff --git a/webos/build.mjs b/webos/build.mjs new file mode 100644 index 0000000..b6ca8e9 --- /dev/null +++ b/webos/build.mjs @@ -0,0 +1,150 @@ +// Builds the webOS app into dist/: the bundled script, the page, the icons +// and config.js with the arr-tui config baked in. +// +// node build.mjs config from $ARR_TUI_CONFIG or ~/.config/arr-tui/config.json +// node build.mjs --config FILE another config file +// node build.mjs --demo the fake services of dev/screenshots (server.py) +// node build.mjs --dev not minified +// +// The baked-in config holds API keys: never publish a package built with it. + +import * as esbuild from "esbuild"; +import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { deflateSync } from "node:zlib"; + +const here = dirname(fileURLToPath(import.meta.url)); +const dist = join(here, "dist"); +const args = process.argv.slice(2); +const flag = (name) => args.includes(name); +const option = (name) => { + const i = args.indexOf(name); + return i >= 0 ? args[i + 1] : undefined; +}; + +// The oldest browser engine targeted: webOS 5 (2020 TVs) has Chromium 68. +const target = "chrome68"; + +function configPath() { + if (option("--config")) return option("--config"); + if (process.env.ARR_TUI_CONFIG) return process.env.ARR_TUI_CONFIG; + const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config"); + return join(base, "arr-tui", "config.json"); +} + +function demoConfig() { + const key = "0123456789abcdef0123456789abcdef"; + return { + radarr: { url: "http://127.0.0.1:19891", apikey: key, default_quality_profile: "Ultra-HD", default_root_folder: "/movies" }, + sonarr: { url: "http://127.0.0.1:19892", apikey: key, default_quality_profile: "HD-1080p", default_root_folder: "/tv" }, + sabnzbd: { url: "http://127.0.0.1:19893", apikey: key }, + trakt: { client_id: key }, + ui: { theme: "dark" }, + }; +} + +// pick keeps the settings the TV app uses, nothing else from the file. +function pick(c) { + const svc = (s = {}) => ({ url: s.url ?? "", apikey: s.apikey ?? "" }); + const arr = (s = {}) => ({ ...svc(s), default_quality_profile: s.default_quality_profile, default_root_folder: s.default_root_folder }); + const ui = c.ui ?? {}; + return { + radarr: arr(c.radarr), + sonarr: arr(c.sonarr), + sabnzbd: svc(c.sabnzbd), + trakt: { client_id: c.trakt?.client_id }, + ui: { theme: ui.theme, refresh_seconds: ui.refresh_seconds, covers: ui.covers, notify_downloads: ui.notify_downloads }, + }; +} + +function loadConfig() { + if (flag("--demo")) return { config: demoConfig(), from: "the demo services", extra: 'window.ARR_TUI_TRAKT_URL = "http://127.0.0.1:19894";\n' }; + const path = configPath(); + if (!existsSync(path)) { + console.warn(`no config at ${path}: the app starts with Settings`); + return { config: pick({}), from: "nothing", extra: "" }; + } + return { config: pick(JSON.parse(readFileSync(path, "utf8"))), from: path, extra: "" }; +} + +// --- Icons: drawn here, so no image tools are needed --- + +function png(size, draw) { + const px = Buffer.alloc(size * size * 4); + for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) px.set(draw(x + 0.5, y + 0.5, size), (y * size + x) * 4); + const raw = Buffer.alloc((size * 4 + 1) * size); + for (let y = 0; y < size; y++) px.copy(raw, y * (size * 4 + 1) + 1, y * size * 4, (y + 1) * size * 4); + const chunk = (type, data) => { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length); + const td = Buffer.concat([Buffer.from(type), data]); + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(td) >>> 0); + return Buffer.concat([len, td, crc]); + }; + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(size, 0); + ihdr.writeUInt32BE(size, 4); + ihdr[8] = 8; // bit depth + ihdr[9] = 6; // RGBA + return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", ihdr), chunk("IDAT", deflateSync(raw)), chunk("IEND", Buffer.alloc(0))]); +} + +function crc32(buf) { + let c = ~0; + for (const b of buf) { + c ^= b; + for (let k = 0; k < 8; k++) c = c & 1 ? (c >>> 1) ^ 0xedb88320 : c >>> 1; + } + return ~c; +} + +// The icon: a dark rounded square with three yellow list bars of different +// lengths and a blue dot (a title downloading), as in the Library. +function icon(x, y, s) { + const u = (v) => v * s; + const r = u(0.18); + const inside = (() => { + const cx = Math.min(Math.max(x, r), s - r); + const cy = Math.min(Math.max(y, r), s - r); + return (x - cx) ** 2 + (y - cy) ** 2 <= r * r; + })(); + if (!inside) return [0, 0, 0, 0]; + const bars = [[0.26, 0.42, 0.74], [0.45, 0.42, 0.62], [0.64, 0.42, 0.70]]; + for (const [top, left, right] of bars) { + if (y >= u(top) && y <= u(top + 0.1) && x >= u(left) && x <= u(right)) return [0xf2, 0xb6, 0x32, 255]; + } + const dots = [[0.31, [0x7f, 0xcc, 0x6e]], [0.50, [0x5f, 0xad, 0xf2]], [0.69, [0x7f, 0xcc, 0x6e]]]; + for (const [cy, rgb] of dots) { + if ((x - u(0.3)) ** 2 + (y - u(cy)) ** 2 <= u(0.055) ** 2) return [...rgb, 255]; + } + return [0x1d, 0x20, 0x27, 255]; +} + +// --- Build --- + +rmSync(dist, { recursive: true, force: true }); +mkdirSync(dist, { recursive: true }); +for (const f of readdirSync(join(here, "static"))) copyFileSync(join(here, "static", f), join(dist, f)); +writeFileSync(join(dist, "icon.png"), png(80, icon)); +writeFileSync(join(dist, "largeIcon.png"), png(130, icon)); + +const { config, from, extra } = loadConfig(); +writeFileSync(join(dist, "config.js"), `// Built from ${from === "nothing" || from === "the demo services" ? from : "the arr-tui config"}. Holds API keys: keep this package private.\nwindow.ARR_TUI_CONFIG = ${JSON.stringify(config, null, 2)};\n${extra}`, { mode: 0o600 }); + +await esbuild.build({ + entryPoints: [join(here, "src", "main.ts")], + bundle: true, + outfile: join(dist, "app.js"), + format: "iife", + target, + minify: !flag("--dev"), + sourcemap: flag("--dev") ? "inline" : false, + legalComments: "none", + logLevel: "warning", +}); + +const services = ["radarr", "sonarr", "sabnzbd"].filter((s) => config[s].url).join(", ") || "none"; +console.log(`dist/ built (config from ${from}; services: ${services}${config.trakt.client_id ? ", Trakt" : ""})`); diff --git a/webos/dev/drive.mjs b/webos/dev/drive.mjs new file mode 100644 index 0000000..5f2ce0c --- /dev/null +++ b/webos/dev/drive.mjs @@ -0,0 +1,94 @@ +// Runs the built app (dist/) in Chromium at 1920×1080 from file://, as the +// TV loads it, presses remote buttons from a scene file and takes +// screenshots. For development and the README pictures. +// +// node dev/drive.mjs SCENE OUTDIR +// +// CHROMIUM (default: chromium on PATH). Scene lines: +// key Down [Down ...] remote buttons: Left Right Up Down Enter Back Red +// Green Yellow Blue PageUp PageDown 1-7 +// type TEXT type into the focused field +// sleep SECONDS +// shot NAME OUTDIR/NAME.png +// log print the app's console and the focused element + +import puppeteer from "puppeteer-core"; +import { mkdirSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const [scenePath, outDir] = process.argv.slice(2); +if (!scenePath || !outDir) { + console.error("usage: node dev/drive.mjs SCENE OUTDIR"); + process.exit(2); +} +mkdirSync(outDir, { recursive: true }); + +const codes = { + Left: 37, Up: 38, Right: 39, Down: 40, Enter: 13, Back: 461, + Red: 403, Green: 404, Yellow: 405, Blue: 406, PageUp: 33, PageDown: 34, +}; +for (let i = 1; i <= 9; i++) codes[String(i)] = 48 + i; + +const browser = await puppeteer.launch({ + executablePath: process.env.CHROMIUM || "chromium", + headless: true, + // file:// pages may call the local fake services. + args: ["--window-size=1920,1080", "--allow-file-access-from-files", "--no-sandbox"], + defaultViewport: { width: 1920, height: 1080 }, +}); +const page = await browser.newPage(); +const logs = []; +// Request URLs in browser messages can carry an API key (SABnzbd, posters). +const redact = (t) => t.replace(/(apikey=)[^&\s'"]+/gi, "$1…"); +page.on("console", (m) => logs.push(redact(`${m.type()}: ${m.text()}`))); +page.on("pageerror", (e) => logs.push(redact(`pageerror: ${e.message}`))); +await page.goto("file://" + resolve(here, "..", "dist", "index.html")); + +// press sends a remote button as the TV does: a keydown with its keyCode. +async function press(name) { + const code = codes[name]; + if (!code) throw new Error("unknown key " + name); + await page.evaluate((code) => { + const target = document.activeElement || document.body; + const ev = new KeyboardEvent("keydown", { bubbles: true, cancelable: true }); + Object.defineProperty(ev, "keyCode", { get: () => code }); + Object.defineProperty(ev, "which", { get: () => code }); + target.dispatchEvent(ev); + }, code); + await new Promise((r) => setTimeout(r, 120)); +} + +for (const raw of readFileSync(scenePath, "utf8").split("\n")) { + const line = raw.trim(); + if (!line || line.startsWith("#")) continue; + const [cmd, ...rest] = line.split(/\s+/); + switch (cmd) { + case "key": + for (const k of rest) await press(k); + break; + case "type": + await page.keyboard.type(line.slice(5)); + break; + case "sleep": + await new Promise((r) => setTimeout(r, parseFloat(rest[0]) * 1000)); + break; + case "shot": + await page.screenshot({ path: join(outDir, rest[0] + ".png") }); + break; + case "log": { + const focused = await page.evaluate(() => { + const a = document.activeElement; + return a ? `${a.tagName}.${a.className} "${(a.textContent || "").slice(0, 40)}"` : "none"; + }); + console.log(logs.splice(0).join("\n")); + console.log("focused:", focused); + break; + } + default: + throw new Error("unknown scene command: " + line); + } +} +if (logs.length) console.log(logs.join("\n")); +await browser.close(); diff --git a/webos/go.mod b/webos/go.mod new file mode 100644 index 0000000..79b24f1 --- /dev/null +++ b/webos/go.mod @@ -0,0 +1,5 @@ +// Not Go: this file keeps webos/ (and the Go files some npm packages ship +// in node_modules) out of the arr-tui module, so `go test ./...` skips it. +module git.stinnesbeck.com/nils/arr-tui/webos + +go 1.26 diff --git a/webos/package-lock.json b/webos/package-lock.json new file mode 100644 index 0000000..941b6c4 --- /dev/null +++ b/webos/package-lock.json @@ -0,0 +1,7099 @@ +{ + "name": "arr-tui-webos", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "arr-tui-webos", + "devDependencies": { + "@types/node": "24.19.2", + "@webos-tools/cli": "3.2.6", + "esbuild": "0.28.2", + "puppeteer-core": "25.13.0", + "typescript": "7.0.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.4.tgz", + "integrity": "sha512-okJ6yQMc0SBexOKxMi966omwAV1uAPwgF/YMZdb04gZQPXVjWHl9bmXjZYtIcsN1+PN3Bz4iFfGK3g+c6lZJ0Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.8.5", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } + } + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@webos-tools/cli": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@webos-tools/cli/-/cli-3.2.6.tgz", + "integrity": "sha512-VkCV8oBycgd0UTT3n8xOCUKjzIFaJM5MzJWr9BqmHfplUemtMsTBx+1GNOyxio6Ky1mj/iyqAUx4VcUy2nQu3Q==", + "dev": true, + "hasInstallScript": true, + "hasShrinkwrap": true, + "license": "Apache-2.0", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "ar-async": "0.1.4", + "async": "3.2.6", + "bluebird": "3.7.2", + "body-parser": "2.3.0", + "chalk": "4.1.2", + "chardet": "2.2.0", + "chokidar": "4.0.3", + "combined-stream": "1.0.8", + "csv-writer": "1.6.0", + "easy-table": "1.2.0", + "elfy": "1.0.0", + "encoding": "0.1.13", + "express": "5.2.1", + "fs-extra": "11.3.6", + "fstream": "1.0.12", + "inherits": "2.0.4", + "inquirer": "8.2.7", + "jsonschema": "1.5.0", + "mkdirp": "3.0.1", + "nopt": "8.1.0", + "npmlog": "7.0.1", + "ora": "5.4.0", + "rimraf": "6.1.3", + "semver": "7.7.4", + "shelljs": "0.10.0", + "sprintf-js": "1.1.3", + "ssdp-js": "1.0.1", + "ssh2": "^1.17.0", + "stream-buffers": "3.0.3", + "strip-bom": "4.0.0", + "tar": "^7.5.22", + "temp": "0.9.4", + "terser": "5.48.0" + }, + "bin": { + "ares": "bin/ares.js", + "ares-config": "bin/ares-config.js", + "ares-device": "bin/ares-device.js", + "ares-device-info": "bin/ares-device-info.js", + "ares-generate": "bin/ares-generate.js", + "ares-inspect": "bin/ares-inspect.js", + "ares-install": "bin/ares-install.js", + "ares-launch": "bin/ares-launch.js", + "ares-log": "bin/ares-log.js", + "ares-novacom": "bin/ares-novacom.js", + "ares-package": "bin/ares-package.js", + "ares-pull": "bin/ares-pull.js", + "ares-push": "bin/ares-push.js", + "ares-server": "bin/ares-server.js", + "ares-setup-device": "bin/ares-setup-device.js", + "ares-shell": "bin/ares-shell.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webos-tools/cli/node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "extraneous": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@webos-tools/cli/node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "extraneous": true, + "license": "BSD-3-Clause" + }, + "node_modules/@webos-tools/cli/node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@webos-tools/cli/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "extraneous": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@webos-tools/cli/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@webos-tools/cli/node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@webos-tools/cli/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@webos-tools/cli/node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@webos-tools/cli/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@webos-tools/cli/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@webos-tools/cli/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@webos-tools/cli/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@webos-tools/cli/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@webos-tools/cli/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@webos-tools/cli/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@webos-tools/cli/node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "extraneous": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "extraneous": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@webos-tools/cli/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@webos-tools/cli/node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/ar-async": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ar-async/-/ar-async-0.1.4.tgz", + "integrity": "sha512-iuFSHl96gQAStEPK457rAqVX0n5CVWrnE7SPdc/yS0ixkQXRd6h9W+8TJ1IA0sqgLolIO3oudpG8jl033GAiNg==", + "dev": true + }, + "node_modules/@webos-tools/cli/node_modules/are-we-there-yet": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-4.0.2.tgz", + "integrity": "sha512-ncSWAawFhKMJDTdoAeOV+jyW1VCMj5QIAwULIBV0SSR7B/RLPPEQiknKcg/RIIZlUQrxELpsxMiTUoAQ4sIUyg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "extraneous": true, + "license": "Python-2.0" + }, + "node_modules/@webos-tools/cli/node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/@webos-tools/cli/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@webos-tools/cli/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/@webos-tools/cli/node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@webos-tools/cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@webos-tools/cli/node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@webos-tools/cli/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@webos-tools/cli/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@webos-tools/cli/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@webos-tools/cli/node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@webos-tools/cli/node_modules/csv-writer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz", + "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/@webos-tools/cli/node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@webos-tools/cli/node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/easy-table": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.2.0.tgz", + "integrity": "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "optionalDependencies": { + "wcwidth": "^1.0.1" + } + }, + "node_modules/@webos-tools/cli/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/elfy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/elfy/-/elfy-1.0.0.tgz", + "integrity": "sha512-4Kp3AA94jC085IJox+qnvrZ3PudqTi4gQNvIoTZfJJ9IqkRuCoqP60vCVYlIg00c5aYusi5Wjh2bf0cHYt+6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "endian-reader": "^0.3.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/@webos-tools/cli/node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/endian-reader": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/endian-reader/-/endian-reader-0.3.0.tgz", + "integrity": "sha512-zPlHN59VLEjeJtpEU41ti/i7ZvTbwclvUN2M8anCsI3tOC/3mq6WNTJEKi49A5eLGvDkA0975LZb67Xwp7u4xQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "extraneous": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "extraneous": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webos-tools/cli/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "extraneous": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webos-tools/cli/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "extraneous": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webos-tools/cli/node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "extraneous": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@webos-tools/cli/node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "extraneous": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "extraneous": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "extraneous": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@webos-tools/cli/node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/express/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@webos-tools/cli/node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/flat-cache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "extraneous": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "extraneous": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "extraneous": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "extraneous": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/fs-extra": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@webos-tools/cli/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/fstream/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "extraneous": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/gauge": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-5.0.2.tgz", + "integrity": "sha512-pMaFftXPtiGIHCJHdcUUx9Rby/rFT/Kkt3fIIGCs+9PMDIljSyRiqraTlxNtBReJRDfUefpa263RQ3vnp5G/LQ==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^4.0.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "extraneous": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "extraneous": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "extraneous": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/@webos-tools/cli/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@webos-tools/cli/node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/@webos-tools/cli/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/@webos-tools/cli/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/inquirer": { + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.0", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/inquirer/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "extraneous": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/jasmine": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.7.0.tgz", + "integrity": "sha512-pifsdoSBgOlAbw1Tg1Vm4LxXEzDv6a+dTzHUaI9aYYqdP+PiMFgz2Mce/7TBfvuP9kshl0yZn7+G0/G1n+yExw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "glob": "^10.2.2", + "jasmine-core": "~5.7.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/jasmine-core": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.7.1.tgz", + "integrity": "sha512-QnurrtpKsPoixxG2R3d1xP0St/2kcX5oTZyDyQJMY+Vzi/HUlu1kGm+2V8Tz+9lV991leB1l0xcsyz40s9xOOw==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/jasmine-spec-reporter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz", + "integrity": "sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg==", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "colors": "1.4.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "extraneous": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/jsonschema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@webos-tools/cli/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/@webos-tools/cli/node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "extraneous": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@webos-tools/cli/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@webos-tools/cli/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@webos-tools/cli/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@webos-tools/cli/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@webos-tools/cli/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@webos-tools/cli/node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "extraneous": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/npmlog": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-7.0.1.tgz", + "integrity": "sha512-uJ0YFk/mCQpLBt+bxN88AKd+gyqZvZDbtiNxk6Waqcj2aPRyfVx8ITawkyQynxUagInjdYT1+qj4NfA5KJJUxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^4.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^5.0.0", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/@webos-tools/cli/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/ora": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.0.tgz", + "integrity": "sha512-1StwyXQGoU6gdjYkyVcqOLnVlbKj+6yPNNOxJVgpt9t4eksKjiriiHuxktLYkgllwk+D6MbC4ihH84L1udRXPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/@webos-tools/cli/node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@webos-tools/cli/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "extraneous": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@webos-tools/cli/node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@webos-tools/cli/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@webos-tools/cli/node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@webos-tools/cli/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@webos-tools/cli/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@webos-tools/cli/node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@webos-tools/cli/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/rimraf/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@webos-tools/cli/node_modules/rimraf/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@webos-tools/cli/node_modules/rimraf/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/rimraf/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@webos-tools/cli/node_modules/rimraf/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@webos-tools/cli/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/@webos-tools/cli/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@webos-tools/cli/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/shelljs": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.10.0.tgz", + "integrity": "sha512-Jex+xw5Mg2qMZL3qnzXIfaxEtBaC4n7xifqaqtrZDdlheR70OGkydrPJWT0V1cA1k3nanC86x9FwAmQl6w3Klw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "execa": "^5.1.1", + "fast-glob": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@webos-tools/cli/node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@webos-tools/cli/node_modules/ssdp-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ssdp-js/-/ssdp-js-1.0.1.tgz", + "integrity": "sha512-QxrgpbiD7ItnpBs5UiWVCHk52OiiPjj9KD2rm2susYvrbkRIXWvUg3v8fQxrXWKoSfP8qXt5HmkH/tAPz8j6bA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/stream-buffers": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.3.tgz", + "integrity": "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@webos-tools/cli/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@webos-tools/cli/node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/temp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@webos-tools/cli/node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/@webos-tools/cli/node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@webos-tools/cli/node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@webos-tools/cli/node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@webos-tools/cli/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@webos-tools/cli/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/@webos-tools/cli/node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "extraneous": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@webos-tools/cli/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@webos-tools/cli/node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@webos-tools/cli/node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "extraneous": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webos-tools/cli/node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@webos-tools/cli/node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/@webos-tools/cli/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@webos-tools/cli/node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@webos-tools/cli/node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/@webos-tools/cli/node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@webos-tools/cli/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@webos-tools/cli/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@webos-tools/cli/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webos-tools/cli/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@webos-tools/cli/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chromium-bidi": { + "version": "157.0.8090-0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-157.0.8090-0.tgz", + "integrity": "sha512-KJw7IQgqXwxpJgJlEIPg5mr2ZoqA+jkTRtc5EABI7sUJ3bu3cpI7NuEBoj4Gn1AsF78CaT9So5WQWspK3/wYag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^4.6.5" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1696802", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1696802.tgz", + "integrity": "sha512-FO5/SENfvc2K8wRnWQflKcwuoTH4dBkXCWTQ9ebIWtuX8JM5+ZvCotaEmkfh21pO7TvIKGTM3hG4xFiq9WTwmg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.6.tgz", + "integrity": "sha512-LYSks0tNVqbtez/9OB0YFP6qLRSqkdAxJWOQpVaR67lNTbj2zNLyYpowkRMia5lXLCZKzxwoxkxH2MbbBR2aEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.13.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.13.0.tgz", + "integrity": "sha512-cyBS0OEx3e9NS53B5F6+UazXtR7f9Tw0cR1/FbPW59/hV6JPZbkIiMnxK2543IkFp3rsGlLn1iPDLF77TJsJsA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.2.4", + "chromium-bidi": "157.0.8090-0", + "devtools-protocol": "0.0.1696802", + "typed-query-selector": "^2.12.3", + "webdriver-bidi-protocol": "1.0.1", + "ws": "^8.21.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webdriver-bidi-protocol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-1.0.1.tgz", + "integrity": "sha512-WXWs/aXF2a6Al1Z2/V/7Gp2oPZOAAmHjJOTmv4KCXRJ122IMj3tSSMc6a4q3K4QOmhQbse/Ahr+oTVJOU2dUiw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/webos/package.json b/webos/package.json new file mode 100644 index 0000000..362d9e5 --- /dev/null +++ b/webos/package.json @@ -0,0 +1,22 @@ +{ + "name": "arr-tui-webos", + "private": true, + "description": "arr-tui for LG webOS TVs: Radarr, Sonarr and SABnzbd on the big screen", + "type": "module", + "scripts": { + "build": "node build.mjs", + "demo": "node build.mjs --demo", + "typecheck": "tsc --noEmit", + "test": "node --test test/*.test.ts", + "package": "node build.mjs && ares-package dist -o out", + "install-tv": "ares-install -d tv out/com.stinnesbeck.arrtui_*.ipk", + "launch": "ares-launch -d tv com.stinnesbeck.arrtui" + }, + "devDependencies": { + "@types/node": "24.19.2", + "@webos-tools/cli": "3.2.6", + "esbuild": "0.28.2", + "puppeteer-core": "25.13.0", + "typescript": "7.0.2" + } +} diff --git a/webos/src/api/arr.ts b/webos/src/api/arr.ts new file mode 100644 index 0000000..92c9de8 --- /dev/null +++ b/webos/src/api/arr.ts @@ -0,0 +1,260 @@ +// Client and types shared by Radarr and Sonarr (their v3 API), ported from +// internal/arr. + +import { HTTPError, NotConfiguredError, query, request, trimSlash } from "./http.ts"; + +export interface Image { + coverType: string; + url?: string; + remoteUrl?: string; +} + +export interface QualityProfile { + id: number; + name: string; +} + +export interface RootFolder { + id: number; + path: string; + freeSpace: number; + accessible: boolean; +} + +export interface SystemStatus { + appName: string; + version: string; +} + +export interface MediaFile { + size: number; + sceneName?: string; + relativePath?: string; + quality?: { quality?: { name?: string; resolution?: number } }; + mediaInfo?: { + videoCodec?: string; + videoDynamicRange?: string; + videoDynamicRangeType?: string; + audioCodec?: string; + audioAdditionalFeatures?: string; + }; + languages?: { name: string }[]; +} + +export interface QueueItem { + id: number; + title: string; + status: string; + trackedDownloadStatus?: string; + trackedDownloadState?: string; + statusMessages?: { title: string; messages: string[] }[]; + errorMessage?: string; + size: number; + sizeleft: number; + timeleft?: string; + downloadClient?: string; + downloadId?: string; + indexer?: string; + protocol?: string; + movieId?: number; + seriesId?: number; + episodeId?: number; + movie?: { title: string; year: number }; + series?: { title: string }; + episode?: { seasonNumber: number; episodeNumber: number; title: string }; +} + +export interface Release { + guid: string; + title: string; + indexer: string; + indexerId: number; + protocol: string; + size: number; + ageMinutes: number; + qualityWeight: number; + customFormatScore: number; + releaseGroup?: string; + approved: boolean; + rejected: boolean; + temporarilyRejected: boolean; + rejections?: string[]; + fullSeason?: boolean; + quality?: { quality?: { name?: string; resolution?: number } }; + languages?: { name: string }[]; + customFormats?: { name: string }[]; +} + +export function queueName(q: QueueItem): string { + if (q.movie?.title) return q.movie.year > 0 ? `${q.movie.title} (${q.movie.year})` : q.movie.title; + if (q.series?.title) { + if (q.episode) return `${q.series.title} S${pad2(q.episode.seasonNumber)}E${pad2(q.episode.episodeNumber)}`; + return q.series.title; + } + return q.title; +} + +export function queueProgress(q: QueueItem): number { + if (q.size <= 0) return 0; + return Math.min(Math.max(((q.size - q.sizeleft) / q.size) * 100, 0), 100); +} + +export function queueProblem(q: QueueItem): string { + const parts: string[] = []; + if (q.errorMessage) parts.push(q.errorMessage); + for (const m of q.statusMessages ?? []) { + parts.push(...m.messages); + if (m.messages.length === 0 && m.title) parts.push(m.title); + } + return parts.join("; "); +} + +export function pad2(n: number): string { + return n < 10 ? "0" + n : String(n); +} + +type Body = Record | unknown[]; + +export class ArrClient { + name: string; // "Radarr" or "Sonarr", for messages + baseURL = ""; + apiKey = ""; + + constructor(name: string, baseURL: string, apiKey: string) { + this.name = name; + this.configure(baseURL, apiKey); + } + + configure(baseURL: string, apiKey: string): void { + this.baseURL = trimSlash(baseURL); + this.apiKey = apiKey.trim(); + } + + configured(): boolean { + return this.baseURL !== ""; + } + + // do sends a request to /api/v3/ and decodes the JSON answer. + async do(method: string, path: string, params: Record = {}, body?: Body, opts: { signal?: AbortSignal; timeoutMs?: number } = {}): Promise { + if (!this.configured()) throw new NotConfiguredError(this.name); + const headers: Record = { "X-Api-Key": this.apiKey, Accept: "application/json" }; + if (body !== undefined) headers["Content-Type"] = "application/json"; + const resp = await request( + this.baseURL + "/api/v3/" + path.replace(/^\/+/, "") + query(params), + { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: opts.signal }, + opts.timeoutMs ?? 30_000, + ); + const text = await resp.text(); + if (!resp.ok) throw new HTTPError(resp.status, errorMessage(text)); + if (!text) return undefined as T; + try { + return JSON.parse(text) as T; + } catch { + throw new Error(`${this.name}: the answer is not JSON (${text.slice(0, 80)})`); + } + } + + // update reads /, lets mutate change the object and PUTs it back + // whole (Radarr and Sonarr replace the resource on PUT). + async update(path: string, id: number, mutate: (obj: Record) => void): Promise { + const obj = await this.do>("GET", `${path}/${id}`); + mutate(obj); + await this.do("PUT", `${path}/${id}`, {}, obj); + } + + command(body: Record): Promise { + return this.do("POST", "command", {}, body); + } + + systemStatus(): Promise { + return this.do("GET", "system/status", {}, undefined, { timeoutMs: 10_000 }); + } + + qualityProfiles(): Promise { + return this.do("GET", "qualityprofile"); + } + + rootFolders(): Promise { + return this.do("GET", "rootfolder"); + } + + async queue(include: Record): Promise { + const page = await this.do<{ records: QueueItem[] }>("GET", "queue", { page: 1, pageSize: 200, ...include }); + return page.records ?? []; + } + + removeQueueItem(id: number, blocklist: boolean): Promise { + return this.do("DELETE", `queue/${id}`, { removeFromClient: true, blocklist }); + } + + // releases runs an interactive search; it queries every indexer and can + // take minutes. + releases(params: Record, signal?: AbortSignal): Promise { + return this.do("GET", "release", params, undefined, { signal, timeoutMs: 180_000 }); + } + + grab(r: Release): Promise { + return this.do("POST", "release", {}, { guid: r.guid, indexerId: r.indexerId }); + } + + // posterURL returns an image URL for an : the instance's own copy for + // library titles (with the API key as a query parameter, as an cannot + // send headers), else the metadata provider's, at a size fitting the use. + posterURL(images: Image[] | undefined, inLibrary: boolean, large: boolean): string { + const img = images?.find((i) => i.coverType === "poster"); + if (!img) return ""; + if (inLibrary && img.url) { + let path = img.url; + if (!large) path = scaledVariant(path, 500); + return this.resolve(path) + (path.includes("?") ? "&" : "?") + "apikey=" + encodeURIComponent(this.apiKey); + } + return img.remoteUrl ? tmdbSized(img.remoteUrl, large) : ""; + } + + // resolve joins a server-relative path to the base URL. Radarr/Sonarr put + // their URL base into image paths, so it must not be added twice. + resolve(ref: string): string { + try { + const base = new URL(this.baseURL); + const prefix = base.pathname.replace(/\/+$/, ""); + if (prefix && ref.startsWith(prefix + "/")) return base.origin + ref; + } catch { + /* fall through */ + } + return this.baseURL + ref; + } +} + +// errorMessage reads Radarr/Sonarr error bodies: {"message": "..."} or a list +// of validation failures. +export function errorMessage(text: string): string { + try { + const v = JSON.parse(text); + if (v && typeof v.message === "string" && v.message) return v.message; + if (Array.isArray(v) && v.length) return v.map((e: { errorMessage?: string }) => e.errorMessage ?? "").join("; "); + } catch { + /* not JSON */ + } + return text.trim().slice(0, 200); +} + +// scaledVariant turns /MediaCover/1/poster.jpg?x into /MediaCover/1/poster-500.jpg?x. +export function scaledVariant(p: string, height: number): string { + const [path, q] = p.split("?", 2); + if (!path.endsWith("/poster.jpg")) return p; + const scaled = path.slice(0, -4) + "-" + height + ".jpg"; + return q === undefined ? scaled : scaled + "?" + q; +} + +// tmdbSized asks TheMovieDB for the size shown instead of the original +// (often 2000×3000 and over 2 MB). +export function tmdbSized(raw: string, large: boolean): string { + try { + const u = new URL(raw); + if (u.host !== "image.tmdb.org" || !u.pathname.startsWith("/t/p/original/")) return raw; + u.pathname = "/t/p/" + (large ? "w780" : "w342") + u.pathname.slice("/t/p/original".length); + return u.toString(); + } catch { + return raw; + } +} diff --git a/webos/src/api/http.ts b/webos/src/api/http.ts new file mode 100644 index 0000000..90078c9 --- /dev/null +++ b/webos/src/api/http.ts @@ -0,0 +1,64 @@ +// Shared HTTP helpers. The TV app talks to Radarr, Sonarr, SABnzbd and Trakt +// directly from the browser: all four answer cross-origin requests (CORS). + +export class HTTPError extends Error { + status: number; + constructor(status: number, message: string) { + super(message ? `HTTP ${status}: ${message}` : `unexpected response: HTTP ${status}`); + this.status = status; + } +} + +export class NotConfiguredError extends Error { + constructor(what: string) { + super(`${what} is not configured: set its address and API key in Settings`); + } +} + +// request sends a request that gives up after timeoutMs. Network failures +// become an Error naming the host only (the URL may carry an API key). +export async function request(url: string, init: RequestInit = {}, timeoutMs = 30_000): Promise { + const ctl = new AbortController(); + const timer = setTimeout(() => ctl.abort(), timeoutMs); + const outer = init.signal; + if (outer) { + if (outer.aborted) ctl.abort(); + else outer.addEventListener("abort", () => ctl.abort()); + } + try { + return await fetch(url, { ...init, signal: ctl.signal }); + } catch (e) { + if (outer?.aborted) throw new DOMException("cancelled", "AbortError"); + if (ctl.signal.aborted) throw new Error(`${hostOf(url)}: no answer within ${Math.round(timeoutMs / 1000)} s`); + // A browser cannot follow a redirect (e.g. http to https) of a cross-origin + // request unless the redirect itself allows it; the TUI never notices. + const hint = url.startsWith("http://") ? "; if the address redirects to https, enter the https address" : ""; + throw new Error(`${hostOf(url)}: cannot connect (${(e as Error).message})${hint}`); + } finally { + clearTimeout(timer); + } +} + +export function hostOf(url: string): string { + try { + return new URL(url).host; + } catch { + return "server"; + } +} + +export function isAbort(e: unknown): boolean { + return e instanceof DOMException && e.name === "AbortError"; +} + +export function query(params: Record): string { + const parts: string[] = []; + for (const [k, v] of Object.entries(params)) { + if (v !== undefined) parts.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(v))); + } + return parts.length ? "?" + parts.join("&") : ""; +} + +export function trimSlash(u: string): string { + return u.trim().replace(/\/+$/, ""); +} diff --git a/webos/src/api/radarr.ts b/webos/src/api/radarr.ts new file mode 100644 index 0000000..63689f4 --- /dev/null +++ b/webos/src/api/radarr.ts @@ -0,0 +1,113 @@ +// Radarr v3 client, ported from internal/radarr. + +import { ArrClient, type Image, type MediaFile, type QueueItem, type Release } from "./arr.ts"; + +export interface Rating { + votes: number; + value: number; +} + +export interface Movie { + id: number; // 0 when not in the library + title: string; + originalTitle?: string; + year: number; + overview?: string; + runtime: number; + genres?: string[]; + studio?: string; + status?: string; + tmdbId: number; + imdbId?: string; + titleSlug?: string; + images?: Image[]; + ratings?: { imdb?: Rating; tmdb?: Rating }; + monitored: boolean; + hasFile: boolean; + qualityProfileId?: number; + inCinemas?: string; + physicalRelease?: string; + digitalRelease?: string; + sizeOnDisk: number; + isAvailable: boolean; + path?: string; + movieFile?: MediaFile; +} + +export function movieRating(m: Movie): number { + if (m.ratings?.imdb && m.ratings.imdb.value > 0) return m.ratings.imdb.value; + return m.ratings?.tmdb?.value ?? 0; +} + +export function inLibrary(m: { id: number }): boolean { + return !!m.id; +} + +export interface AddMovieOptions { + qualityProfileId: number; + rootFolderPath: string; + monitored: boolean; + searchNow: boolean; +} + +export class RadarrClient extends ArrClient { + constructor(baseURL: string, apiKey: string) { + super("Radarr", baseURL, apiKey); + } + + lookup(term: string, signal?: AbortSignal): Promise { + return this.do("GET", "movie/lookup", { term }, undefined, { signal }); + } + + movies(): Promise { + return this.do("GET", "movie", {}, undefined, { timeoutMs: 60_000 }); + } + + // movieByTMDB returns the library movie with the TMDb id, or null. The + // answer is checked: a version that ignores the filter sends the library. + async movieByTMDB(tmdbId: number): Promise { + const movies = await this.do("GET", "movie", { tmdbId }); + return movies.find((m) => m.tmdbId === tmdbId && m.id) ?? null; + } + + addMovie(m: Movie, o: AddMovieOptions): Promise { + return this.do("POST", "movie", {}, { + title: m.title, + year: m.year, + tmdbId: m.tmdbId, + titleSlug: m.titleSlug, + images: m.images, + qualityProfileId: o.qualityProfileId, + rootFolderPath: o.rootFolderPath, + monitored: o.monitored, + minimumAvailability: "released", + addOptions: { searchForMovie: o.searchNow }, + }); + } + + searchMovie(id: number): Promise { + return this.command({ name: "MoviesSearch", movieIds: [id] }); + } + + movieQueue(): Promise { + return this.queue({ includeMovie: "true" }); + } + + movieReleases(id: number, signal?: AbortSignal): Promise { + return this.releases({ movieId: id }, signal); + } + + setMonitored(id: number, monitored: boolean): Promise { + return this.update("movie", id, (o) => { + o.monitored = monitored; + }); + } + + deleteMovie(id: number, deleteFiles: boolean, addExclusion: boolean): Promise { + return this.do("DELETE", `movie/${id}`, { deleteFiles, addImportExclusion: addExclusion }); + } + + calendar(start: string, end: string, unmonitored: boolean): Promise { + return this.do("GET", "calendar", { start, end, unmonitored }); + } +} diff --git a/webos/src/api/sabnzbd.ts b/webos/src/api/sabnzbd.ts new file mode 100644 index 0000000..3d72ac9 --- /dev/null +++ b/webos/src/api/sabnzbd.ts @@ -0,0 +1,124 @@ +// SABnzbd client, ported from internal/sabnzbd. + +import { NotConfiguredError, query, request, trimSlash } from "./http.ts"; + +export interface Slot { + nzo_id: string; + filename: string; + cat: string; + status: string; // Downloading, Queued, Paused, Fetching, ... + priority: string; + size: string; + sizeleft: string; + mb: number; + mbleft: number; + percentage: number; + timeleft: string; +} + +export interface Queue { + status: string; + paused: boolean; + kbpersec: number; + speedlimit_abs: number; + size: string; + sizeleft: string; + timeleft: string; + noofslots_total: number; + slots: Slot[]; +} + +export interface HistorySlot { + nzo_id: string; + name: string; + category: string; + status: string; // Completed, Failed, Extracting, ... + size: string; + fail_message: string; + completed: number; // unix time +} + +// num reads SABnzbd's numbers, which come as numbers or numeric strings. +export function num(v: unknown): number { + if (typeof v === "number") return v; + const f = parseFloat(String(v ?? "")); + return Number.isFinite(f) ? f : 0; +} + +export class SABnzbdClient { + baseURL = ""; + apiKey = ""; + + constructor(baseURL: string, apiKey: string) { + this.configure(baseURL, apiKey); + } + + configure(baseURL: string, apiKey: string): void { + this.baseURL = trimSlash(baseURL); + this.apiKey = apiKey.trim(); + } + + configured(): boolean { + return this.baseURL !== ""; + } + + // call performs an API request. SABnzbd reports failures with HTTP 200 and + // {"status": false, "error": "..."}. + async call(params: Record, timeoutMs = 30_000): Promise { + if (!this.configured()) throw new NotConfiguredError("SABnzbd"); + const resp = await request(this.baseURL + "/api" + query({ ...params, output: "json", apikey: this.apiKey }), {}, timeoutMs); + const text = await resp.text(); + if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.trim().slice(0, 200)}`); + let v: { status?: unknown; error?: string }; + try { + v = JSON.parse(text); + } catch { + throw new Error(`unexpected response: ${text.trim().slice(0, 200)}`); // e.g. "Access denied" + } + if (v.error) throw new Error(v.error); + if (v.status === false) throw new Error("request failed"); + return v as T; + } + + async version(): Promise { + return (await this.call<{ version: string }>({ mode: "version" }, 10_000)).version; + } + + async queue(): Promise { + const q = (await this.call<{ queue: Queue }>({ mode: "queue" })).queue; + q.kbpersec = num(q.kbpersec); + q.speedlimit_abs = num(q.speedlimit_abs); + for (const s of q.slots ?? []) { + s.mb = num(s.mb); + s.mbleft = num(s.mbleft); + s.percentage = num(s.percentage); + } + q.slots ??= []; + return q; + } + + async history(limit: number): Promise { + const h = await this.call<{ history: { slots: HistorySlot[] } }>({ mode: "history", limit }); + return h.history?.slots ?? []; + } + + async pause(): Promise { + await this.call({ mode: "pause" }); + } + + async resume(): Promise { + await this.call({ mode: "resume" }); + } + + async deleteJob(id: string, deleteFiles: boolean): Promise { + await this.call({ mode: "queue", name: "delete", value: id, del_files: deleteFiles ? 1 : 0 }); + } + + async deleteHistory(id: string, deleteFiles: boolean): Promise { + await this.call({ mode: "history", name: "delete", value: id, del_files: deleteFiles ? 1 : 0 }); + } + + async retry(id: string): Promise { + await this.call({ mode: "retry", value: id }); + } +} diff --git a/webos/src/api/sonarr.ts b/webos/src/api/sonarr.ts new file mode 100644 index 0000000..007d5b8 --- /dev/null +++ b/webos/src/api/sonarr.ts @@ -0,0 +1,174 @@ +// Sonarr v3/v4 client, ported from internal/sonarr. + +import { ArrClient, type Image, type MediaFile, type QueueItem, type Release } from "./arr.ts"; + +export interface Statistics { + seasonCount?: number; + episodeFileCount: number; + episodeCount: number; + totalEpisodeCount?: number; + sizeOnDisk: number; +} + +export interface Season { + seasonNumber: number; + monitored: boolean; + statistics?: Statistics; + images?: Image[]; // only from getSeries +} + +export interface Series { + id: number; // 0 when not in the library + title: string; + year: number; + overview?: string; + network?: string; + status?: string; + runtime: number; + genres?: string[]; + tvdbId: number; + tmdbId?: number; + imdbId?: string; + titleSlug?: string; + seriesType?: string; + images?: Image[]; + ratings?: { votes: number; value: number }; + seasons?: Season[]; + monitored: boolean; + statistics?: Statistics; +} + +export interface Episode { + id: number; + seriesId: number; + seasonNumber: number; + episodeNumber: number; + title: string; + airDate?: string; + airDateUtc?: string; + overview?: string; + hasFile: boolean; + monitored: boolean; + series?: Series; +} + +export function seasonCount(s: Series): number { + return (s.seasons ?? []).filter((x) => x.seasonNumber > 0).length; +} + +export const monitorModes: [label: string, value: string][] = [ + ["All episodes", "all"], + ["Future episodes", "future"], + ["Missing episodes", "missing"], + ["First season", "firstSeason"], + ["Latest season", "latestSeason"], + ["None", "none"], +]; + +export interface AddSeriesOptions { + qualityProfileId: number; + rootFolderPath: string; + monitored: boolean; + monitor: string; + searchNow: boolean; +} + +export class SonarrClient extends ArrClient { + constructor(baseURL: string, apiKey: string) { + super("Sonarr", baseURL, apiKey); + } + + lookup(term: string, signal?: AbortSignal): Promise { + return this.do("GET", "series/lookup", { term }, undefined, { signal }); + } + + allSeries(): Promise { + return this.do("GET", "series", {}, undefined, { timeoutMs: 60_000 }); + } + + async seriesByTVDB(tvdbId: number): Promise { + const series = await this.do("GET", "series", { tvdbId }); + return series.find((s) => s.tvdbId === tvdbId && s.id) ?? null; + } + + // getSeries fetches a library series with its season posters. + getSeries(id: number): Promise { + return this.do("GET", `series/${id}`, { includeSeasonImages: true }); + } + + addSeries(s: Series, o: AddSeriesOptions): Promise { + return this.do("POST", "series", {}, { + title: s.title, + year: s.year, + tvdbId: s.tvdbId, + titleSlug: s.titleSlug, + seriesType: s.seriesType, + images: s.images, + seasons: s.seasons, + qualityProfileId: o.qualityProfileId, + rootFolderPath: o.rootFolderPath, + monitored: o.monitored, + seasonFolder: true, + addOptions: { monitor: o.monitor || "all", searchForMissingEpisodes: o.searchNow }, + }); + } + + searchSeries(id: number): Promise { + return this.command({ name: "SeriesSearch", seriesId: id }); + } + + searchSeason(id: number, season: number): Promise { + return this.command({ name: "SeasonSearch", seriesId: id, seasonNumber: season }); + } + + searchEpisode(episodeId: number): Promise { + return this.command({ name: "EpisodeSearch", episodeIds: [episodeId] }); + } + + setMonitored(id: number, monitored: boolean): Promise { + return this.update("series", id, (o) => { + o.monitored = monitored; + }); + } + + setSeasonMonitored(id: number, season: number, monitored: boolean): Promise { + return this.update("series", id, (o) => { + const seasons = o.seasons as { seasonNumber: number; monitored: boolean }[] | undefined; + const s = seasons?.find((x) => x.seasonNumber === season); + if (!s) throw new Error(`season ${season} not found`); + s.monitored = monitored; + }); + } + + setEpisodesMonitored(ids: number[], monitored: boolean): Promise { + return this.do("PUT", "episode/monitor", {}, { episodeIds: ids, monitored }); + } + + seriesQueue(): Promise { + return this.queue({ includeSeries: "true", includeEpisode: "true" }); + } + + episodes(seriesId: number): Promise { + return this.do("GET", "episode", { seriesId }); + } + + episodeReleases(episodeId: number, signal?: AbortSignal): Promise { + return this.releases({ episodeId }, signal); + } + + seasonReleases(seriesId: number, season: number, signal?: AbortSignal): Promise { + return this.releases({ seriesId, seasonNumber: season }, signal); + } + + episodeFiles(seriesId: number): Promise { + return this.do("GET", "episodefile", { seriesId }); + } + + deleteSeries(id: number, deleteFiles: boolean, addExclusion: boolean): Promise { + return this.do("DELETE", `series/${id}`, { deleteFiles, addImportListExclusion: addExclusion }); + } + + calendar(start: string, end: string, unmonitored: boolean): Promise { + return this.do("GET", "calendar", { start, end, unmonitored, includeSeries: true }); + } +} diff --git a/webos/src/api/trakt.ts b/webos/src/api/trakt.ts new file mode 100644 index 0000000..356f4de --- /dev/null +++ b/webos/src/api/trakt.ts @@ -0,0 +1,168 @@ +// Trakt's public lists and casts (client ID only), ported from internal/trakt. + +import { query, request, trimSlash } from "./http.ts"; + +export type Kind = "movie" | "show"; + +export interface List { + name: string; + path: string; + count: string; // field holding the list's number, "" for none + note?: (n: number) => string; + kind: Kind; +} + +export interface IDs { + trakt: number; + slug?: string; + imdb?: string; + tmdb?: number; + tvdb?: number; +} + +export interface Entry { + title: string; + year: number; + ids: IDs; + count: number; // the list's number for it, 0 if none +} + +export interface Person { + name: string; + role: string; +} + +export interface People { + cast: Person[]; + leaders: Person[]; // directors of a movie, creators of a show +} + +export interface Ref { + kind: Kind; + tmdb?: number; + tvdb?: number; + imdb?: string; +} + +export class TraktNotFound extends Error { + constructor() { + super("trakt: not found"); + } +} + +const watching = (n: number) => compact(n) + " watching"; +const watchers = (n: number) => compact(n) + " watched it"; +const onLists = (n: number) => "on " + compact(n) + " lists"; +const revenue = (n: number) => "$" + compact(n) + " last weekend"; + +export const movieLists: List[] = [ + { name: "Trending now", path: "/movies/trending", count: "watchers", note: watching, kind: "movie" }, + { name: "Most watched this week", path: "/movies/watched/weekly", count: "watcher_count", note: watchers, kind: "movie" }, + { name: "Anticipated", path: "/movies/anticipated", count: "list_count", note: onLists, kind: "movie" }, + { name: "Popular", path: "/movies/popular", count: "", kind: "movie" }, + { name: "Box office (US)", path: "/movies/boxoffice", count: "revenue", note: revenue, kind: "movie" }, +]; + +export const showLists: List[] = [ + { name: "Trending now", path: "/shows/trending", count: "watchers", note: watching, kind: "show" }, + { name: "Most watched this week", path: "/shows/watched/weekly", count: "watcher_count", note: watchers, kind: "show" }, + { name: "Anticipated", path: "/shows/anticipated", count: "list_count", note: onLists, kind: "show" }, + { name: "Popular", path: "/shows/popular", count: "", kind: "show" }, +]; + +export function listNote(l: List, n: number): string { + return l.note && n > 0 ? l.note(n) : ""; +} + +export function refKey(r: Ref): string { + const k = r.kind + ":"; + if (r.kind === "movie" && r.tmdb) return k + "tmdb:" + r.tmdb; + if (r.kind === "show" && r.tvdb) return k + "tvdb:" + r.tvdb; + if (r.imdb) return k + "imdb:" + r.imdb; + return ""; +} + +export class TraktClient { + baseURL: string; + clientID: string; + + constructor(clientID: string, baseURL = "https://api.trakt.tv") { + this.clientID = clientID.trim(); + this.baseURL = trimSlash(baseURL); + } + + configured(): boolean { + return this.clientID !== ""; + } + + async get(path: string, params: Record = {}): Promise { + if (!this.configured()) throw new Error("no Trakt client ID: create an API app on trakt.tv and enter its client ID in Settings"); + // No User-Agent: browsers do not let pages set it. + const resp = await request(this.baseURL + path + query(params), { + headers: { "Content-Type": "application/json", "trakt-api-key": this.clientID, "trakt-api-version": "2" }, + }, 20_000); + if (resp.status === 401 || resp.status === 403) throw new Error(`trakt: the client ID was not accepted (HTTP ${resp.status})`); + if (resp.status === 429) throw new Error("trakt: too many requests, try again in a few minutes"); + if (resp.status === 404) throw new TraktNotFound(); + if (!resp.ok) throw new Error(`trakt: HTTP ${resp.status}`); + return resp.json(); + } + + async entries(l: List, limit: number): Promise { + return decodeList(await this.get(l.path, { page: 1, limit }), l).slice(0, limit); + } + + async people(r: Ref): Promise { + const id = await this.traktID(r); + const raw = (await this.get(`/${r.kind}s/${id}/people`)) as { + cast?: { character?: string; characters?: string[]; person: { name: string } }[]; + crew?: Record; + }; + const p: People = { cast: [], leaders: [] }; + for (const m of raw.cast ?? []) { + const role = m.characters?.length ? m.characters.join(" / ") : (m.character ?? ""); + p.cast.push({ name: m.person.name, role }); + } + const [key, job] = r.kind === "show" ? ["created by", ""] : ["directing", "Director"]; + for (const m of raw.crew?.[key] ?? []) { + const jobs = [m.job ?? "", ...(m.jobs ?? [])]; + if (!job || jobs.includes(job)) p.leaders.push({ name: m.person.name, role: m.job ?? "" }); + } + return p; + } + + async traktID(r: Ref): Promise { + let type: string, id: string; + if (r.kind === "movie" && r.tmdb) [type, id] = ["tmdb", String(r.tmdb)]; + else if (r.kind === "show" && r.tvdb) [type, id] = ["tvdb", String(r.tvdb)]; + else if (r.imdb) [type, id] = ["imdb", r.imdb]; + else throw new TraktNotFound(); + const results = (await this.get(`/search/${type}/${encodeURIComponent(id)}`, { type: r.kind })) as Record[]; + for (const res of results) { + const t = res[r.kind]?.ids?.trakt; + if (t) return t; + } + throw new TraktNotFound(); + } +} + +// decodeList reads a list answer: bare titles, or items with the title under +// "movie"/"show" next to the list's number. +export function decodeList(data: unknown, l: List): Entry[] { + if (!Array.isArray(data)) throw new Error(`trakt: unexpected answer for ${l.path}`); + return data.map((item: Record) => { + const t = (item[l.kind] ?? item) as { title: string; year: number; ids: IDs }; + const n = l.count ? Number(item[l.count] ?? 0) : 0; + return { title: t.title, year: t.year, ids: t.ids ?? { trakt: 0 }, count: Number.isFinite(n) ? n : 0 }; + }); +} + +// compact writes a number briefly: 950, 1.2k, 34k, 1.5M. +export function compact(n: number): string { + const trim = (s: string) => s.replace(".0", ""); + if (n < 1000) return String(Math.trunc(n)); + if (n < 10_000) return trim((n / 1000).toFixed(1) + "k"); + if (n < 1_000_000) return Math.trunc(n / 1000) + "k"; + if (n < 10_000_000) return trim((n / 1_000_000).toFixed(1) + "M"); + return Math.trunc(n / 1_000_000) + "M"; +} diff --git a/webos/src/app.ts b/webos/src/app.ts new file mode 100644 index 0000000..782e45e --- /dev/null +++ b/webos/src/app.ts @@ -0,0 +1,191 @@ +// The services, shared caches and the page stack every view uses. + +import { RadarrClient } from "./api/radarr.ts"; +import { SonarrClient } from "./api/sonarr.ts"; +import { SABnzbdClient } from "./api/sabnzbd.ts"; +import { TraktClient, refKey, TraktNotFound, type People, type Ref } from "./api/trakt.ts"; +import { loadConfig, type Config } from "./config.ts"; +import { h } from "./ui/dom.ts"; +import { focusFirst, popLayer, pushLayer } from "./ui/focus.ts"; + +export type ServiceName = "Radarr" | "Sonarr" | "SABnzbd" | "Trakt"; + +class Services { + cfg: Config; + radarr: RadarrClient; + sonarr: SonarrClient; + sab: SABnzbdClient; + trakt: TraktClient; + // Called a few seconds after a grab (the Library's download marks). + afterGrab: (() => void)[] = []; + // Called when the config changes (Settings saved). + onConfig: (() => void)[] = []; + status: Record = { Radarr: "", Sonarr: "", SABnzbd: "", Trakt: "" }; // "" ok, else the error + onStatus: (() => void)[] = []; + + constructor() { + this.cfg = loadConfig(); + const c = this.cfg; + this.radarr = new RadarrClient(c.radarr.url, c.radarr.apikey); + this.sonarr = new SonarrClient(c.sonarr.url, c.sonarr.apikey); + this.sab = new SABnzbdClient(c.sabnzbd.url, c.sabnzbd.apikey); + this.trakt = new TraktClient(c.trakt.client_id ?? "", traktURL()); + } + + apply(c: Config): void { + this.cfg = c; + this.radarr.configure(c.radarr.url, c.radarr.apikey); + this.sonarr.configure(c.sonarr.url, c.sonarr.apikey); + this.sab.configure(c.sabnzbd.url, c.sabnzbd.apikey); + this.trakt = new TraktClient(c.trakt.client_id ?? "", traktURL()); + people.failed.clear(); + for (const f of this.onConfig) f(); + } + + setStatus(name: ServiceName, err: unknown): void { + const text = err ? (err instanceof Error ? err.message : String(err)) : ""; + if (this.status[name] === text) return; + this.status[name] = text; + for (const f of this.onStatus) f(); + } + + covers(): boolean { + return this.cfg.ui.covers !== false; + } +} + +// traktURL: another Trakt API for development (?trakt=http://127.0.0.1:19894). +function traktURL(): string | undefined { + return new URLSearchParams(location.search).get("trakt") ?? (window as unknown as { ARR_TUI_TRAKT_URL?: string }).ARR_TUI_TRAKT_URL; +} + +export const app = new Services(); + +// --- Pages: full-screen views opened on top (releases, episodes) --- + +export interface Page { + el: HTMLElement; + close: () => void; +} + +export function openPage(el: HTMLElement, onClose?: () => void): Page { + el.classList.add("page"); + document.getElementById("overlays")!.appendChild(el); + let closed = false; + const page: Page = { + el, + close() { + if (closed) return; + closed = true; + popLayer(el); + el.remove(); + onClose?.(); + }, + }; + pushLayer(el, page.close); + focusFirst(el); + return page; +} + +export function pageHeader(title: string, ...extra: (Node | string)[]): HTMLElement { + return h("div.page-header", h("h1", title), ...extra); +} + +// --- Stores kept on the TV (local storage), like arr-tui's disk cache --- + +interface Saved { + saved: number; // ms + data: T; +} + +class Store { + private key: string; + private ttl: number; + private cap: number; + private entries: Record> | null = null; + private timer = 0; + + constructor(key: string, ttlDays: number, cap: number) { + this.key = key; + this.ttl = ttlDays * 86400_000; + this.cap = cap; + } + + private load(): Record> { + if (!this.entries) { + try { + this.entries = JSON.parse(localStorage.getItem(this.key) ?? "{}"); + } catch { + this.entries = {}; + } + } + return this.entries!; + } + + get(k: string): T | undefined { + const e = this.load()[k]; + return e && Date.now() - e.saved < this.ttl ? e.data : undefined; + } + + put(k: string, data: T): void { + const all = this.load(); + all[k] = { saved: Date.now(), data }; + const keys = Object.keys(all); + if (keys.length > this.cap) { + keys.sort((a, b) => all[a].saved - all[b].saved); + for (const old of keys.slice(0, keys.length - this.cap)) delete all[old]; + } + clearTimeout(this.timer); + this.timer = window.setTimeout(() => this.save(), 3000); + } + + save(): void { + if (!this.entries) return; + const now = Date.now(); + for (const [k, e] of Object.entries(this.entries)) if (now - e.saved >= this.ttl) delete this.entries[k]; + try { + localStorage.setItem(this.key, JSON.stringify(this.entries)); + } catch { + /* full: kept for this run only */ + } + } +} + +// Lookups of Trakt list titles that are not in the library (3 days). +export const lookups = new Store("arr-tui-lookups", 3, 300); + +// Casts from Trakt (7 days). +const peopleStore = new Store("arr-tui-people", 7, 1000); + +export const people = { + failed: new Map(), + waiting: new Map>(), + + // load returns the cast of a title, from the store or Trakt. A title Trakt + // does not know has no cast. + load(r: Ref): Promise { + const key = refKey(r); + const known = peopleStore.get(key); + if (known) return Promise.resolve(known); + const failedAt = this.failed.get(key); + if (failedAt && Date.now() - failedAt < 120_000) return Promise.reject(new Error("cast not available")); + let p = this.waiting.get(key); + if (!p) { + p = app.trakt.people(r).catch((e) => { + if (e instanceof TraktNotFound) return { cast: [], leaders: [] }; + this.failed.set(key, Date.now()); + throw e; + }).then((ppl) => { + peopleStore.put(key, ppl); + return ppl; + }).finally(() => this.waiting.delete(key)); + this.waiting.set(key, p); + } + return p; + }, +}; + +window.addEventListener("beforeunload", () => { + lookups.save(); + peopleStore.save(); +}); diff --git a/webos/src/config.ts b/webos/src/config.ts new file mode 100644 index 0000000..a300e93 --- /dev/null +++ b/webos/src/config.ts @@ -0,0 +1,86 @@ +// The configuration: the arr-tui config file baked in at build time +// (config.js sets window.ARR_TUI_CONFIG), with what was changed in Settings +// on the TV kept in local storage on top. + +export interface Service { + url: string; + apikey: string; +} + +export interface Arr extends Service { + default_quality_profile?: string; + default_root_folder?: string; +} + +export interface Config { + radarr: Arr; + sonarr: Arr; + sabnzbd: Service; + trakt: { client_id?: string }; + ui: { + refresh_seconds?: number; + covers?: boolean; + notify_downloads?: boolean; + theme?: string; // auto, dark, light + }; +} + +const storageKey = "arr-tui-settings"; + +export function emptyConfig(): Config { + return { radarr: { url: "", apikey: "" }, sonarr: { url: "", apikey: "" }, sabnzbd: { url: "", apikey: "" }, trakt: {}, ui: {} }; +} + +// builtIn is the config the app was built with. +export function builtIn(): Config { + const raw = (window as unknown as { ARR_TUI_CONFIG?: Partial }).ARR_TUI_CONFIG; + return merge(emptyConfig(), raw ?? {}); +} + +export function loadConfig(): Config { + let saved: Partial = {}; + try { + saved = JSON.parse(localStorage.getItem(storageKey) ?? "{}"); + } catch { + /* unreadable: the built-in config */ + } + return merge(builtIn(), saved); +} + +export function saveConfig(c: Config): void { + try { + localStorage.setItem(storageKey, JSON.stringify(c)); + } catch { + /* storage full or blocked: the change lasts until the app closes */ + } +} + +export function resetConfig(): void { + try { + localStorage.removeItem(storageKey); + } catch { + /* nothing stored */ + } +} + +export function hasLocalChanges(): boolean { + try { + return localStorage.getItem(storageKey) !== null; + } catch { + return false; + } +} + +function merge(base: Config, over: Partial): Config { + return { + radarr: { ...base.radarr, ...over.radarr }, + sonarr: { ...base.sonarr, ...over.sonarr }, + sabnzbd: { ...base.sabnzbd, ...over.sabnzbd }, + trakt: { ...base.trakt, ...over.trakt }, + ui: { ...base.ui, ...over.ui }, + }; +} + +export function refreshSeconds(c: Config): number { + return c.ui.refresh_seconds && c.ui.refresh_seconds > 0 ? c.ui.refresh_seconds : 3; +} diff --git a/webos/src/describe.ts b/webos/src/describe.ts new file mode 100644 index 0000000..2a6a241 --- /dev/null +++ b/webos/src/describe.ts @@ -0,0 +1,82 @@ +// How movies and series are described in lists and the details pane. + +import { app } from "./app.ts"; +import { pad2 } from "./api/arr.ts"; +import { movieRating, type Movie } from "./api/radarr.ts"; +import { seasonCount, type Series } from "./api/sonarr.ts"; +import { formatSize, plural, runtime } from "./format.ts"; +import type { Detail, Line } from "./ui/detail.ts"; + +export function titleYear(t: { title: string; year: number }): string { + return t.year > 0 ? `${t.title} (${t.year})` : t.title; +} + +export function movieDetail(m: Movie, large = true): Detail { + const facts: string[] = []; + const r = movieRating(m); + if (r > 0) facts.push("★ " + r.toFixed(1)); + if (m.runtime > 0) facts.push(runtime(m.runtime)); + if (m.genres?.length) facts.push(m.genres.join(", ")); + const lines: Line[] = []; + if (m.studio) lines.push({ text: "Studio: " + m.studio }); + if (m.status) lines.push({ text: "Status: " + m.status }); + if (m.hasFile) lines.push({ text: "In library, downloaded", cls: "ok" }); + else if (m.id) lines.push({ text: "In library, not downloaded", cls: "warn" }); + return { + title: titleYear(m), + facts, + lines, + plot: m.overview ?? "", + poster: app.radarr.posterURL(m.images, !!m.id, large), + ref: { kind: "movie", tmdb: m.tmdbId, imdb: m.imdbId }, + }; +} + +export function seriesDetail(s: Series, large = true): Detail { + const facts: string[] = []; + if (s.ratings && s.ratings.value > 0) facts.push("★ " + s.ratings.value.toFixed(1)); + const n = seasonCount(s); + if (n > 0) facts.push(plural(n, "season")); + if (s.runtime > 0) facts.push(s.runtime + " min"); + if (s.genres?.length) facts.push(s.genres.join(", ")); + const lines: Line[] = []; + if (s.network) lines.push({ text: "Network: " + s.network }); + if (s.status) lines.push({ text: "Status: " + s.status }); + if (s.id) { + const st = s.statistics; + if (st && st.episodeCount > 0 && st.episodeFileCount >= st.episodeCount) lines.push({ text: "In library, all episodes downloaded", cls: "ok" }); + else lines.push({ text: `In library, ${st?.episodeFileCount ?? 0}/${st?.episodeCount ?? 0} episodes downloaded`, cls: "warn" }); + } + return { + title: titleYear(s), + facts, + lines, + plot: s.overview ?? "", + poster: app.sonarr.posterURL(s.images, !!s.id, large), + ref: { kind: "show", tvdb: s.tvdbId, imdb: s.imdbId }, + }; +} + +export function monitoredLine(m: boolean): Line { + return m ? { text: "Monitored: yes" } : { text: "Monitored: no", cls: "muted" }; +} + +export function sizeLine(b: number): Line | null { + return b > 0 ? { text: "Size on disk: " + formatSize(b) } : null; +} + +// Download state of a title in the Radarr/Sonarr queue. +export interface DlInfo { + count: number; + progress: number; + timeLeft: string; +} + +export function dlLine(d: DlInfo): Line { + if (d.count > 1) return { text: `Downloading ${d.count} episodes`, cls: "dl" }; + return { text: `Downloading ${Math.round(d.progress)}%` + (d.timeLeft ? ` · ${d.timeLeft} left` : ""), cls: "dl" }; +} + +export function episodeCode(season: number, episode: number): string { + return `S${pad2(season)}E${pad2(episode)}`; +} diff --git a/webos/src/format.ts b/webos/src/format.ts new file mode 100644 index 0000000..d03bed0 --- /dev/null +++ b/webos/src/format.ts @@ -0,0 +1,65 @@ +// Display helpers, ported from internal/ui/format.go and releases.go. + +export function formatSize(b: number): string { + const k = 1024; + if (b >= k * k * k) return (b / (k * k * k)).toFixed(1) + " GB"; + if (b >= k * k) return Math.round(b / (k * k)) + " MB"; + if (b > 0) return Math.round(b / k) + " KB"; + return "–"; +} + +export function formatReleaseAge(minutes: number): string { + if (minutes < 60) return Math.trunc(minutes) + "m"; + if (minutes < 48 * 60) return Math.trunc(minutes / 60) + "h"; + return Math.trunc(minutes / 1440) + "d"; +} + +export function formatSpeed(kbPerSec: number): string { + if (kbPerSec >= 1024) return (kbPerSec / 1024).toFixed(1) + " MB/s"; + if (kbPerSec >= 1) return Math.round(kbPerSec) + " KB/s"; + return "0 KB/s"; +} + +// formatAge renders a unix time relative to now ("5m ago", "3d ago"). +export function formatAge(unix: number, now = Date.now()): string { + if (unix <= 0) return ""; + const s = now / 1000 - unix; + if (s < 60) return "just now"; + if (s < 3600) return Math.trunc(s / 60) + "m ago"; + if (s < 48 * 3600) return Math.trunc(s / 3600) + "h ago"; + return Math.trunc(s / 86400) + "d ago"; +} + +export function runtime(min: number): string { + return `${Math.trunc(min / 60)}h${String(min % 60).padStart(2, "0")}m`; +} + +export function plural(n: number, word: string): string { + return `${n} ${word}${n === 1 ? "" : "s"}`; +} + +// isoDay is the local date as YYYY-MM-DD. +export function isoDay(d: Date): string { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; +} + +export function addDays(d: Date, n: number): Date { + const out = new Date(d); + out.setDate(out.getDate() + n); + return out; +} + +// dayLabel: "Today", "Tomorrow", "Yesterday" or "Mon 26 Oct". +export function dayLabel(day: string, today: string): string { + const d = new Date(day + "T12:00:00"); + const t = new Date(today + "T12:00:00"); + const diff = Math.round((d.getTime() - t.getTime()) / 86400000); + if (diff === 0) return "Today"; + if (diff === 1) return "Tomorrow"; + if (diff === -1) return "Yesterday"; + return d.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" }); +} + +export function capitalize(s: string): string { + return s ? s[0].toUpperCase() + s.slice(1) : s; +} diff --git a/webos/src/main.ts b/webos/src/main.ts new file mode 100644 index 0000000..0261de4 --- /dev/null +++ b/webos/src/main.ts @@ -0,0 +1,135 @@ +// arr-tui for webOS: the shell with the views, the status of the services, +// the remote's hints and its buttons. + +import { app } from "./app.ts"; +import { applyTheme } from "./theme.ts"; +import { h, replace } from "./ui/dom.ts"; +import { current, focus, focusFirst, initFocus, Keys } from "./ui/focus.ts"; +import { button, confirmDialog, toast } from "./ui/widgets.ts"; +import { CalendarView } from "./views/calendar.ts"; +import { DownloadsView, watchDownloads } from "./views/downloads.ts"; +import { LibraryView } from "./views/library.ts"; +import { LogView } from "./views/log.ts"; +import { SearchView } from "./views/search.ts"; +import { SettingsView } from "./views/settings.ts"; +import type { Hint, View } from "./views/view.ts"; + +interface Entry { + name: string; + view: View; + tab: HTMLButtonElement; +} + +function start(): void { + applyTheme(app.cfg.ui.theme); + const content = h("main.content"); + const hints = h("div.footer-hints"); + const dots = h("div.status-dots"); + const tabs = h("nav.tabs.fgroup"); + const entries: Entry[] = []; + let cur: Entry | null = null; + let switchTimer = 0; + + const show = (e: Entry, focusView: boolean) => { + if (cur === e) { + if (focusView) focusFirst(e.view.el); + return; + } + cur?.view.hide(); + cur?.tab.classList.remove("on"); + cur = e; + e.tab.classList.add("on"); + replace(content, e.view.el); + renderHints(e.view.hints); + e.view.show(); + if (!focusView && current() !== e.tab) focus(e.tab); + }; + + const views: [string, View][] = [ + ["Movies", new SearchView("movie")], + ["Series", new SearchView("series")], + ["Library", new LibraryView()], + ["Downloads", new DownloadsView()], + ["Calendar", new CalendarView()], + ["Settings", new SettingsView()], + ["Log", new LogView()], + ]; + views.forEach(([name, view], i) => { + const tab = button(`${i + 1} ${name}`, () => show(entry, true), "tab"); + const entry: Entry = { name, view, tab }; + // Moving along the tabs switches views once the focus rests there. + tab.addEventListener("focus", () => { + clearTimeout(switchTimer); + switchTimer = window.setTimeout(() => { + if (document.activeElement === tab) show(entry, false); + }, 250); + }); + entries.push(entry); + tabs.appendChild(tab); + }); + + function renderHints(list: Hint[]): void { + replace(hints, ...list.map(([b, text]) => h("span.hint", h(`span.key.${b}` as "span", b === "ok" ? "OK" : b === "back" ? "Back" : ""), text)), + h("span.hint", h("span.key.back", "Back"), "back")); + } + + function renderDots(): void { + replace(dots, ...(["Radarr", "Sonarr", "SABnzbd", "Trakt"] as const).map((name) => { + const configured = name === "Radarr" ? app.radarr.configured() : name === "Sonarr" ? app.sonarr.configured() + : name === "SABnzbd" ? app.sab.configured() : app.trakt.configured(); + const err = app.status[name]; + return h(`span.status-dot.${!configured ? "off" : err ? "bad" : "ok"}` as "span", { title: err || name }, name); + })); + } + app.onStatus.push(renderDots); + app.onConfig.push(renderDots); + renderDots(); + + const root = h("div.app", + h("header.topbar", h("div.brand", "arr-tui"), tabs, dots), + content, + h("footer.footer", hints), + ); + document.getElementById("root")!.appendChild(root); + + // Back: from a view to its tab; on the tabs, ask before leaving the app. + const back = () => { + const el = current(); + if (cur && el && !tabs.contains(el)) { + focus(cur.tab); + return; + } + confirmDialog("Leave arr-tui?", "Leave", () => window.close()); + }; + + const keys = (ev: KeyboardEvent): boolean => { + // 1-7 open a view. + const n = ev.keyCode >= 49 && ev.keyCode <= 57 ? ev.keyCode - 49 : ev.keyCode >= 97 && ev.keyCode <= 105 ? ev.keyCode - 97 : -1; + if (n >= 0 && n < entries.length) { + show(entries[n], true); + return true; + } + // Desktop browsers: F1-F4 stand in for the colour buttons. + const code = ({ 112: Keys.red, 113: Keys.green, 114: Keys.yellow, 115: Keys.blue } as Record)[ev.keyCode] ?? ev.keyCode; + return !!cur?.view.keys?.(code); + }; + initFocus(root, back, keys); + + const first = app.radarr.configured() || app.sonarr.configured() || app.sab.configured() ? 0 : 5; // nothing set up: Settings + show(entries[first], false); + watchDownloads(); + if (first === 5) toast("Nothing is configured yet: enter the addresses and API keys of your services.", "warn"); + + // Check the services once at start, for the status dots. + for (const [name, check] of [ + ["Radarr", () => app.radarr.systemStatus()], + ["Sonarr", () => app.sonarr.systemStatus()], + ["SABnzbd", () => app.sab.version()], + ] as const) { + const configured = name === "Radarr" ? app.radarr.configured() : name === "Sonarr" ? app.sonarr.configured() : app.sab.configured(); + if (configured) check().then(() => app.setStatus(name, null), (e) => app.setStatus(name, e)); + } +} + +if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); +else start(); diff --git a/webos/src/media.ts b/webos/src/media.ts new file mode 100644 index 0000000..707b931 --- /dev/null +++ b/webos/src/media.ts @@ -0,0 +1,200 @@ +// File and release properties and the property filter, ported from +// internal/arr/mediafile.go, internal/arr/release.go and internal/ui/libfilter.go. + +import type { MediaFile, Release } from "./api/arr.ts"; + +export interface FileAttrs { + resolution: number; // 2160, 1080, 720, 480 ... or 0 + codec: string; // x265, x264, AV1 or "" + hdr: boolean; + dv: boolean; + audio: string[]; + languages: string[]; +} + +export const resOptions = ["2160p", "1080p", "720p", "SD"]; +export const codecOptions = ["x265", "x264", "AV1"]; +export const hdrOptions = ["HDR", "DV", "SDR"]; +export const audioOptions = ["Atmos", "TrueHD", "DTS:X", "DTS-HD", "DTS", "DD+", "DD", "AAC", "FLAC", "Opus"]; + +const reRes = /\b(2160|1080|720|576|480)p\b/i; +const re265 = /(x\.?265|h\.?265|hevc)/i; +const re264 = /(x\.?264|h\.?264|\bavc\b)/i; +const reAV1 = /\bav1\b/i; +const reDV = /(\bdv\b|dovi|dolby[ .]?vision)/i; +const reAtmos = /(atmos|\bddpa\b)/i; +const reTrueHD = /true[ .-]?hd/i; +const reDTSX = /\bdts[ .:-]?x\b/i; +const reDTSHD = /dts[ .-]?(hd|ma|hra|es)/i; +const reDTS = /\bdts([^a-z]|$)/i; +const reDDPlus = /(e[ .-]?ac[ .-]?3|\bddp|dd\+|dolby digital plus)/i; +const reDD = /(\bac[ .-]?3\b|\bdd[ .]?[0-9]|dolby digital\b)/i; +const reAAC = /\baac/i; +const reFLAC = /\bflac\b/i; +const reOpus = /\bopus\b/i; +const reHDR = /\b(hdr(10(\+|plus)?)?|hlg|pq)\b/i; + +export function codecOf(s: string): string { + if (re265.test(s)) return "x265"; + if (re264.test(s)) return "x264"; + if (reAV1.test(s)) return "AV1"; + return ""; +} + +export function audioTags(s: string): string[] { + const tags: string[] = []; + if (reAtmos.test(s)) tags.push("Atmos"); + if (reTrueHD.test(s)) tags.push("TrueHD"); + const dtsx = reDTSX.test(s); + const dtshd = reDTSHD.test(s); + if (dtsx) tags.push("DTS:X"); + if (dtshd) tags.push("DTS-HD"); + if (dtsx || dtshd || reDTS.test(s)) tags.push("DTS"); + if (reDDPlus.test(s)) tags.push("DD+"); + else if (reDD.test(s)) tags.push("DD"); + if (reAAC.test(s)) tags.push("AAC"); + if (reFLAC.test(s)) tags.push("FLAC"); + if (reOpus.test(s)) tags.push("Opus"); + return tags; +} + +function resolutionIn(text: string): number { + const m = reRes.exec(text); + return m ? parseInt(m[1], 10) : 0; +} + +// fileAttrs uses Radarr/Sonarr's media info where present, else the names. +export function fileAttrs(f: MediaFile): FileAttrs { + const q = f.quality?.quality; + const names = [f.sceneName ?? "", f.relativePath ?? "", q?.name ?? ""].join(" "); + const mi = f.mediaInfo ?? {}; + let dr = `${mi.videoDynamicRangeType ?? ""} ${mi.videoDynamicRange ?? ""}`; + if (!dr.trim()) dr = names; + let audio = `${mi.audioCodec ?? ""} ${mi.audioAdditionalFeatures ?? ""}`; + if (!audio.trim()) audio = names; + return { + resolution: q?.resolution || resolutionIn(names), + codec: mi.videoCodec ? codecOf(mi.videoCodec) : codecOf(names), + dv: reDV.test(dr), + hdr: reHDR.test(dr), + audio: audioTags(audio), + languages: (f.languages ?? []).map((l) => l.name).filter(Boolean), + }; +} + +// releaseAttrs reads what a release announces: title, quality, custom formats. +export function releaseAttrs(r: Release): FileAttrs { + const q = r.quality?.quality; + const text = [r.title, q?.name ?? "", ...(r.customFormats ?? []).map((c) => c.name)].join(" "); + return { + resolution: q?.resolution || resolutionIn(text), + codec: codecOf(text), + dv: reDV.test(text), + hdr: reHDR.test(text), + audio: audioTags(text), + languages: (r.languages ?? []).map((l) => l.name).filter(Boolean), + }; +} + +// AttrFilter: "" means any. All fields must match the same file. +export interface AttrFilter { + res: string; + lang: string; + codec: string; + hdr: string; + audio: string; +} + +export const noFilter = (): AttrFilter => ({ res: "", lang: "", codec: "", hdr: "", audio: "" }); + +export function filterActive(f: AttrFilter): boolean { + return !!(f.res || f.lang || f.codec || f.hdr || f.audio); +} + +export function filterLabel(f: AttrFilter): string { + return [f.res, f.lang, f.codec, f.hdr, f.audio].filter(Boolean).join(" "); +} + +export function matchFile(f: AttrFilter, a: FileAttrs): boolean { + if (f.res === "SD") { + if (a.resolution <= 0 || a.resolution > 576) return false; + } else if (f.res && f.res !== a.resolution + "p") return false; + if (f.codec && f.codec !== a.codec) return false; + if (f.hdr === "HDR" && !a.hdr) return false; + if (f.hdr === "DV" && !a.dv) return false; + if (f.hdr === "SDR" && (a.hdr || a.dv)) return false; + if (f.audio && !a.audio.includes(f.audio)) return false; + if (f.lang && !a.languages.some((l) => l.toLowerCase() === f.lang.toLowerCase())) return false; + return true; +} + +// matchAny: any of an item's files (a series has several). None: no match. +export function matchAny(f: AttrFilter, files: FileAttrs[]): boolean { + return files.some((a) => matchFile(f, a)); +} + +// Pool is what the filter dialog works on: the files of every item that +// passes the other filters. unknown: some are not read yet, nothing can be +// ruled out. +export interface Pool { + files: FileAttrs[][]; + unknown: boolean; +} + +export function poolHas(p: Pool, f: AttrFilter): boolean { + return p.unknown || p.files.some((files) => matchAny(f, files)); +} + +export function poolLanguages(p: Pool): string[] { + const seen = new Set(); + for (const files of p.files) for (const a of files) for (const l of a.languages) seen.add(l); + return [...seen].sort(); +} + +export interface Facet { + label: string; + key: keyof AttrFilter; + candidates: (p: Pool) => string[]; +} + +// Most used first. +export const facets: Facet[] = [ + { label: "Quality", key: "res", candidates: () => resOptions }, + { label: "Language", key: "lang", candidates: poolLanguages }, + { label: "Codec", key: "codec", candidates: () => codecOptions }, + { label: "HDR", key: "hdr", candidates: () => hdrOptions }, + { label: "Audio", key: "audio", candidates: () => audioOptions }, +]; + +// listedValues: the options a facet offers, those that together with the +// other choices still leave a result, plus the current choice; "" first (any). +export function listedValues(fc: Facet, state: AttrFilter, pool: Pool): string[] { + const cur = state[fc.key]; + let cands = fc.candidates(pool); + if (cur && !cands.includes(cur)) cands = [cur, ...cands]; + const vals = [""]; + for (const o of cands) { + if (o === cur || poolHas(pool, { ...state, [fc.key]: o })) vals.push(o); + } + return vals; +} + +// --- Releases --- + +export type SortMode = "best" | "size" | "age"; + +export function isRejected(r: Release): boolean { + return r.rejected || (r.rejections?.length ?? 0) > 0; +} + +export function visibleReleases(all: Release[], hideRejected: boolean, mode: SortMode, af: AttrFilter): Release[] { + const out = all.filter((r) => !(hideRejected && isRejected(r)) && (!filterActive(af) || matchFile(af, releaseAttrs(r)))); + const cmp = (a: number, b: number) => (a < b ? -1 : a > b ? 1 : 0); + // Array.prototype.sort is stable in Chrome 70+; older TVs may reorder ties. + return out.sort((a, b) => { + if (mode === "size") return cmp(b.size, a.size); + if (mode === "age") return cmp(a.ageMinutes, b.ageMinutes); + return cmp(+isRejected(a), +isRejected(b)) || cmp(b.customFormatScore, a.customFormatScore) || + cmp(b.qualityWeight, a.qualityWeight) || cmp(a.ageMinutes, b.ageMinutes); + }); +} diff --git a/webos/src/seasons.ts b/webos/src/seasons.ts new file mode 100644 index 0000000..af72dd8 --- /dev/null +++ b/webos/src/seasons.ts @@ -0,0 +1,33 @@ +// Seasons of a series: counts, labels and episode states. + +import type { Episode } from "./api/sonarr.ts"; + +export interface SeasonSummary { + number: number; + have: number; + total: number; +} + +// summarizeSeasons counts the episodes per season, newest season first, +// specials last. +export function summarizeSeasons(eps: Episode[]): SeasonSummary[] { + const by = new Map(); + for (const e of eps) { + const s = by.get(e.seasonNumber) ?? { number: e.seasonNumber, have: 0, total: 0 }; + s.total++; + if (e.hasFile) s.have++; + by.set(e.seasonNumber, s); + } + return [...by.values()].sort((a, b) => (a.number === 0 ? 1 : b.number === 0 ? -1 : b.number - a.number)); +} + +export function seasonLabel(n: number): string { + return n === 0 ? "Specials" : "Season " + n; +} + +export function episodeStatus(e: Episode, today: string): [string, string] { + if (e.hasFile) return ["✓ downloaded", "ok"]; + if (!e.airDate || e.airDate > today) return ["unaired", "muted"]; + if (!e.monitored) return ["unmonitored", "muted"]; + return ["missing", "warn"]; +} diff --git a/webos/src/theme.ts b/webos/src/theme.ts new file mode 100644 index 0000000..f12ca5f --- /dev/null +++ b/webos/src/theme.ts @@ -0,0 +1,10 @@ +// Dark and light themes (CSS variables in style.css). "auto" follows the +// system's preference where the platform reports one, else dark. + +export function applyTheme(setting: string | undefined): void { + let theme = setting === "light" || setting === "dark" ? setting : "dark"; + if (!setting || setting === "auto") { + theme = window.matchMedia?.("(prefers-color-scheme: light)").matches ? "light" : "dark"; + } + document.documentElement.setAttribute("data-theme", theme); +} diff --git a/webos/src/ui/detail.ts b/webos/src/ui/detail.ts new file mode 100644 index 0000000..29b5f23 --- /dev/null +++ b/webos/src/ui/detail.ts @@ -0,0 +1,106 @@ +// The details pane: poster, title, facts, state, plot and the cast. + +import { app, people } from "../app.ts"; +import { refKey, type People, type Ref } from "../api/trakt.ts"; +import { h, replace } from "./dom.ts"; + +export interface Line { + text: string; + cls?: string; // ok, warn, muted, dl (downloading), bold +} + +export interface Detail { + title: string; + sub?: string; // a second heading line (an episode) + facts: string[]; + lines: Line[]; + plot: string; + poster: string; // URL, "" for none + ref?: Ref; // for the cast +} + +export class DetailPane { + el: HTMLElement; + private poster: HTMLImageElement; + private text: HTMLElement; + private cast: HTMLElement; + private castKey = ""; + private settle = 0; + private posterSettle = 0; + + constructor() { + this.poster = h("img.detail-poster", { alt: "" }); + this.poster.addEventListener("error", () => this.poster.classList.add("none")); + this.poster.addEventListener("load", () => this.poster.classList.remove("loading")); + this.text = h("div.detail-text"); + this.cast = h("div.detail-cast"); + this.el = h("div.detail", this.poster, this.text, this.cast); + } + + show(d: Detail | null): void { + if (!d) { + replace(this.text); + replace(this.cast); + this.poster.classList.add("none"); + this.castKey = ""; + return; + } + replace(this.text, + h("h2.detail-title", d.title), + d.sub ? h("div.detail-sub", d.sub) : null, + d.facts.length ? h("div.detail-facts", d.facts.join(" · ")) : null, + ...d.lines.map((l) => h(`div.detail-line${l.cls ? "." + l.cls : ""}` as "div", l.text)), + d.plot ? h("p.detail-plot", d.plot) : null, + ); + this.showPoster(app.covers() ? d.poster : ""); + this.showCast(d.ref); + } + + // showPoster waits until the selection rests (a held arrow key must not + // start a download per step). + private showPoster(url: string): void { + clearTimeout(this.posterSettle); + if (!url) { + this.poster.classList.add("none"); + this.poster.removeAttribute("src"); + return; + } + if (this.poster.getAttribute("src") === url) return; + this.poster.classList.remove("none"); + this.poster.classList.add("loading"); + this.posterSettle = window.setTimeout(() => { + this.poster.src = url; + }, 150); + } + + private showCast(ref: Ref | undefined): void { + const key = ref ? refKey(ref) : ""; + if (key === this.castKey) return; + this.castKey = key; + replace(this.cast); + clearTimeout(this.settle); + if (!ref || !key || !app.trakt.configured()) return; + this.settle = window.setTimeout(() => { + people.load(ref).then((p) => { + if (this.castKey === key) replace(this.cast, ...castLines(p, ref.kind)); + }, () => { + /* no cast: nothing to show */ + }); + }, 300); + } +} + +function castLines(p: People, kind: "movie" | "show"): HTMLElement[] { + const out: HTMLElement[] = []; + if (p.leaders.length) { + out.push(h("div.cast-head", kind === "show" ? "Created by" : "Director"), h("div.cast-names", p.leaders.map((l) => l.name).join(", "))); + } + if (p.cast.length) { + out.push(h("div.cast-head", "Cast")); + for (const c of p.cast.slice(0, 8)) { + out.push(h("div.cast-person", h("span", c.name), c.role ? h("span.muted", " as " + c.role) : null)); + } + if (p.cast.length > 8) out.push(h("div.muted", `and ${p.cast.length - 8} more`)); + } + return out; +} diff --git a/webos/src/ui/dom.ts b/webos/src/ui/dom.ts new file mode 100644 index 0000000..dcbe1c3 --- /dev/null +++ b/webos/src/ui/dom.ts @@ -0,0 +1,42 @@ +// Tiny DOM helpers: h("div.card", {onclick}, child, "text"). + +type Child = Node | string | number | null | undefined | false; +type Attrs = Record; + +export function h(spec: K | `${K}.${string}` | `${K}#${string}`, attrs?: Attrs | Child, ...children: Child[]): HTMLElementTagNameMap[K] { + const [tagAndId, ...classes] = spec.split("."); + const [tag, id] = tagAndId.split("#"); + const el = document.createElement(tag) as HTMLElementTagNameMap[K]; + if (id) el.id = id; + if (classes.length) el.className = classes.join(" "); + if (attrs && typeof attrs === "object" && !(attrs instanceof Node)) { + for (const [k, v] of Object.entries(attrs)) { + if (v === undefined || v === null || v === false) continue; + if (k.startsWith("on") && typeof v === "function") el.addEventListener(k.slice(2), v as EventListener); + else if (k === "class") el.className += (el.className ? " " : "") + v; + else if (k === "style") el.setAttribute("style", String(v)); + else if (k in el && typeof v !== "string") (el as unknown as Record)[k] = v; + else el.setAttribute(k, v === true ? "" : String(v)); + } + } else if (attrs !== undefined) { + children.unshift(attrs as Child); + } + append(el, ...children); + return el; +} + +export function append(el: Element, ...children: Child[]): void { + for (const c of children) { + if (c === null || c === undefined || c === false) continue; + el.appendChild(c instanceof Node ? c : document.createTextNode(String(c))); + } +} + +export function clear(el: Element): void { + while (el.firstChild) el.removeChild(el.firstChild); +} + +export function replace(el: Element, ...children: Child[]): void { + clear(el); + append(el, ...children); +} diff --git a/webos/src/ui/focus.ts b/webos/src/ui/focus.ts new file mode 100644 index 0000000..ba82e57 --- /dev/null +++ b/webos/src/ui/focus.ts @@ -0,0 +1,222 @@ +// Remote-control focus: the arrow keys move between focusable elements by +// their position on screen (spatial navigation), OK presses, Back goes back. +// +// Focusable elements carry the class "f". Dialogs open a layer: navigation +// stays inside the topmost layer, and closing it returns the focus to where +// it was. Elements can handle keys themselves (a list moves its selection +// with up and down) by setting onKey; returning true stops the navigation. +// A group (class "fgroup") remembers its last focused element, so moving back +// into a group continues where one left it. + +export const Keys = { + left: 37, + up: 38, + right: 39, + down: 40, + enter: 13, + back: 461, // webOS remote + escape: 27, + backspace: 8, + red: 403, + green: 404, + yellow: 405, + blue: 406, + pageUp: 33, // also channel up on the remote + pageDown: 34, +} as const; + +export type Dir = "left" | "right" | "up" | "down"; + +export interface KeyHandling { + onKey?: (ev: KeyboardEvent) => boolean; +} + +interface Layer { + root: HTMLElement; + back: () => void; + restore: HTMLElement | null; // focused when the layer opened +} + +const layers: Layer[] = []; +let globalKeys: (ev: KeyboardEvent) => boolean = () => false; + +export function initFocus(root: HTMLElement, back: () => void, keys: (ev: KeyboardEvent) => boolean): void { + layers.length = 0; + layers.push({ root, back, restore: null }); + globalKeys = keys; + document.addEventListener("keydown", onKeyDown, true); + // The Magic Remote's pointer: hovering focuses, like moving there. + document.addEventListener("mouseover", (ev) => { + const el = (ev.target as Element | null)?.closest?.(".f") as HTMLElement | null; + if (el && inLayer(el) && el !== document.activeElement && !isTextInput(document.activeElement)) focus(el, false); + }); +} + +export function pushLayer(root: HTMLElement, back: () => void): void { + layers.push({ root, back, restore: current() }); +} + +// popLayer closes the topmost layer (root must be it) and refocuses. +export function popLayer(root: HTMLElement): void { + const i = layers.findIndex((l) => l.root === root); + if (i <= 0) return; + const [layer] = layers.splice(i, 1); + if (i === layers.length) { + const back = layer.restore; + if (back && back.isConnected && visible(back)) focus(back); + else focusFirst(top().root); + } +} + +export function top(): Layer { + return layers[layers.length - 1]; +} + +export function current(): HTMLElement | null { + const a = document.activeElement as HTMLElement | null; + return a && a !== document.body && a.classList?.contains("f") ? a : null; +} + +export function focus(el: HTMLElement, scroll = true): void { + if (!el.hasAttribute("tabindex") && !isTextInput(el)) el.tabIndex = -1; + el.focus({ preventScroll: true }); + if (scroll) el.scrollIntoView({ block: "nearest", inline: "nearest" }); + const g = el.closest(".fgroup") as (HTMLElement & { lastFocus?: HTMLElement }) | null; + if (g) g.lastFocus = el; +} + +// focusFirst focuses the first focusable element in root (or a remembered +// one of its group). +export function focusFirst(root: HTMLElement): boolean { + const g = root as HTMLElement & { lastFocus?: HTMLElement }; + if (g.lastFocus && g.lastFocus.isConnected && visible(g.lastFocus) && root.contains(g.lastFocus)) { + focus(g.lastFocus); + return true; + } + const el = focusables(root)[0]; + if (el) focus(el); + return !!el; +} + +export function focusables(root: Element): HTMLElement[] { + return Array.from(root.querySelectorAll(".f")).filter(visible); +} + +function visible(el: HTMLElement): boolean { + if (el.classList.contains("f-off") || (el as HTMLButtonElement).disabled) return false; + const r = el.getBoundingClientRect(); + return r.width > 0 && r.height > 0; +} + +function inLayer(el: Element): boolean { + return top().root.contains(el); +} + +function isTextInput(el: Element | null): el is HTMLInputElement { + return !!el && el.tagName === "INPUT" && ["text", "search", "password", "url", ""].includes((el as HTMLInputElement).type); +} + +export function isBackKey(ev: KeyboardEvent): boolean { + if (ev.keyCode === Keys.back || ev.keyCode === Keys.escape) return true; + return ev.keyCode === Keys.backspace && !isTextInput(document.activeElement); +} + +function onKeyDown(ev: KeyboardEvent): void { + const el = current(); + if (el && inLayer(el) && (el as KeyHandling).onKey?.(ev)) { + ev.preventDefault(); + return; + } + if (isBackKey(ev)) { + if (isTextInput(document.activeElement) && ev.keyCode === Keys.escape && document.activeElement.value) { + return; // let Escape do nothing special in a field with text + } + ev.preventDefault(); + top().back(); + return; + } + const dir = ({ 37: "left", 38: "up", 39: "right", 40: "down" } as Record)[ev.keyCode]; + if (dir) { + if (isTextInput(el) && (dir === "left" || dir === "right") && caretInside(el, dir)) return; + ev.preventDefault(); + if (!el || !inLayer(el)) { + focusFirst(top().root); + return; + } + const next = neighbour(el, dir, focusables(top().root)); + if (next) focus(enterGroup(el, next)); + return; + } + if (ev.keyCode === Keys.enter && el && !isTextInput(el)) { + ev.preventDefault(); + el.click(); + return; + } + // View keys (numbers, colour buttons) only while no dialog or page is open. + if (layers.length === 1 && !isTextInput(document.activeElement) && globalKeys(ev)) ev.preventDefault(); +} + +function caretInside(input: HTMLInputElement, dir: Dir): boolean { + const pos = input.selectionStart ?? 0; + return dir === "left" ? pos > 0 : pos < input.value.length; +} + +// enterGroup: moving into another group goes to the element remembered there. +function enterGroup(from: HTMLElement, to: HTMLElement): HTMLElement { + const g = to.closest(".fgroup") as (HTMLElement & { lastFocus?: HTMLElement }) | null; + if (!g || g.contains(from)) return to; + const last = g.lastFocus; + return last && last.isConnected && visible(last) && g.contains(last) ? last : to; +} + +// neighbour finds the element to move to: the nearest one in the direction, +// preferring those that overlap the current one across the direction (the +// card right below, not the one diagonally closer). +export function neighbour(from: HTMLElement, dir: Dir, candidates: HTMLElement[]): HTMLElement | null { + const c = from.getBoundingClientRect(); + let best: HTMLElement | null = null; + let bestScore = Infinity; + for (const el of candidates) { + if (el === from || el.contains(from) || from.contains(el)) continue; + const r = el.getBoundingClientRect(); + const slack = 4; + let gap: number, cross: number; + switch (dir) { + case "right": + if (r.left < c.right - slack) continue; + gap = r.left - c.right; + cross = intervalGap(c.top, c.bottom, r.top, r.bottom); + break; + case "left": + if (r.right > c.left + slack) continue; + gap = c.left - r.right; + cross = intervalGap(c.top, c.bottom, r.top, r.bottom); + break; + case "down": + if (r.top < c.bottom - slack) continue; + gap = r.top - c.bottom; + cross = intervalGap(c.left, c.right, r.left, r.right); + break; + case "up": + if (r.bottom > c.top + slack) continue; + gap = c.top - r.bottom; + cross = intervalGap(c.left, c.right, r.left, r.right); + break; + } + // Among overlapping ones, the one whose start is nearest wins (a list + // of columns under a wide bar: the left-most below it, not a random one). + const align = dir === "up" || dir === "down" ? Math.abs(r.left - c.left) : Math.abs(r.top - c.top); + const score = Math.max(gap, 0) + (cross > 0 ? 10000 + cross * 3 : align * 0.01); + if (score < bestScore) { + bestScore = score; + best = el; + } + } + return best; +} + +function intervalGap(a1: number, a2: number, b1: number, b2: number): number { + if (b2 <= a1) return a1 - b2; + if (b1 >= a2) return b1 - a2; + return 0; +} diff --git a/webos/src/ui/list.ts b/webos/src/ui/list.ts new file mode 100644 index 0000000..ef1c65d --- /dev/null +++ b/webos/src/ui/list.ts @@ -0,0 +1,188 @@ +// ListView: a focusable list with its own selection, like the TUI's tables. +// Up and down move the selection (and leave the list at its ends), channel +// up / down page, OK activates. Only the rows in view are in the DOM, so a +// library of thousands of titles stays quick on TV hardware. + +import { h, replace } from "./dom.ts"; +import { Keys, type KeyHandling } from "./focus.ts"; + +export interface Row { + item?: T; // undefined: a heading (not selectable) + key: string; +} + +export interface ListOptions { + rowHeight: number; // pixels + render: (item: T, selected: boolean) => HTMLElement; + heading?: (key: string) => HTMLElement; + onSelect?: (item: T | undefined) => void; + onActivate?: (item: T) => void; + placeholder?: string; + header?: HTMLElement; // column titles, above the rows +} + +export class ListView { + el: HTMLElement & KeyHandling; + private body: HTMLElement; + private rows: Row[] = []; + private sel = -1; + private top = 0; + private opts: ListOptions; + private placeholder: string; + + constructor(cls: string, opts: ListOptions) { + this.opts = opts; + this.placeholder = opts.placeholder ?? ""; + this.body = h("div.list-body"); + opts.header?.classList.add("list-row"); + this.el = h(`div.list.f.${cls}`, {}, opts.header ?? null, this.body) as HTMLElement & KeyHandling; + this.el.tabIndex = -1; + this.el.onKey = (ev) => this.key(ev); + this.el.addEventListener("focus", () => this.draw()); + this.el.addEventListener("blur", () => this.draw()); + this.el.addEventListener("wheel", (ev) => { + ev.preventDefault(); + this.move(ev.deltaY > 0 ? 3 : -3); + }, { passive: false }); + this.body.addEventListener("click", (ev) => { + const rowEl = (ev.target as Element).closest("[data-i]") as HTMLElement | null; + if (!rowEl) return; + const i = Number(rowEl.dataset.i); + if (i === this.sel) this.activate(); + else this.select(i); + }); + new ResizeObserverLike(this.body, () => this.draw()); + } + + get selected(): T | undefined { + return this.rows[this.sel]?.item; + } + + get length(): number { + return this.rows.filter((r) => r.item !== undefined).length; + } + + setPlaceholder(text: string): void { + this.placeholder = text; + if (!this.rows.length) this.draw(); + } + + // setRows replaces the rows, keeping the selection on the same key, or at + // the same place if that row is gone (removed: the next one moves up). + setRows(rows: Row[], keepPlace = true): void { + const oldKey = this.rows[this.sel]?.key; + const oldIndex = this.sel; + this.rows = rows; + let sel = oldKey !== undefined ? rows.findIndex((r) => r.key === oldKey) : -1; + if (sel < 0 && keepPlace && oldIndex >= 0) sel = Math.min(oldIndex, rows.length - 1); + if (sel < 0) sel = 0; + this.sel = this.nearestItem(sel, 1); + if (this.sel < 0) this.sel = this.nearestItem(rows.length - 1, -1); + this.draw(); + this.opts.onSelect?.(this.selected); + } + + setItems(items: T[], key: (t: T) => string): void { + this.setRows(items.map((item) => ({ item, key: key(item) }))); + } + + // redraw re-renders the visible rows (an item changed in place). + redraw(): void { + this.draw(); + } + + select(i: number): void { + const n = this.nearestItem(i, i >= this.sel ? 1 : -1); + if (n < 0 || n === this.sel) return; + this.sel = n; + this.draw(); + this.opts.onSelect?.(this.selected); + } + + selectKey(key: string): void { + const i = this.rows.findIndex((r) => r.key === key); + if (i >= 0) this.select(i); + } + + private nearestItem(i: number, step: number): number { + for (let j = i; j >= 0 && j < this.rows.length; j += step) if (this.rows[j].item !== undefined) return j; + return -1; + } + + private activate(): void { + const it = this.selected; + if (it !== undefined) this.opts.onActivate?.(it); + } + + private move(delta: number): boolean { + const target = this.nearestItem(Math.max(0, Math.min(this.rows.length - 1, this.sel + delta)), delta > 0 ? 1 : -1); + const fallback = this.nearestItem(Math.max(0, Math.min(this.rows.length - 1, this.sel + delta)), delta > 0 ? -1 : 1); + const n = target >= 0 ? target : fallback; + if (n < 0 || n === this.sel) return false; + this.select(n); + return true; + } + + private key(ev: KeyboardEvent): boolean { + switch (ev.keyCode) { + case Keys.up: + return this.move(-1); + case Keys.down: + return this.move(1); + case Keys.pageUp: + this.move(-this.visibleRows()); + return true; + case Keys.pageDown: + this.move(this.visibleRows()); + return true; + case Keys.enter: + this.activate(); + return true; + } + return false; + } + + private visibleRows(): number { + return Math.max(1, Math.floor(this.body.clientHeight / this.opts.rowHeight)); + } + + private draw(): void { + const n = this.visibleRows(); + if (this.sel < this.top) this.top = this.sel; + if (this.sel >= this.top + n) this.top = this.sel - n + 1; + this.top = Math.max(0, Math.min(this.top, Math.max(0, this.rows.length - n))); + const focused = document.activeElement === this.el; + const out: HTMLElement[] = []; + if (!this.rows.length && this.placeholder) out.push(h("div.list-placeholder", this.placeholder)); + for (let i = this.top; i < Math.min(this.rows.length, this.top + n); i++) { + const row = this.rows[i]; + let el: HTMLElement; + if (row.item === undefined) { + el = this.opts.heading ? this.opts.heading(row.key) : h("div.list-heading", row.key); + } else { + el = this.opts.render(row.item, i === this.sel); + el.dataset.i = String(i); + if (i === this.sel) el.classList.add(focused ? "sel" : "sel-idle"); + } + el.classList.add("list-row"); + el.style.height = this.opts.rowHeight + "px"; + out.push(el); + } + if (this.rows.length > n) { + const pct = this.top / (this.rows.length - n); + const bar = h("div.list-scroll", h("div.list-thumb", { style: `height:${Math.max(8, (n / this.rows.length) * 100)}%;top:${pct * (100 - Math.max(8, (n / this.rows.length) * 100))}%` })); + out.push(bar); + } + replace(this.body, ...out); + } +} + +// ResizeObserverLike redraws when the element changes size: ResizeObserver +// (Chrome 64+), else on window resizes. +class ResizeObserverLike { + constructor(el: HTMLElement, cb: () => void) { + const RO = (window as unknown as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver; + if (RO) new RO(() => cb()).observe(el); + else window.addEventListener("resize", cb); + } +} diff --git a/webos/src/ui/widgets.ts b/webos/src/ui/widgets.ts new file mode 100644 index 0000000..17a3fdb --- /dev/null +++ b/webos/src/ui/widgets.ts @@ -0,0 +1,206 @@ +// Dialogs, action menus, option pickers, buttons and toasts. + +import { append, h, replace } from "./dom.ts"; +import { focus, focusFirst, Keys, popLayer, pushLayer, type KeyHandling } from "./focus.ts"; + +export function button(label: string, onClick: () => void, cls = ""): HTMLButtonElement { + return h(`button.btn.f${cls ? "." + cls : ""}` as "button", { onclick: onClick, tabindex: -1 }, label); +} + +// Dialog: a centred box over everything; Back closes it. +export class Dialog { + el: HTMLElement; + box: HTMLElement; + private onClose?: () => void; + private closed = false; + + constructor(title: string, cls = "", onClose?: () => void) { + this.onClose = onClose; + this.box = h("div.dialog-box", h("h2", title)); + if (cls) this.box.classList.add(cls); + this.el = h("div.dialog", this.box); + } + + add(...children: (Node | string | null)[]): this { + append(this.box, ...children); + return this; + } + + // open shows the dialog; called again (content changed), it only moves + // the focus. + open(focusEl?: HTMLElement): this { + if (this.closed) return this; + if (!this.el.isConnected) { + document.getElementById("overlays")!.appendChild(this.el); + pushLayer(this.el, () => this.close()); + } + if (focusEl) focus(focusEl); + else focusFirst(this.el); + return this; + } + + close(): void { + if (this.closed) return; + this.closed = true; + popLayer(this.el); + this.el.remove(); + this.onClose?.(); + } +} + +export interface Action { + label: string; + run: () => void; + danger?: boolean; + hint?: string; +} + +// actionSheet lists what can be done with an item; OK runs one. +export function actionSheet(title: string, actions: Action[]): void { + const d = new Dialog(title, "sheet"); + for (const a of actions) { + const b = button(a.label, () => { + d.close(); + a.run(); + }, a.danger ? "danger" : ""); + if (a.hint) b.appendChild(h("span.btn-hint", a.hint)); + d.add(b); + } + d.add(button("Cancel", () => d.close(), "ghost")); + d.open(); +} + +export function confirmDialog(message: string, yes: string, run: () => void, danger = false): void { + const d = new Dialog("Are you sure?"); + d.add(h("p.dialog-text", message)); + const no = button("Cancel", () => d.close(), "ghost"); + d.add(h("div.row-buttons", button(yes, () => { + d.close(); + run(); + }, danger ? "danger" : ""), no)); + d.open(no); +} + +// Choice: a value picked from options with left / right (or OK for the +// next one), the TV-friendly dropdown. +export class Choice { + el: HTMLElement & KeyHandling; + private valueEl: HTMLElement; + options: { label: string; value: V }[] = []; + index = 0; + onChange?: (v: V) => void; + + constructor(label: string, options: { label: string; value: V }[], value?: V, onChange?: (v: V) => void) { + this.valueEl = h("span.choice-value"); + this.el = h("div.choice.f", { tabindex: -1 }, h("span.choice-label", label), h("span.choice-arrows", "‹", this.valueEl, "›")) as HTMLElement & KeyHandling; + this.onChange = onChange; + this.setOptions(options, value); + this.el.onKey = (ev) => { + if (ev.keyCode !== Keys.left && ev.keyCode !== Keys.right) return false; + this.step(ev.keyCode === Keys.left ? -1 : 1); + return true; + }; + this.el.addEventListener("click", (ev) => { + const r = this.el.getBoundingClientRect(); + const x = (ev as MouseEvent).clientX; + // A pointer click on the left third goes back; OK and other clicks forward. + this.step(x > 0 && x < r.left + r.width / 3 ? -1 : 1); + }); + } + + get value(): V { + return this.options[this.index]?.value; + } + + setOptions(options: { label: string; value: V }[], value?: V): void { + this.options = options; + const i = value === undefined ? -1 : options.findIndex((o) => o.value === value); + this.index = i >= 0 ? i : Math.min(this.index, Math.max(0, options.length - 1)); + this.show(); + } + + private step(d: number): void { + if (this.options.length < 2) return; + this.index = (this.index + d + this.options.length) % this.options.length; + this.show(); + this.onChange?.(this.value); + } + + private show(): void { + replace(this.valueEl, this.options[this.index]?.label ?? "–"); + } +} + +// Toggle: a checkbox; OK flips it. +export class Toggle { + el: HTMLElement; + value: boolean; + onChange?: (v: boolean) => void; + + constructor(label: string, value: boolean, onChange?: (v: boolean) => void) { + this.value = value; + this.onChange = onChange; + this.el = h("div.toggle.f", { tabindex: -1, onclick: () => this.set(!this.value, true) }, h("span.toggle-box"), h("span", label)); + this.set(value, false); + } + + set(v: boolean, notify: boolean): void { + this.value = v; + this.el.classList.toggle("on", v); + if (notify) this.onChange?.(v); + } +} + +export function textField(label: string, value: string, opts: { secret?: boolean; placeholder?: string; onInput?: (v: string) => void } = {}): { el: HTMLElement; input: HTMLInputElement } { + const input = h("input.f", { type: opts.secret ? "password" : "text", value, placeholder: opts.placeholder ?? "", spellcheck: false, autocomplete: "off" }); + if (opts.onInput) input.addEventListener("input", () => opts.onInput!(input.value)); + return { el: h("label.field", h("span.field-label", label), input), input }; +} + +// --- Toasts and the log --- + +export type Level = "info" | "ok" | "warn" | "error"; + +export interface LogEntry { + at: Date; + level: Level; + text: string; +} + +export const log: LogEntry[] = []; +const logListeners: (() => void)[] = []; + +export function onLog(f: () => void): void { + logListeners.push(f); +} + +// toast shows a message for a few seconds and keeps it in the log. +export function toast(text: string, level: Level = "info"): void { + log.push({ at: new Date(), level, text }); + if (log.length > 500) log.splice(0, log.length - 500); + for (const f of logListeners) f(); + const box = document.getElementById("toasts"); + if (!box) return; + const el = h(`div.toast.toast-${level}` as "div", text); + box.appendChild(el); + while (box.children.length > 4) box.firstElementChild!.remove(); + setTimeout(() => el.classList.add("gone"), level === "error" ? 7000 : 4000); + setTimeout(() => el.remove(), level === "error" ? 7600 : 4600); +} + +export function errorText(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + +// run executes an action with a "…" toast and a result toast. +export async function run(doing: string, done: string, f: () => Promise): Promise { + toast(doing + "…"); + try { + await f(); + toast(done, "ok"); + return true; + } catch (e) { + toast(`${doing} failed: ${errorText(e)}`, "error"); + return false; + } +} diff --git a/webos/src/views/actions.ts b/webos/src/views/actions.ts new file mode 100644 index 0000000..fcf7439 --- /dev/null +++ b/webos/src/views/actions.ts @@ -0,0 +1,74 @@ +// What can be done with a library title, shared by Movies, Series, Library +// and Calendar. + +import { app } from "../app.ts"; +import type { Movie } from "../api/radarr.ts"; +import type { Series } from "../api/sonarr.ts"; +import { titleYear } from "../describe.ts"; +import { h } from "../ui/dom.ts"; +import { actionSheet, button, Dialog, run, Toggle, type Action } from "../ui/widgets.ts"; +import { openEpisodes } from "./episodes.ts"; +import { openReleases } from "./releases.ts"; + +export function movieReleases(m: Movie): void { + openReleases("Releases for " + titleYear(m), (signal) => app.radarr.movieReleases(m.id, signal), (r) => app.radarr.grab(r)); +} + +// movieActions: changed runs after something changed the movie (reload). +export function movieActions(m: Movie, changed: () => void, removed?: () => void): Action[] { + const name = titleYear(m); + return [ + { label: "Search releases", hint: "interactive", run: () => movieReleases(m) }, + { label: "Automatic search", run: () => void run("Searching " + name, "Search started: " + name, () => app.radarr.searchMovie(m.id)) }, + { + label: m.monitored ? "Unmonitor" : "Monitor", + run: () => void run(name + ": " + (m.monitored ? "unmonitoring" : "monitoring"), "Done: " + name, () => app.radarr.setMonitored(m.id, !m.monitored)).then((ok) => ok && changed()), + }, + { label: "Remove from library…", danger: true, run: () => removeDialog(name, false, (files, excl) => app.radarr.deleteMovie(m.id, files, excl), removed ?? changed) }, + ]; +} + +export function seriesActions(s: Series, changed: () => void, removed?: () => void): Action[] { + const name = titleYear(s); + return [ + { label: "Seasons and episodes", run: () => openEpisodes(s, changed) }, + { + label: "Search all missing episodes", + run: () => actionSheet(`Search all missing episodes of ${name}?`, [ + { label: "Search", run: () => void run("Searching " + name, "Search started: " + name, () => app.sonarr.searchSeries(s.id)) }, + ]), + }, + { + label: s.monitored ? "Unmonitor" : "Monitor", + run: () => void run(name + ": " + (s.monitored ? "unmonitoring" : "monitoring"), "Done: " + name, () => app.sonarr.setMonitored(s.id, !s.monitored)).then((ok) => ok && changed()), + }, + { label: "Remove from library…", danger: true, run: () => removeDialog(name, true, (files, excl) => app.sonarr.deleteSeries(s.id, files, excl), removed ?? changed) }, + ]; +} + +export function showMovieActions(m: Movie, changed: () => void, removed?: () => void): void { + actionSheet(titleYear(m), movieActions(m, changed, removed)); +} + +export function showSeriesActions(s: Series, changed: () => void, removed?: () => void): void { + actionSheet(titleYear(s), seriesActions(s, changed, removed)); +} + +// removeDialog asks before removing; files and the import exclusion are +// separate choices, both off. +function removeDialog(name: string, series: boolean, remove: (files: boolean, exclusion: boolean) => Promise, removed: () => void): void { + const d = new Dialog("Remove " + name); + const files = new Toggle(series ? "Delete all episode files" : "Delete the movie's files", false); + const excl = new Toggle("Do not add again from import lists", false); + const cancel = button("Cancel", () => d.close(), "ghost"); + d.add( + h("p.dialog-text", "The title leaves the library. Files stay on disk unless you tick the box."), + files.el, + excl.el, + h("div.row-buttons", button("Remove", () => { + d.close(); + void run("Removing " + name, "Removed " + name, () => remove(files.value, excl.value)).then((ok) => ok && removed()); + }, "danger"), cancel), + ); + d.open(cancel); +} diff --git a/webos/src/views/add.ts b/webos/src/views/add.ts new file mode 100644 index 0000000..bc4e4ed --- /dev/null +++ b/webos/src/views/add.ts @@ -0,0 +1,70 @@ +// The add dialog: quality profile, root folder, monitoring and an optional +// automatic search, with the defaults from the config. + +import { app } from "../app.ts"; +import type { QualityProfile, RootFolder } from "../api/arr.ts"; +import type { Movie } from "../api/radarr.ts"; +import { monitorModes, type Series } from "../api/sonarr.ts"; +import { titleYear } from "../describe.ts"; +import { formatSize } from "../format.ts"; +import { h } from "../ui/dom.ts"; +import { button, Choice, Dialog, errorText, toast, Toggle } from "../ui/widgets.ts"; + +export type AddTarget = { kind: "movie"; movie: Movie } | { kind: "series"; series: Series }; + +// openAdd asks for the options and adds the title; added gets the new id. +export function openAdd(t: AddTarget, added: (id: number) => void): void { + const client = t.kind === "movie" ? app.radarr : app.sonarr; + const defaults = t.kind === "movie" ? app.cfg.radarr : app.cfg.sonarr; + const name = t.kind === "movie" ? titleYear(t.movie) : titleYear(t.series); + const d = new Dialog("Add " + name); + const loading = h("p.dialog-text", "Loading quality profiles and root folders…"); + d.add(loading).open(); + + Promise.all([client.qualityProfiles(), client.rootFolders()]).then(([profiles, folders]) => { + loading.remove(); + build(profiles, folders); + }, (e) => { + loading.textContent = `${client.name}: ${errorText(e)}`; + d.add(button("Close", () => d.close())); + d.open(); + }); + + function build(profiles: QualityProfile[], folders: RootFolder[]): void { + if (!profiles.length || !folders.length) { + d.add(h("p.dialog-text", `${client.name} has no ${profiles.length ? "root folders" : "quality profiles"} set up.`), button("Close", () => d.close())); + d.open(); + return; + } + const profile = new Choice("Quality profile", profiles.map((p) => ({ label: p.name, value: p.id })), + profiles.find((p) => p.name === defaults.default_quality_profile)?.id); + const folder = new Choice("Root folder", folders.map((f) => ({ label: `${f.path} (${formatSize(f.freeSpace)} free)`, value: f.path })), + folders.find((f) => f.path === defaults.default_root_folder)?.path); + const monitored = new Toggle("Monitored", true); + const mode = t.kind === "series" ? new Choice("Monitor", monitorModes.map(([label, value]) => ({ label, value })), "all") : null; + const search = new Toggle("Search now", true); + // Radarr/Sonarr skip unmonitored items when searching. + const sync = () => { + const nothing = !monitored.value || mode?.value === "none"; + search.el.classList.toggle("f-off", nothing); + if (nothing) search.set(false, false); + }; + monitored.onChange = sync; + if (mode) mode.onChange = sync; + const addBtn = button("Add", () => { + addBtn.disabled = true; + const opts = { qualityProfileId: profile.value, rootFolderPath: folder.value, monitored: monitored.value, searchNow: search.value }; + const req = t.kind === "movie" ? app.radarr.addMovie(t.movie, opts) : app.sonarr.addSeries(t.series, { ...opts, monitor: mode!.value }); + req.then((created) => { + d.close(); + toast(`Added ${name}` + (opts.searchNow ? ", searching" : ""), "ok"); + added(created?.id ?? 0); + }, (e) => { + addBtn.disabled = false; + toast("Adding failed: " + errorText(e), "error"); + }); + }); + d.add(profile.el, folder.el, monitored.el, mode?.el ?? null, search.el, h("div.row-buttons", addBtn, button("Cancel", () => d.close(), "ghost"))); + d.open(addBtn); + } +} diff --git a/webos/src/views/calendar.ts b/webos/src/views/calendar.ts new file mode 100644 index 0000000..0fdfdb8 --- /dev/null +++ b/webos/src/views/calendar.ts @@ -0,0 +1,218 @@ +// Calendar: two weeks of movie releases and airing episodes, by day. +// Ported from internal/ui/calendar.go. + +import { app } from "../app.ts"; +import type { Movie } from "../api/radarr.ts"; +import type { Episode } from "../api/sonarr.ts"; +import { episodeCode, movieDetail, titleYear } from "../describe.ts"; +import { addDays, dayLabel, isoDay } from "../format.ts"; +import { DetailPane, type Detail } from "../ui/detail.ts"; +import { h, replace } from "../ui/dom.ts"; +import { focus, Keys } from "../ui/focus.ts"; +import { ListView, type Row } from "../ui/list.ts"; +import { actionSheet, button, errorText, run, Toggle } from "../ui/widgets.ts"; +import { movieReleases } from "./actions.ts"; +import { openReleases } from "./releases.ts"; +import type { Hint, View } from "./view.ts"; + +const days = 14; +const step = 7; + +interface Entry { + key: string; + day: string; // YYYY-MM-DD, local + at: number; // ms, 0 for date-only entries (movie releases) + title: string; + sub: string; + have: boolean; + monitored: boolean; + missing: boolean; + detail: () => Detail; + movie?: Movie; + episode?: Episode; +} + +// releaseDay reads a movie release date: midnight UTC meaning "this date", +// taken as written rather than shifted into the local zone. +export function releaseDay(s: string | undefined): string { + return s && /^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : ""; +} + +export function movieEntries(m: Movie, from: string, to: string, today: string): Entry[] { + const out: Entry[] = []; + for (const [name, date] of [["In cinemas", m.inCinemas], ["Digital release", m.digitalRelease], ["Physical release", m.physicalRelease]] as const) { + const day = releaseDay(date); + if (!day || day < from || day > to) continue; + out.push({ + key: `m${m.id}/${name}/${day}`, day, at: 0, title: titleYear(m), sub: name, + have: m.hasFile, monitored: m.monitored, missing: m.monitored && !m.hasFile && day <= today, + detail: () => { + const d = movieDetail(m); + d.lines.push({ text: `Release: ${name}, ${new Date(day + "T12:00:00").toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric" })}` }); + return d; + }, + movie: m, + }); + } + return out; +} + +export function episodeEntry(e: Episode, from: string, to: string, now: Date): Entry | null { + let day = ""; + let at = 0; + const t = e.airDateUtc ? Date.parse(e.airDateUtc) : NaN; + if (!isNaN(t)) { + at = t; + day = isoDay(new Date(t)); + } else if (e.airDate) day = e.airDate; + if (!day || day < from || day > to) return null; + const series = e.series?.title ?? "Unknown series"; + const sub = `${episodeCode(e.seasonNumber, e.episodeNumber)} ${e.title}`; + const aired = at ? at < now.getTime() : day < isoDay(now); + return { + key: "e" + e.id, day, at, title: series, sub, + have: e.hasFile, monitored: e.monitored, missing: e.monitored && !e.hasFile && aired, + detail: () => { + const when = at ? new Date(at).toLocaleString("en-GB", { weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }) + : new Date(day + "T12:00:00").toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" }); + return { + title: series, + sub, + facts: [], + lines: [ + { text: "Airs " + when }, + e.hasFile ? { text: "Downloaded", cls: "ok" } : !e.monitored ? { text: "Not monitored", cls: "muted" } : { text: "Not downloaded", cls: "warn" }, + ], + plot: e.overview || e.series?.overview || "", + poster: e.series ? app.sonarr.posterURL(e.series.images, true, true) : "", + ref: e.series ? { kind: "show", tvdb: e.series.tvdbId, imdb: e.series.imdbId } : undefined, + }; + }, + episode: e, + }; +} + +export class CalendarView implements View { + el: HTMLElement; + hints: Hint[] = [["ok", "releases and search"], ["red", "week back"], ["green", "week forward"], ["blue", "reload"]]; + private offset = 0; + private unmonitored = false; + private range: HTMLElement; + private list: ListView; + private detail = new DetailPane(); + private errors: string[] = []; + + constructor() { + this.range = h("div.cal-range"); + const unmon = new Toggle("Unmonitored too", false, (v) => { + this.unmonitored = v; + this.reload(); + }); + this.list = new ListView("calendar", { + rowHeight: 50, + heading: (key) => h("div.cal-day", key.slice(2)), + render: (e) => h("div.cal-row", + h("span.muted", e.at ? new Date(e.at).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }) : ""), + h(`span.dot${e.monitored ? ".on" : ""}` as "span", e.monitored ? "●" : "○"), + h("span.ellipsis.bold", e.title), + h("span.ellipsis", e.sub), + h(`span.${e.have ? "ok" : e.missing ? "warn" : "muted"}` as "span", e.have ? "✓ downloaded" : e.missing ? "missing" : e.monitored ? "" : "unmonitored"), + ), + onSelect: (e) => this.detail.show(e ? e.detail() : null), + onActivate: (e) => this.activate(e), + placeholder: "Loading…", + }); + this.el = h("div.view.calendar-view", + h("div.cal-main", + h("div.toolbar.fgroup", + button("◀ Week", () => this.move(-step)), button("Today", () => this.move(-this.offset)), button("Week ▶", () => this.move(step)), + unmon.el, this.range), + this.list.el), + this.detail.el, + ); + } + + show(): void { + this.reload(); + if (!this.el.contains(document.activeElement)) focus(this.list.el); + } + + hide(): void {} + + keys(code: number): boolean { + switch (code) { + case Keys.red: + this.move(-step); + return true; + case Keys.green: + this.move(step); + return true; + case Keys.blue: + this.reload(); + return true; + } + return false; + } + + private move(d: number): void { + this.offset += d; + this.reload(); + } + + private reload(): void { + const now = new Date(); + const today = isoDay(now); + const from = isoDay(addDays(now, this.offset)); + const to = isoDay(addDays(now, this.offset + days - 1)); + replace(this.range, `${new Date(from + "T12:00:00").toLocaleDateString("en-GB", { day: "numeric", month: "short" })} – ${new Date(to + "T12:00:00").toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" })}`); + // Radarr's window is by release date; a day on either side catches + // dates that the zone moves. + const qFrom = isoDay(addDays(now, this.offset - 1)); + const qTo = isoDay(addDays(now, this.offset + days)); + this.errors = []; + const movies = app.radarr.configured() + ? app.radarr.calendar(qFrom, qTo, this.unmonitored).then((ms) => ([] as Entry[]).concat(...ms.map((m) => movieEntries(m, from, to, today))), (e) => { + this.errors.push("Radarr: " + errorText(e)); + return [] as Entry[]; + }) + : Promise.resolve([] as Entry[]); + const episodes = app.sonarr.configured() + ? app.sonarr.calendar(qFrom, qTo, this.unmonitored).then((es) => es.map((e) => episodeEntry(e, from, to, now)).filter((x): x is Entry => !!x), (e) => { + this.errors.push("Sonarr: " + errorText(e)); + return [] as Entry[]; + }) + : Promise.resolve([] as Entry[]); + Promise.all([movies, episodes]).then(([ms, es]) => { + const all = ms.concat(es).sort((a, b) => + a.day.localeCompare(b.day) || a.at - b.at || a.title.toLowerCase().localeCompare(b.title.toLowerCase()) || a.sub.localeCompare(b.sub)); + const rows: Row[] = []; + let lastDay = ""; + for (const e of all) { + if (e.day !== lastDay) { + rows.push({ key: "d:" + dayLabel(e.day, today) + (["Today", "Tomorrow", "Yesterday"].includes(dayLabel(e.day, today)) ? " · " + new Date(e.day + "T12:00:00").toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" }) : "") }); + lastDay = e.day; + } + rows.push({ item: e, key: e.key }); + } + this.list.setPlaceholder(this.errors.join(" · ") || "Nothing in these two weeks."); + this.list.setRows(rows, false); + }); + } + + private activate(e: Entry): void { + if (e.movie) { + const m = e.movie; + actionSheet(titleYear(m), [ + { label: "Search releases", run: () => movieReleases(m) }, + { label: "Automatic search", run: () => void run("Searching " + m.title, "Search started: " + m.title, () => app.radarr.searchMovie(m.id)) }, + ]); + return; + } + const ep = e.episode!; + const title = `${e.title} ${episodeCode(ep.seasonNumber, ep.episodeNumber)}`; + actionSheet(title, [ + { label: "Search releases", run: () => openReleases("Releases for " + title, (sig) => app.sonarr.episodeReleases(ep.id, sig), (r) => app.sonarr.grab(r)) }, + { label: "Automatic search", run: () => void run("Searching " + title, "Search started: " + title, () => app.sonarr.searchEpisode(ep.id)) }, + ]); + } +} diff --git a/webos/src/views/downloads.ts b/webos/src/views/downloads.ts new file mode 100644 index 0000000..62c479e --- /dev/null +++ b/webos/src/views/downloads.ts @@ -0,0 +1,239 @@ +// Downloads: the SABnzbd queue and history and the Radarr/Sonarr queues, +// refreshed every few seconds while visible; pause / resume, delete, retry, +// remove and blocklist. Also the background watch that announces finished +// downloads. Ported from internal/ui/downloads.go and watch.go. + +import { app } from "../app.ts"; +import { queueName, queueProblem, queueProgress, type ArrClient, type QueueItem } from "../api/arr.ts"; +import type { HistorySlot, Queue, Slot } from "../api/sabnzbd.ts"; +import { refreshSeconds } from "../config.ts"; +import { capitalize, formatAge, formatSpeed } from "../format.ts"; +import { h, replace } from "../ui/dom.ts"; +import { focus, Keys } from "../ui/focus.ts"; +import { ListView } from "../ui/list.ts"; +import { actionSheet, button, confirmDialog, errorText, run, toast } from "../ui/widgets.ts"; +import type { Hint, View } from "./view.ts"; + +interface ArrRow { + source: ArrClient; + item: QueueItem; +} + +const stateLabels: Record = { + downloading: "Downloading", + importPending: "Import pending", + importBlocked: "Import blocked", + importing: "Importing", + imported: "Imported", + failedPending: "Failed (pending)", + failed: "Failed", + ignored: "Ignored", +}; + +export function arrState(q: QueueItem): [string, string] { + const label = stateLabels[q.trackedDownloadState ?? ""] || capitalize(q.status ?? ""); + const st = q.trackedDownloadState; + if (q.trackedDownloadStatus === "error" || st === "failed" || st === "failedPending" || st === "importBlocked") return [label, "bad"]; + if (q.trackedDownloadStatus === "warning") return [label, "warn"]; + return [label, ""]; +} + +function bar(pct: number): HTMLElement { + return h("span.bar", h("span.bar-fill", { style: `width:${Math.max(0, Math.min(100, pct))}%` })); +} + +export class DownloadsView implements View { + el: HTMLElement; + hints: Hint[] = [["ok", "what to do"], ["yellow", "pause / resume SABnzbd"], ["blue", "refresh"]]; + private sabHead: HTMLElement; + private pauseBtn: HTMLButtonElement; + private queue: ListView; + private arr: ListView; + private history: ListView; + private paused = false; + private timer = 0; + + constructor() { + this.sabHead = h("div.sab-head"); + this.pauseBtn = button("Pause", () => this.togglePause()); + this.queue = new ListView("dl-queue", { + rowHeight: 50, + header: h("div.dl-row.list-cols", h("span", "SABnzbd queue"), h("span", "Status"), h("span", "Progress"), h("span.num", "Left"), h("span.num", "Time")), + render: (s) => h("div.dl-row", + h("span.ellipsis", s.filename), + h(`span.${s.status === "Downloading" ? "dl" : "muted"}` as "span", s.status), + h("span.progress", bar(s.percentage), h("span.num", Math.round(s.percentage) + "%")), + h("span.num", s.sizeleft), + h("span.num", s.status === "Downloading" ? s.timeleft : ""), + ), + onActivate: (s) => this.slotActions(s), + placeholder: "Loading…", + }); + this.arr = new ListView("dl-arr", { + rowHeight: 50, + header: h("div.dl-row.list-cols", h("span", "Radarr / Sonarr queue"), h("span", "State"), h("span", "Progress"), h("span", "Client"), h("span.num", "")), + render: (r) => { + const [label, cls] = arrState(r.item); + const problem = queueProblem(r.item); + return h("div.dl-row", + h("span.ellipsis", queueName(r.item), problem ? h("span.dl-problem", " — " + problem) : null), + h(`span.${cls || "plain"}` as "span", label), + h("span.progress", bar(queueProgress(r.item)), h("span.num", Math.round(queueProgress(r.item)) + "%")), + h("span.ellipsis.muted", r.item.downloadClient ?? ""), + h("span"), + ); + }, + onActivate: (r) => this.arrActions(r), + placeholder: "Loading…", + }); + this.history = new ListView("dl-history", { + rowHeight: 50, + header: h("div.dl-row.list-cols", h("span", "SABnzbd history"), h("span", "Status"), h("span", ""), h("span.num", "Size"), h("span.num", "When")), + render: (s) => h("div.dl-row", + h("span.ellipsis", s.name, s.fail_message ? h("span.dl-problem", " — " + s.fail_message) : null), + h(`span.${s.status === "Failed" ? "bad" : s.status === "Completed" ? "ok" : "plain"}` as "span", s.status), + h("span"), + h("span.num", s.size), + h("span.num.muted", formatAge(s.completed)), + ), + onActivate: (s) => this.historyActions(s), + placeholder: "Loading…", + }); + this.el = h("div.view.downloads-view", + h("div.toolbar.fgroup", this.pauseBtn, this.sabHead), + this.queue.el, this.arr.el, this.history.el, + ); + } + + show(): void { + this.refresh(); + this.timer = window.setInterval(() => this.refresh(), refreshSeconds(app.cfg) * 1000); + if (!this.el.contains(document.activeElement)) focus(this.queue.el); + } + + hide(): void { + clearInterval(this.timer); + } + + keys(code: number): boolean { + if (code === Keys.yellow) { + this.togglePause(); + return true; + } + if (code === Keys.blue) { + this.refresh(); + return true; + } + return false; + } + + refresh(): void { + if (app.sab.configured()) { + app.sab.queue().then((q) => { + app.setStatus("SABnzbd", null); + this.showQueue(q); + }, (e) => { + app.setStatus("SABnzbd", e); + this.queue.setPlaceholder("SABnzbd: " + errorText(e)); + this.queue.setItems([], (s) => s.nzo_id); + replace(this.sabHead); + }); + app.sab.history(30).then((hs) => { + this.history.setPlaceholder("No finished downloads."); + this.history.setItems(hs, (s) => s.nzo_id); + }, (e) => { + this.history.setPlaceholder("SABnzbd: " + errorText(e)); + this.history.setItems([], (s) => s.nzo_id); + }); + } else { + this.queue.setPlaceholder("SABnzbd is not configured."); + this.history.setPlaceholder(""); + this.queue.setItems([], (s) => s.nzo_id); + } + const sources = [app.radarr, app.sonarr].filter((c) => c.configured()); + Promise.all(sources.map((c) => (c === app.radarr ? app.radarr.movieQueue() : app.sonarr.seriesQueue()).then( + (items) => items.map((item) => ({ source: c as ArrClient, item })), + (e) => { + this.arr.setPlaceholder(`${c.name}: ${errorText(e)}`); + return [] as ArrRow[]; + }, + ))).then((parts) => { + const rows = ([] as ArrRow[]).concat(...parts); + if (rows.length) this.arr.setPlaceholder(""); + else if (!this.arr.length) this.arr.setPlaceholder("Nothing in the Radarr or Sonarr queue."); + this.arr.setItems(rows, (r) => r.source.name + r.item.id); + }); + } + + private showQueue(q: Queue): void { + this.paused = q.paused; + this.pauseBtn.textContent = q.paused ? "Resume" : "Pause"; + const parts = [q.paused ? "Paused" : q.status || "Idle"]; + if (!q.paused && q.kbpersec > 0) parts.push(formatSpeed(q.kbpersec)); + if (q.slots.length) parts.push(`${q.sizeleft} left`, q.timeleft); + replace(this.sabHead, h(`span.${q.paused ? "warn" : "plain"}` as "span", parts.join(" · "))); + this.queue.setPlaceholder("The SABnzbd queue is empty."); + this.queue.setItems(q.slots, (s) => s.nzo_id); + } + + private togglePause(): void { + const resume = this.paused; + void run(resume ? "Resuming SABnzbd" : "Pausing SABnzbd", resume ? "SABnzbd resumed" : "SABnzbd paused", + () => (resume ? app.sab.resume() : app.sab.pause())).then(() => this.refresh()); + } + + private slotActions(s: Slot): void { + actionSheet(s.filename, [ + { label: "Delete from the queue", danger: true, run: () => confirmDialog(`Delete ${s.filename} from the SABnzbd queue?`, "Delete", () => { + void run("Deleting", "Deleted " + s.filename, () => app.sab.deleteJob(s.nzo_id, false)).then(() => this.refresh()); + }, true) }, + ]); + } + + private historyActions(s: HistorySlot): void { + const actions = [ + { label: "Remove from the history", danger: true, run: () => confirmDialog(`Remove ${s.name} from the history?`, "Remove", () => { + void run("Removing", "Removed " + s.name, () => app.sab.deleteHistory(s.nzo_id, false)).then(() => this.refresh()); + }, true) }, + ]; + if (s.status === "Failed") actions.unshift({ label: "Retry", danger: false, run: () => void run("Retrying " + s.name, "Queued again: " + s.name, () => app.sab.retry(s.nzo_id)).then(() => this.refresh()) }); + actionSheet(s.name, actions); + } + + private arrActions(r: ArrRow): void { + const name = queueName(r.item); + const remove = (blocklist: boolean) => confirmDialog( + blocklist ? `Remove ${name} from the ${r.source.name} queue and the download client, and blocklist the release? ${r.source.name} then searches for another one.` : `Remove ${name} from the ${r.source.name} queue and the download client?`, + blocklist ? "Remove and blocklist" : "Remove", + () => void run("Removing " + name, "Removed " + name, () => r.source.removeQueueItem(r.item.id, blocklist)).then(() => this.refresh()), + true, + ); + actionSheet(name, [ + { label: "Remove", danger: true, run: () => remove(false) }, + { label: "Remove and blocklist", danger: true, run: () => remove(true) }, + ]); + } +} + +// watchDownloads checks SABnzbd's history every 15 s and announces +// downloads that finished or failed since the app started. +export function watchDownloads(): void { + const seen = new Map(); + let baseline = true; + const poll = () => { + if (!app.sab.configured() || app.cfg.ui.notify_downloads === false) return; + app.sab.history(30).then((slots) => { + for (const s of slots) { + const prev = seen.get(s.nzo_id); + seen.set(s.nzo_id, s.status); + const final = s.status === "Completed" || s.status === "Failed"; + if (baseline || !final || prev === s.status) continue; + if (s.status === "Failed") toast("Download failed: " + s.name + (s.fail_message ? ` (${s.fail_message.slice(0, 60)})` : ""), "error"); + else toast("Download finished: " + s.name, "ok"); + } + baseline = false; + }, () => undefined); + }; + poll(); + setInterval(poll, 15_000); +} diff --git a/webos/src/views/episodes.ts b/webos/src/views/episodes.ts new file mode 100644 index 0000000..286b47a --- /dev/null +++ b/webos/src/views/episodes.ts @@ -0,0 +1,197 @@ +// A library series' seasons and episodes, with download marks, release and +// automatic searches and monitoring. Ported from internal/ui/episodes.go. + +import { app, openPage, pageHeader } from "../app.ts"; +import { queueProgress } from "../api/arr.ts"; +import type { Episode, Series } from "../api/sonarr.ts"; +import { dlLine, episodeCode, monitoredLine, seriesDetail, titleYear, type DlInfo } from "../describe.ts"; +import { isoDay } from "../format.ts"; +import { DetailPane, type Line } from "../ui/detail.ts"; +import { h, replace } from "../ui/dom.ts"; +import { focus } from "../ui/focus.ts"; +import { ListView, type Row } from "../ui/list.ts"; +import { actionSheet, errorText, run } from "../ui/widgets.ts"; +import { openReleases } from "./releases.ts"; +import { episodeStatus, seasonLabel, summarizeSeasons } from "../seasons.ts"; + +interface EpRow { + season: number; + ep?: Episode; +} + +export function openEpisodes(start: Series, changed?: () => void): void { + let series = start; + let episodes: Episode[] = []; + let loaded = false; + const expanded = new Set(); + let downloading = new Map(); + const detail = new DetailPane(); + const header = h("div.page-status"); + + const list = new ListView("episodes", { + rowHeight: 52, + render: (r) => (r.ep ? episodeRow(r.ep) : seasonRow(r.season)), + onSelect: (r) => showDetail(r), + onActivate: (r) => activate(r), + placeholder: "Loading…", + }); + const el = h("div.episodes-page", pageHeader(titleYear(series), header), h("div.split", list.el, detail.el), h("div.hints", "OK: expand, releases and more · Back return")); + const page = openPage(el, () => clearInterval(watch)); + focus(list.el); + + const watch = window.setInterval(loadQueue, 10_000); + reload(); + loadQueue(); + + function reload(): void { + Promise.all([app.sonarr.getSeries(series.id), app.sonarr.episodes(series.id)]).then(([s, eps]) => { + if (!loaded) { + const newest = summarizeSeasons(eps)[0]; + if (newest) expanded.add(newest.number); // open the newest season first + } + series = s; + episodes = eps; + loaded = true; + app.setStatus("Sonarr", null); + render(); + }, (e) => { + app.setStatus("Sonarr", e); + list.setPlaceholder("Sonarr: " + errorText(e)); + }); + } + + // loadQueue marks the episodes in the download queue; when one has left + // it, the episodes are loaded again so its status follows. + function loadQueue(): void { + app.sonarr.seriesQueue().then((items) => { + const now = new Map(); + for (const q of items) { + if (q.seriesId === series.id && q.episodeId && !now.has(q.episodeId)) { + now.set(q.episodeId, { count: 1, progress: queueProgress(q), timeLeft: q.timeleft ?? "" }); + } + } + const left = [...downloading.keys()].some((id) => !now.has(id)); + downloading = now; + if (left) reload(); + else render(); + }, () => undefined); + } + + function seasonMonitored(n: number): boolean { + return series.seasons?.find((s) => s.seasonNumber === n)?.monitored ?? false; + } + + function seasonDownloads(n: number): number { + return episodes.filter((e) => e.seasonNumber === n && downloading.has(e.id)).length; + } + + function mark(monitored: boolean, dl: boolean): HTMLElement { + return h(`span.dot${dl ? ".dl" : monitored ? ".on" : ""}` as "span", monitored ? "●" : "○"); + } + + function seasonRow(n: number): HTMLElement { + const s = summarizeSeasons(episodes).find((x) => x.number === n) ?? { number: n, have: 0, total: 0 }; + const cls = s.have >= s.total ? "ok" : seasonMonitored(n) ? "warn" : "muted"; + return h("div.ep-row.season", + mark(seasonMonitored(n), seasonDownloads(n) > 0), + h("span", expanded.has(n) ? "▼" : "▶"), + h("span.bold", seasonLabel(n)), + h("span"), + h(`span.${cls}` as "span", `${s.have}/${s.total}`), + ); + } + + function episodeRow(e: Episode): HTMLElement { + const dl = downloading.get(e.id); + let [status, cls] = episodeStatus(e, isoDay(new Date())); + if (dl && !e.hasFile) [status, cls] = [`downloading ${Math.round(dl.progress)}%`, "dl"]; + return h("div.ep-row", + mark(e.monitored, !!dl), + h("span.muted", "E" + String(e.episodeNumber).padStart(2, "0")), + h("span.ellipsis", e.title), + h("span.muted", e.airDate ?? ""), + h(`span.${cls}` as "span", status), + ); + } + + function render(): void { + const rows: Row[] = []; + for (const s of summarizeSeasons(episodes)) { + rows.push({ item: { season: s.number }, key: "s" + s.number }); + if (!expanded.has(s.number)) continue; + for (const e of episodes.filter((x) => x.seasonNumber === s.number).sort((a, b) => a.episodeNumber - b.episodeNumber)) { + rows.push({ item: { season: s.number, ep: e }, key: "e" + e.id }); + } + } + if (loaded) list.setPlaceholder("Sonarr has no episodes for this series"); + list.setRows(rows); + const st = series.statistics; + replace(header, `${series.monitored ? "monitored" : "unmonitored"} · ${st?.episodeFileCount ?? 0}/${st?.episodeCount ?? 0} episodes`); + } + + function seasonPoster(n: number): string { + const img = series.seasons?.find((s) => s.seasonNumber === n)?.images?.find((i) => i.coverType === "poster" && i.remoteUrl); + return img?.remoteUrl ?? app.sonarr.posterURL(series.images, true, true); + } + + function showDetail(r: EpRow | undefined): void { + const d = seriesDetail(series); + if (!r) return detail.show(d); + const lines: Line[] = []; + const s = summarizeSeasons(episodes).find((x) => x.number === r.season); + if (!r.ep) { + d.sub = seasonLabel(r.season); + if (s) lines.push({ text: `${s.have} of ${s.total} episodes downloaded` }); + const n = seasonDownloads(r.season); + if (n === 1) lines.push({ text: "Downloading 1 episode", cls: "dl" }); + else if (n > 1) lines.push({ text: `Downloading ${n} episodes`, cls: "dl" }); + lines.push(monitoredLine(seasonMonitored(r.season))); + } else { + const e = r.ep; + d.sub = `${episodeCode(e.seasonNumber, e.episodeNumber)} ${e.title}`; + if (e.airDate) lines.push({ text: "Aired: " + e.airDate }); + const [status, cls] = episodeStatus(e, isoDay(new Date())); + lines.push({ text: "Status: " + status, cls }); + const dl = downloading.get(e.id); + if (dl) lines.push(dlLine(dl)); + lines.push(monitoredLine(e.monitored)); + if (e.overview) d.plot = e.overview; + } + d.lines = [...d.lines.filter((l) => l.text.startsWith("Network")), ...lines]; + d.poster = seasonPoster(r.season); + detail.show(d); + } + + function done(): void { + reload(); + changed?.(); + } + + function activate(r: EpRow): void { + const id = series.id; + if (!r.ep) { + const n = r.season; + const want = !seasonMonitored(n); + actionSheet(seasonLabel(n), [ + { label: expanded.has(n) ? "Collapse" : "Expand", run: () => { + if (expanded.has(n)) expanded.delete(n); + else expanded.add(n); + render(); + } }, + { label: "Search releases", hint: "season packs and episodes", run: () => openReleases(`Releases for ${series.title}, ${seasonLabel(n)}`, (sig) => app.sonarr.seasonReleases(id, n, sig), (rel) => app.sonarr.grab(rel)) }, + { label: "Automatic search", run: () => void run("Searching " + seasonLabel(n), "Search started: " + seasonLabel(n), () => app.sonarr.searchSeason(id, n)) }, + { label: want ? "Monitor the season" : "Unmonitor the season", run: () => void run(`${seasonLabel(n)}: ${want ? "monitoring" : "unmonitoring"}`, "Done", () => app.sonarr.setSeasonMonitored(id, n, want)).then((ok) => ok && done()) }, + ]); + return; + } + const e = r.ep; + const code = episodeCode(e.seasonNumber, e.episodeNumber); + actionSheet(`${code} ${e.title}`, [ + { label: "Search releases", run: () => openReleases(`Releases for ${series.title} ${code}`, (sig) => app.sonarr.episodeReleases(e.id, sig), (rel) => app.sonarr.grab(rel)) }, + { label: "Automatic search", run: () => void run("Searching " + code, "Search started: " + code, () => app.sonarr.searchEpisode(e.id)) }, + { label: e.monitored ? "Unmonitor" : "Monitor", run: () => void run(`${code}: ${e.monitored ? "unmonitoring" : "monitoring"}`, "Done", () => app.sonarr.setEpisodesMonitored([e.id], !e.monitored)).then((ok) => ok && done()) }, + ]); + } + + void page; +} diff --git a/webos/src/views/filter.ts b/webos/src/views/filter.ts new file mode 100644 index 0000000..3baabd3 --- /dev/null +++ b/webos/src/views/filter.ts @@ -0,0 +1,49 @@ +// The property filter dialog: quality, language, codec, HDR and audio. Every +// change applies at once (the list behind follows); Cancel restores the +// filter it started with. Options that would leave nothing are not offered. + +import { facets, listedValues, type AttrFilter, type Pool } from "../media.ts"; +import { h } from "../ui/dom.ts"; +import { button, Choice, Dialog } from "../ui/widgets.ts"; + +export function openFilterDialog(cur: AttrFilter, pool: () => Pool, set: (f: AttrFilter) => void): { refresh: () => void } { + const orig = { ...cur }; + let state = { ...cur }; + let done = false; + const d = new Dialog("Filter by file properties", "filter", () => { + if (!done) set(orig); // Back: undo + }); + const choices: Choice[] = []; + const refresh = () => { + const p = pool(); + facets.forEach((fc, i) => { + const vals = listedValues(fc, state, p); + choices[i].setOptions(vals.map((v) => ({ label: v || "any", value: v })), state[fc.key]); + }); + }; + for (const fc of facets) { + const c = new Choice(fc.label, [], undefined, (v) => { + state = { ...state, [fc.key]: v }; + set(state); + refresh(); + }); + choices.push(c); + d.add(c.el); + } + refresh(); + d.add(h("div.row-buttons", + button("Done", () => { + done = true; + d.close(); + }), + button("Clear", () => { + state = { res: "", lang: "", codec: "", hdr: "", audio: "" }; + set(state); + refresh(); + }, "ghost"), + button("Cancel", () => d.close(), "ghost"), + )); + d.add(h("p.dialog-hint", "◀ ▶ change a value · the list behind follows along")); + d.open(); + return { refresh }; +} diff --git a/webos/src/views/library.ts b/webos/src/views/library.ts new file mode 100644 index 0000000..6ad58a7 --- /dev/null +++ b/webos/src/views/library.ts @@ -0,0 +1,349 @@ +// Library: the movies of Radarr and the series of Sonarr with their status, +// each with its own status, text and file-property filters, and blue marks +// for titles in the download queue. Ported from internal/ui/library.go. + +import { app } from "../app.ts"; +import { queueProgress, type QueueItem } from "../api/arr.ts"; +import type { Movie } from "../api/radarr.ts"; +import type { Series } from "../api/sonarr.ts"; +import { dlLine, monitoredLine, movieDetail, seriesDetail, sizeLine, titleYear, type DlInfo } from "../describe.ts"; +import { formatSize } from "../format.ts"; +import { fileAttrs, filterActive, filterLabel, matchAny, noFilter, type AttrFilter, type FileAttrs, type Pool } from "../media.ts"; +import { DetailPane, type Detail } from "../ui/detail.ts"; +import { h, replace } from "../ui/dom.ts"; +import { focus, Keys } from "../ui/focus.ts"; +import { ListView } from "../ui/list.ts"; +import { button, Choice, errorText, textField } from "../ui/widgets.ts"; +import { showMovieActions, showSeriesActions } from "./actions.ts"; +import { openFilterDialog } from "./filter.ts"; +import type { Hint, View } from "./view.ts"; + +type Kind = "movies" | "series"; + +interface Item { + id: number; + title: string; + label: string; + monitored: boolean; + have: boolean; + missing: boolean; + status: [string, string]; // text, class + size: string; + detail: () => Detail; + movie?: Movie; + series?: Series; + attrs: FileAttrs[]; + attrsKnown: boolean; +} + +const statusFilters: [string, (i: Item) => boolean][] = [ + ["all", () => true], + ["missing", (i) => i.missing], + ["downloaded", (i) => i.have], + ["monitored", (i) => i.monitored], + ["unmonitored", (i) => !i.monitored], +]; + +function movieItem(m: Movie): Item { + let status: [string, string]; + if (m.hasFile) status = ["✓ " + (m.movieFile?.quality?.quality?.name ?? ""), "ok"]; + else if (!m.monitored) status = ["unmonitored", "muted"]; + else if (!m.isAvailable) status = ["not released", "muted"]; + else status = ["missing", "warn"]; + return { + id: m.id, title: m.title, label: titleYear(m), monitored: m.monitored, have: m.hasFile, + missing: m.monitored && !m.hasFile && m.isAvailable, status, size: m.sizeOnDisk > 0 ? formatSize(m.sizeOnDisk) : "", + detail: () => { + const d = movieDetail(m); + d.lines.push(monitoredLine(m.monitored)); + const s = sizeLine(m.sizeOnDisk); + if (s) d.lines.push(s); + if (m.path) d.lines.push({ text: "Path: " + m.path, cls: "muted" }); + return d; + }, + movie: m, attrs: m.hasFile && m.movieFile ? [fileAttrs(m.movieFile)] : [], attrsKnown: true, + }; +} + +function seriesItem(s: Series): Item { + const st = s.statistics ?? { episodeFileCount: 0, episodeCount: 0, sizeOnDisk: 0 }; + const counts = `${st.episodeFileCount}/${st.episodeCount}`; + let status: [string, string]; + if (!s.monitored) status = [counts + " unmonitored", "muted"]; + else if (st.episodeFileCount >= st.episodeCount) status = [counts, "ok"]; + else status = [counts, "warn"]; + return { + id: s.id, title: s.title, label: titleYear(s), monitored: s.monitored, have: st.episodeFileCount > 0, + missing: s.monitored && st.episodeFileCount < st.episodeCount, status, size: st.sizeOnDisk > 0 ? formatSize(st.sizeOnDisk) : "", + detail: () => { + const d = seriesDetail(s); + d.lines.push(monitoredLine(s.monitored)); + const sz = sizeLine(st.sizeOnDisk); + if (sz) d.lines.push(sz); + return d; + }, + series: s, attrs: [], attrsKnown: st.episodeFileCount === 0, + }; +} + +function queueByTitle(items: QueueItem[], series: boolean): Map { + const out = new Map(); + for (const q of items) { + const id = series ? q.seriesId : q.movieId; + if (!id) continue; + const d = out.get(id); + if (d) d.count++; + else out.set(id, { count: 1, progress: queueProgress(q), timeLeft: q.timeleft ?? "" }); + } + return out; +} + +interface Pane { + kind: Kind; + all: Item[]; + loaded: boolean; + error: string; + status: number; + text: string; + attr: AttrFilter; +} + +export class LibraryView implements View { + el: HTMLElement; + hints: Hint[] = [["ok", "what to do"], ["red", "movies / series"], ["yellow", "filter by file properties"], ["blue", "reload"]]; + private panes: Record = { + movies: { kind: "movies", all: [], loaded: false, error: "", status: 0, text: "", attr: noFilter() }, + series: { kind: "series", all: [], loaded: false, error: "", status: 0, text: "", attr: noFilter() }, + }; + private cur: Kind = "movies"; + private tabs: Record; + private statusChoice: Choice; + private textInput: HTMLInputElement; + private filterBtn: HTMLButtonElement; + private count: HTMLElement; + private list: ListView; + private detail = new DetailPane(); + private downloading: Record> = { movies: new Map(), series: new Map() }; + private watch = 0; + private seriesFiles = new Map(); + private filesPending = 0; + private dialog: { refresh: () => void } | null = null; + + constructor() { + this.tabs = { + movies: button("Movies", () => this.switchTo("movies"), "tab"), + series: button("Series", () => this.switchTo("series"), "tab"), + }; + this.statusChoice = new Choice("Show", statusFilters.map(([name], i) => ({ label: name, value: i })), 0, (v) => { + this.pane().status = v; + this.render(); + }); + const text = textField("", "", { placeholder: "Filter titles…", onInput: (v) => { + this.pane().text = v; + this.render(); + } }); + this.textInput = text.input; + this.textInput.addEventListener("keydown", (ev) => { + if (ev.keyCode === Keys.enter) focus(this.list.el); + }); + this.filterBtn = button("File properties…", () => this.openFilter()); + this.count = h("div.lib-count"); + this.list = new ListView("library", { + rowHeight: 50, + header: h("div.lib-row.list-cols", h("span"), h("span", "Title"), h("span", "Status"), h("span.num", "Size")), + render: (it) => { + const dl = this.downloading[this.cur].get(it.id); + return h("div.lib-row", + h(`span.dot${dl ? ".dl" : it.monitored ? ".on" : ""}` as "span", it.monitored ? "●" : "○"), + h("span.ellipsis", it.label), + h(`span.ellipsis.${it.status[1]}` as "span", it.status[0]), + h("span.num", it.size), + ); + }, + onSelect: (it) => this.showDetail(it), + onActivate: (it) => this.activate(it), + placeholder: "Loading…", + }); + this.el = h("div.view.library-view", + h("div.lib-main", + h("div.toolbar.fgroup", this.tabs.movies, this.tabs.series, this.statusChoice.el, text.el, this.filterBtn, this.count), + this.list.el), + this.detail.el, + ); + app.afterGrab.push(() => this.loadQueue()); + app.onConfig.push(() => { + this.panes.movies.loaded = this.panes.series.loaded = false; + if (this.el.isConnected) this.reload(); + }); + } + + private pane(): Pane { + return this.panes[this.cur]; + } + + show(): void { + this.syncControls(); + this.reload(); + this.loadQueue(); + this.watch = window.setInterval(() => this.loadQueue(), 10_000); + if (!this.el.contains(document.activeElement)) focus(this.list.el); + } + + hide(): void { + clearInterval(this.watch); + } + + keys(code: number): boolean { + switch (code) { + case Keys.red: + this.switchTo(this.cur === "movies" ? "series" : "movies"); + return true; + case Keys.yellow: + this.openFilter(); + return true; + case Keys.blue: + this.reload(); + return true; + } + return false; + } + + private switchTo(k: Kind): void { + this.cur = k; + this.syncControls(); + this.render(); + if (!this.pane().loaded) this.reload(); + } + + private syncControls(): void { + const p = this.pane(); + this.tabs.movies.classList.toggle("on", this.cur === "movies"); + this.tabs.series.classList.toggle("on", this.cur === "series"); + this.statusChoice.setOptions(statusFilters.map(([name], i) => ({ label: name, value: i })), p.status); + this.textInput.value = p.text; + } + + reload(): void { + for (const kind of ["movies", "series"] as Kind[]) { + const p = this.panes[kind]; + const load: Promise = kind === "movies" + ? app.radarr.movies().then((ms) => ms.map(movieItem)) + : app.sonarr.allSeries().then((ss) => ss.map(seriesItem)); + load.then((items) => { + p.all = items.map((it) => this.withCachedFiles(it)); + p.loaded = true; + p.error = ""; + app.setStatus(kind === "movies" ? "Radarr" : "Sonarr", null); + if (kind === this.cur) this.render(); + if (filterActive(this.panes.series.attr) && kind === "series") this.loadSeriesFiles(); + }, (e) => { + p.error = errorText(e); + p.loaded = true; + app.setStatus(kind === "movies" ? "Radarr" : "Sonarr", e); + if (kind === this.cur) this.render(); + }); + } + } + + private loadQueue(): void { + Promise.all([app.radarr.movieQueue().catch(() => null), app.sonarr.seriesQueue().catch(() => null)]).then(([m, s]) => { + if (m) this.downloading.movies = queueByTitle(m, false); + if (s) this.downloading.series = queueByTitle(s, true); + if (m || s) { + this.list.redraw(); + this.showDetail(this.list.selected); + } + }); + } + + private shown(p: Pane, status = p.status, attr = p.attr): Item[] { + const text = p.text.trim().toLowerCase(); + return p.all + .filter((it) => statusFilters[status][1](it) && it.title.toLowerCase().includes(text) && (!filterActive(attr) || matchAny(attr, it.attrs))) + .sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); + } + + private render(): void { + const p = this.pane(); + const items = this.shown(p); + if (p.error) this.list.setPlaceholder((this.cur === "movies" ? "Radarr: " : "Sonarr: ") + p.error); + else if (!p.loaded) this.list.setPlaceholder("Loading…"); + else if (this.filesPending && filterActive(p.attr)) this.list.setPlaceholder("Reading the series' files…"); + else this.list.setPlaceholder("Nothing matches the filters."); + this.list.setItems(items, (it) => String(it.id)); + replace(this.count, `${items.length} of ${p.all.length}`); + this.filterBtn.textContent = filterActive(p.attr) ? "Files: " + filterLabel(p.attr) : "File properties…"; + this.dialog?.refresh(); + } + + private showDetail(it: Item | undefined): void { + if (!it) return this.detail.show(null); + const d = it.detail(); + const dl = this.downloading[this.cur].get(it.id); + if (dl) d.lines.splice(d.lines.findIndex((l) => l.text.startsWith("Monitored")), 0, dlLine(dl)); + this.detail.show(d); + } + + private activate(it: Item): void { + const reload = () => this.reload(); + if (it.movie) showMovieActions(it.movie, reload); + else if (it.series) showSeriesActions(it.series, reload); + } + + private openFilter(): void { + const p = this.pane(); + if (p.kind === "series") this.loadSeriesFiles(); + this.dialog = openFilterDialog(p.attr, () => this.pool(p), (f) => { + p.attr = f; + this.render(); + }); + // The dialog's refresh stays valid until it closes; a stale one is harmless. + } + + // pool: the files of the items that pass the other filters. + private pool(p: Pane): Pool { + const items = this.shown(p, p.status, noFilter()); + return { files: items.map((it) => it.attrs), unknown: items.some((it) => !it.attrsKnown) }; + } + + private withCachedFiles(it: Item): Item { + const st = it.series?.statistics; + const cached = it.series && this.seriesFiles.get(it.id); + if (cached && st && cached.count === st.episodeFileCount && cached.size === st.sizeOnDisk) { + return { ...it, attrs: cached.attrs, attrsKnown: true }; + } + return it; + } + + // loadSeriesFiles reads the files of every series whose properties are not + // known yet (four at a time), for the property filter. + private loadSeriesFiles(): void { + const todo = this.panes.series.all.filter((it) => !it.attrsKnown); + if (!todo.length || this.filesPending) return; + this.filesPending = todo.length; + let next = 0; + const worker = async () => { + while (next < todo.length) { + const it = todo[next++]; + try { + const files = await app.sonarr.episodeFiles(it.id); + const attrs = files.map(fileAttrs); + const st = it.series!.statistics!; + this.seriesFiles.set(it.id, { count: st.episodeFileCount, size: st.sizeOnDisk, attrs }); + const cur = this.panes.series.all.find((x) => x.id === it.id); + if (cur) { + cur.attrs = attrs; + cur.attrsKnown = true; + } + } catch { + /* stays unknown */ + } + this.filesPending--; + } + }; + Promise.all([worker(), worker(), worker(), worker()]).then(() => { + this.filesPending = 0; + if (this.cur === "series") this.render(); + }); + } +} diff --git a/webos/src/views/log.ts b/webos/src/views/log.ts new file mode 100644 index 0000000..5ff0cff --- /dev/null +++ b/webos/src/views/log.ts @@ -0,0 +1,43 @@ +// Log: everything the toasts said, newest first. + +import { h } from "../ui/dom.ts"; +import { focus, Keys } from "../ui/focus.ts"; +import { ListView } from "../ui/list.ts"; +import { log, onLog, type LogEntry } from "../ui/widgets.ts"; +import type { Hint, View } from "./view.ts"; + +export class LogView implements View { + el: HTMLElement; + hints: Hint[] = [["blue", "clear the log"]]; + private list: ListView; + + constructor() { + this.list = new ListView("log", { + rowHeight: 44, + render: (e) => h("div.log-row", h("span.muted", e.at.toLocaleTimeString("en-GB")), h(`span.ellipsis.${e.level === "error" ? "bad" : e.level === "warn" ? "warn" : e.level === "ok" ? "ok" : "plain"}` as "span", e.text)), + placeholder: "Nothing logged yet.", + }); + this.el = h("div.view.log-view", this.list.el); + onLog(() => { + if (this.el.isConnected) this.render(); + }); + } + + show(): void { + this.render(); + focus(this.list.el); + } + + hide(): void {} + + keys(code: number): boolean { + if (code !== Keys.blue) return false; + log.length = 0; + this.render(); + return true; + } + + private render(): void { + this.list.setItems(log.slice().reverse(), (e) => e.at.getTime() + e.text); + } +} diff --git a/webos/src/views/releases.ts b/webos/src/views/releases.ts new file mode 100644 index 0000000..9d33d1a --- /dev/null +++ b/webos/src/views/releases.ts @@ -0,0 +1,126 @@ +// The interactive release search: indexer results with approval and +// rejection reasons, sort, filters, and grab. Ported from internal/ui/releases.go. + +import { app, openPage, pageHeader } from "../app.ts"; +import type { Release } from "../api/arr.ts"; +import { isAbort } from "../api/http.ts"; +import { formatReleaseAge, formatSize } from "../format.ts"; +import { filterActive, filterLabel, isRejected, noFilter, releaseAttrs, visibleReleases, type Pool, type SortMode } from "../media.ts"; +import { h, replace } from "../ui/dom.ts"; +import { ListView } from "../ui/list.ts"; +import { button, Choice, confirmDialog, errorText, toast, Toggle } from "../ui/widgets.ts"; +import { openFilterDialog } from "./filter.ts"; + +export function openReleases(title: string, fetch: (signal: AbortSignal) => Promise, grab: (r: Release) => Promise): void { + const ctl = new AbortController(); + let all: Release[] = []; + let torrents = 0; + let searched = false; + let failed = ""; + let sortMode: SortMode = "best"; + let hideRejected = false; + let attr = noFilter(); + + const status = h("div.page-status"); + const info = h("div.release-info"); + const list = new ListView("releases", { + rowHeight: 52, + header: h("div.release-row.list-cols", h("span"), h("span", "Title"), h("span", "Quality"), h("span.num", "Size"), h("span.num", "Age"), h("span", "Indexer"), h("span.num", "Score")), + render: (r) => h("div.release-row", + h(`span.mark.${!isRejected(r) ? "ok" : r.temporarilyRejected ? "warn" : "bad"}` as "span", !isRejected(r) ? "✓" : r.temporarilyRejected ? "~" : "✗"), + h("span.ellipsis", r.title), + h("span.ellipsis", r.quality?.quality?.name ?? ""), + h("span.num", formatSize(r.size)), + h("span.num", formatReleaseAge(r.ageMinutes)), + h("span.ellipsis", r.indexer), + h("span.num", String(r.customFormatScore)), + ), + onSelect: (r) => showInfo(r), + onActivate: (r) => grabRelease(r), + placeholder: "Searching indexers… (Back cancels)", + }); + + const sort = new Choice("Sort", [{ label: "best", value: "best" }, { label: "size", value: "size" }, { label: "age", value: "age" }], "best", (v) => { + sortMode = v; + render(); + }); + const hide = new Toggle("Hide rejected", false, (v) => { + hideRejected = v; + render(); + }); + const filterBtn = button("Filter…", () => { + openFilterDialog(attr, pool, (f) => { + attr = f; + render(); + }); + }); + + const el = h("div.releases", + pageHeader(title, status), + h("div.toolbar.fgroup", sort.el, hide.el, filterBtn), + list.el, + info, + h("div.hints", "OK grab · Back return"), + ); + const page = openPage(el, () => ctl.abort()); + render(); + + fetch(ctl.signal).then((releases) => { + // Downloads go to SABnzbd, which handles Usenet only. + all = releases.filter((r) => r.protocol !== "torrent"); + torrents = releases.length - all.length; + searched = true; + render(); + list.el.focus(); + }, (e) => { + if (isAbort(e)) return; + failed = errorText(e); + searched = true; + render(); + }); + + function pool(): Pool { + return { files: all.filter((r) => !(hideRejected && isRejected(r))).map((r) => [releaseAttrs(r)]), unknown: false }; + } + + function render(): void { + const shown = visibleReleases(all, hideRejected, sortMode, attr); + if (failed) list.setPlaceholder("Search failed: " + failed); + else if (!searched) list.setPlaceholder("Searching indexers… (Back cancels)"); + else if (all.length) list.setPlaceholder("All releases are hidden by the filters"); + else list.setPlaceholder("No releases found. Are indexers configured?"); + list.setItems(shown, (r) => r.guid); + const parts: string[] = []; + if (searched && !failed) parts.push(`${shown.length} of ${all.length} releases`); + if (filterActive(attr)) parts.push(filterLabel(attr)); + if (torrents) parts.push(`${torrents} torrent release${torrents === 1 ? "" : "s"} hidden`); + replace(status, parts.join(" · ")); + filterBtn.textContent = filterActive(attr) ? `Filter: ${filterLabel(attr)}` : "Filter…"; + } + + function showInfo(r: Release | undefined): void { + if (!r) return replace(info); + const facts = [r.releaseGroup ? "group " + r.releaseGroup : "", (r.languages ?? []).map((l) => l.name).join(", ")].filter(Boolean); + replace(info, + r.rejections?.length ? h("div.bad", "Rejected: " + r.rejections.join("; ")) : r.approved ? h("div.ok", "Approved") : null, + facts.length ? h("div.muted", facts.join(" · ")) : null, + h("div.release-title", r.title), + ); + } + + function grabRelease(r: Release): void { + if (!isRejected(r)) return doGrab(r); + const reasons = r.rejections ?? []; + const more = reasons.length > 3 ? `\n+${reasons.length - 3} more` : ""; + confirmDialog(`This release was rejected:\n${reasons.slice(0, 3).join("\n")}${more}\n\nGrab it anyway?`, "Grab anyway", () => doGrab(r)); + } + + function doGrab(r: Release): void { + toast("Grabbing " + r.title + "…"); + grab(r).then(() => { + toast("Sent to the download client: " + r.title, "ok"); + page.close(); // back to where the search started + for (const f of app.afterGrab) setTimeout(f, 3000); + }, (e) => toast("Grab failed: " + errorText(e), "error")); + } +} diff --git a/webos/src/views/search.ts b/webos/src/views/search.ts new file mode 100644 index 0000000..838c86c --- /dev/null +++ b/webos/src/views/search.ts @@ -0,0 +1,322 @@ +// Movies and Series: search Radarr/Sonarr as you type; with an empty search +// box the Trakt lists (trending, most watched, anticipated, popular, box +// office). Titles not in the library can be added; library titles offer +// releases, automatic search and more. + +import { app, lookups } from "../app.ts"; +import { isAbort } from "../api/http.ts"; +import type { Movie } from "../api/radarr.ts"; +import type { Series } from "../api/sonarr.ts"; +import { listNote, movieLists, showLists, type Entry, type List } from "../api/trakt.ts"; +import { movieDetail, seriesDetail } from "../describe.ts"; +import { DetailPane, type Detail } from "../ui/detail.ts"; +import { h, replace } from "../ui/dom.ts"; +import { focus, focusFirst, Keys } from "../ui/focus.ts"; +import { actionSheet, errorText } from "../ui/widgets.ts"; +import { openAdd } from "./add.ts"; +import { movieActions, seriesActions } from "./actions.ts"; +import type { Hint, View } from "./view.ts"; + +export type SearchKind = "movie" | "series"; + +interface Result { + key: string; + title: string; + year: number; + inLibrary: boolean; + note: string; // the Trakt list's number, "" for search results + poster: string; // small + detail: () => Detail; + movie?: Movie; + series?: Series; +} + +const listTTL = 15 * 60_000; +const listLimit = 20; +const resolveLimit = 4; + +function movieResult(m: Movie, note = ""): Result { + return { key: "tmdb:" + m.tmdbId, title: m.title, year: m.year, inLibrary: !!m.id, note, poster: app.radarr.posterURL(m.images, !!m.id, false), detail: () => movieDetail(m), movie: m }; +} + +function seriesResult(s: Series, note = ""): Result { + return { key: "tvdb:" + s.tvdbId, title: s.title, year: s.year, inLibrary: !!s.id, note, poster: app.sonarr.posterURL(s.images, !!s.id, false), detail: () => seriesDetail(s), series: s }; +} + +// resolve looks a list title up: in the library (quick, always asked), else +// the lookup kept from an earlier run, else Radarr/Sonarr's lookup (slow: it +// asks the metadata provider). +async function resolveEntry(kind: SearchKind, e: Entry): Promise { + if (kind === "movie") { + const id = e.ids.tmdb; + if (!id) return null; + const lib = await app.radarr.movieByTMDB(id); + if (lib) return movieResult(lib); + const key = "tmdb:" + id; + const kept = lookups.get(key) as Movie | undefined; + if (kept) return movieResult(kept); + const found = (await app.radarr.lookup(key))[0]; + if (!found) return null; + if (!found.id) lookups.put(key, found); + return movieResult(found); + } + const id = e.ids.tvdb; + if (!id) return null; + const lib = await app.sonarr.seriesByTVDB(id); + if (lib) return seriesResult(lib); + const key = "tvdb:" + id; + const kept = lookups.get(key) as Series | undefined; + if (kept) return seriesResult(kept); + const found = (await app.sonarr.lookup(key))[0]; + if (!found) return null; + if (!found.id) lookups.put(key, found); + return seriesResult(found); +} + +async function mapLimit(items: T[], limit: number, f: (t: T) => Promise): Promise<{ value?: R; error?: unknown }[]> { + const out: { value?: R; error?: unknown }[] = new Array(items.length); + let next = 0; + const worker = async () => { + while (next < items.length) { + const i = next++; + try { + out[i] = { value: await f(items[i]) }; + } catch (error) { + out[i] = { error }; + } + } + }; + await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker)); + return out; +} + +interface Fetched { + results: Result[]; + error: string; + at: number; +} + +export class SearchView implements View { + el: HTMLElement; + hints: Hint[] = [["ok", "add, or what to do"], ["red", "previous list"], ["green", "next list"], ["blue", "reload"]]; + private kind: SearchKind; + private input: HTMLInputElement; + private chips: HTMLElement; + private grid: HTMLElement; + private status: HTMLElement; + private detail = new DetailPane(); + private lists: List[]; + private listIdx = 0; + private fetched = new Map(); + private loading = new Set(); + private searchCtl: AbortController | null = null; + private timer = 0; + private results: Result[] = []; + + constructor(kind: SearchKind) { + this.kind = kind; + this.lists = kind === "movie" ? movieLists : showLists; + const noun = kind === "movie" ? "movies" : "series"; + const ids = kind === "movie" ? "tmdb:603, imdb:tt0133093" : "tvdb:81189, imdb:tt0903747"; + this.input = h("input.search.f", { type: "search", placeholder: `Search ${noun} (title, ${ids})…`, spellcheck: false, autocomplete: "off" }); + this.input.addEventListener("input", () => { + clearTimeout(this.timer); + this.timer = window.setTimeout(() => this.update(), 600); + }); + this.input.addEventListener("keydown", (ev) => { + if (ev.keyCode === Keys.enter) { + clearTimeout(this.timer); + this.update(); + (ev.target as HTMLInputElement).blur?.(); + const first = this.grid.querySelector(".card"); + if (first) focus(first); + } + }); + this.chips = h("div.chips.fgroup"); + this.status = h("div.grid-status"); + this.grid = h("div.grid.fgroup"); + this.el = h("div.view.search-view", + h("div.search-main", h("div.search-bar", this.input), this.chips, this.status, h("div.grid-scroll", this.grid)), + this.detail.el, + ); + app.onConfig.push(() => { + this.fetched.clear(); + if (this.browsing()) this.update(); + }); + } + + show(): void { + this.renderChips(); + if (!this.results.length) this.update(); + if (!this.el.contains(document.activeElement)) focusFirst(this.el); + } + + hide(): void {} + + keys(code: number): boolean { + if (!this.browsing()) return false; + switch (code) { + case Keys.red: + this.switchList(-1); + return true; + case Keys.green: + this.switchList(1); + return true; + case Keys.blue: + this.fetched.delete(this.listIdx); + this.update(); + return true; + } + return false; + } + + private browsing(): boolean { + return this.input.value.trim() === ""; + } + + private switchList(step: number): void { + this.listIdx = (this.listIdx + step + this.lists.length) % this.lists.length; + this.renderChips(); + this.update(); + } + + private renderChips(): void { + if (!app.trakt.configured()) return replace(this.chips); + replace(this.chips, ...this.lists.map((l, i) => { + const chip = h(`button.chip.f${i === this.listIdx ? ".on" : ""}` as "button", { + tabindex: -1, + onclick: () => { + this.input.value = ""; + this.listIdx = i; + this.renderChips(); + this.update(); + }, + }, l.name); + return chip; + })); + this.chips.classList.toggle("hidden", !this.browsing()); + } + + // update shows the Trakt list (empty box) or searches. + private update(): void { + this.searchCtl?.abort(); + this.chips.classList.toggle("hidden", !this.browsing()); + if (this.browsing()) return this.showList(); + const term = this.input.value.trim(); + const ctl = new AbortController(); + this.searchCtl = ctl; + this.setStatus("Searching…"); + const client = this.kind === "movie" ? app.radarr : app.sonarr; + const search: Promise = this.kind === "movie" + ? app.radarr.lookup(term, ctl.signal).then((ms) => ms.map((m) => movieResult(m))) + : app.sonarr.lookup(term, ctl.signal).then((ss) => ss.map((s) => seriesResult(s))); + search.then((rs) => { + if (ctl.signal.aborted) return; + app.setStatus(client.name as "Radarr" | "Sonarr", null); + this.setResults(rs.slice(0, 40), rs.length ? "" : `Nothing found for “${term}”.`); + }, (e) => { + if (isAbort(e) || ctl.signal.aborted) return; + app.setStatus(client.name as "Radarr" | "Sonarr", e); + this.setResults([], `${client.name}: ${errorText(e)}`); + }); + } + + private showList(): void { + const l = this.lists[this.listIdx]; + if (!app.trakt.configured()) { + return this.setResults([], `Trending, most watched and anticipated ${this.kind === "movie" ? "movies" : "series"} appear here once a Trakt client ID is set in Settings.`); + } + const f = this.fetched.get(this.listIdx); + if (f && Date.now() - f.at < listTTL) return this.setResults(f.results, f.error || (f.results.length ? "" : "Nothing on this list right now.")); + this.setResults([], `Loading ${l.name}…`); + const idx = this.listIdx; + if (this.loading.has(idx)) return; + this.loading.add(idx); + this.fetchList(l).then((fetched) => { + this.loading.delete(idx); + this.fetched.set(idx, fetched); + if (this.browsing() && this.listIdx === idx) this.showList(); + }); + } + + private async fetchList(l: List): Promise { + const at = Date.now(); + let entries: Entry[]; + try { + entries = await app.trakt.entries(l, listLimit); + app.setStatus("Trakt", null); + } catch (e) { + app.setStatus("Trakt", e); + return { results: [], error: errorText(e), at }; + } + const found = await mapLimit(entries, resolveLimit, (e) => resolveEntry(this.kind, e)); + const results: Result[] = []; + let lastErr: unknown = null; + found.forEach((r, i) => { + if (r.value) results.push({ ...r.value, note: listNote(l, entries[i].count) }); + else if (r.error) lastErr = r.error; + }); + const name = this.kind === "movie" ? "Radarr" : "Sonarr"; + return { results, error: !results.length && lastErr ? `${name}: ${errorText(lastErr)}` : "", at }; + } + + private setStatus(text: string): void { + replace(this.status, text); + } + + private setResults(rs: Result[], message: string): void { + const hadFocus = this.grid.contains(document.activeElement); + this.results = rs; + this.setStatus(message); + replace(this.grid, ...rs.map((r) => this.card(r))); + if (hadFocus) { + const first = this.grid.querySelector(".card"); + focus(first ?? this.input); + } + this.detail.show(rs[0]?.detail() ?? null); + } + + private card(r: Result): HTMLElement { + const img = h("img.card-poster", { alt: "", loading: "lazy" }); + if (r.poster && app.covers()) img.src = r.poster; + img.addEventListener("error", () => img.classList.add("none")); + const el = h("div.card.f", { tabindex: -1, onclick: () => this.activate(r) }, + h("div.card-art", img, h("div.card-fallback", r.title), r.inLibrary ? h("div.card-badge", "✓") : null), + h("div.card-title", r.title), + h("div.card-sub", h("span", r.year > 0 ? String(r.year) : ""), r.note ? h("span.card-note", r.note) : null), + ); + el.addEventListener("focus", () => this.detail.show(r.detail())); + return el; + } + + private activate(r: Result): void { + const refresh = () => { + this.fetched.delete(this.listIdx); + this.update(); + }; + if (!r.inLibrary) { + const target = r.movie ? { kind: "movie" as const, movie: r.movie } : { kind: "series" as const, series: r.series! }; + actionSheet(r.title, [{ label: "Add…", run: () => openAdd(target, () => this.markAdded(r)) }]); + return; + } + actionSheet(r.title, r.movie ? movieActions(r.movie, refresh, refresh) : seriesActions(r.series!, refresh, refresh)); + } + + // markAdded ticks an added title; its library copy is fetched again so + // releases and the rest work on it. + private markAdded(r: Result): void { + this.fetched.delete(this.listIdx); + const reload = r.movie ? app.radarr.movieByTMDB(r.movie.tmdbId).then((m) => m && movieResult(m, r.note)) + : app.sonarr.seriesByTVDB(r.series!.tvdbId).then((s) => s && seriesResult(s, r.note)); + reload.then((fresh) => { + const i = this.results.indexOf(r); + if (!fresh || i < 0) return; + this.results[i] = fresh; + const cards = this.grid.querySelectorAll(".card"); + const old = cards[i]; + const card = this.card(fresh); + old.replaceWith(card); + if (document.activeElement === document.body || !document.activeElement) focus(card); + }, () => undefined); + } +} diff --git a/webos/src/views/settings.ts b/webos/src/views/settings.ts new file mode 100644 index 0000000..cc54745 --- /dev/null +++ b/webos/src/views/settings.ts @@ -0,0 +1,179 @@ +// Settings: the addresses, API keys, add defaults, Trakt client ID and +// interface options. The app starts with the config it was built with; +// changes made here are kept on the TV (local storage) until "Reset". + +import { app } from "../app.ts"; +import type { ArrClient } from "../api/arr.ts"; +import { RadarrClient } from "../api/radarr.ts"; +import { SABnzbdClient } from "../api/sabnzbd.ts"; +import { SonarrClient } from "../api/sonarr.ts"; +import { TraktClient, movieLists } from "../api/trakt.ts"; +import { builtIn, hasLocalChanges, resetConfig, saveConfig, type Config } from "../config.ts"; +import { h, replace } from "../ui/dom.ts"; +import { focusFirst } from "../ui/focus.ts"; +import { button, Choice, confirmDialog, errorText, textField, toast, Toggle } from "../ui/widgets.ts"; +import { applyTheme } from "../theme.ts"; +import type { Hint, View } from "./view.ts"; + +interface ArrFields { + url: HTMLInputElement; + key: HTMLInputElement; + profile: Choice; + folder: Choice; + timer: number; +} + +export class SettingsView implements View { + el: HTMLElement; + hints: Hint[] = [["ok", "edit, change or press"]]; + private form: HTMLElement; + private result: HTMLElement; + private radarr!: ArrFields; + private sonarr!: ArrFields; + private sab!: { url: HTMLInputElement; key: HTMLInputElement }; + private trakt!: HTMLInputElement; + private theme!: Choice; + private refresh!: Choice; + private covers!: Toggle; + private notify!: Toggle; + + constructor() { + this.form = h("div.settings-form.fgroup"); + this.result = h("div.settings-result"); + this.el = h("div.view.settings-view", h("div.settings-scroll", this.form, this.result)); + } + + show(): void { + this.build(app.cfg); + focusFirst(this.form); + } + + hide(): void {} + + private arrSection(name: string, c: Config["radarr"], make: (url: string, key: string) => ArrClient): ArrFields { + const url = textField("Address", c.url, { placeholder: "https://radarr.example.com" }); + const key = textField("API key", c.apikey, { secret: true }); + const f: ArrFields = { + url: url.input, + key: key.input, + profile: new Choice("Default quality profile", [{ label: c.default_quality_profile || "(first)", value: c.default_quality_profile ?? "" }], c.default_quality_profile ?? ""), + folder: new Choice("Default root folder", [{ label: c.default_root_folder || "(first)", value: c.default_root_folder ?? "" }], c.default_root_folder ?? ""), + timer: 0, + }; + // The lists come from the server named in the fields, shortly after either changes. + const load = () => { + const client = make(f.url.value, f.key.value); + if (!client.configured() || !f.key.value.trim()) return; + Promise.all([client.qualityProfiles(), client.rootFolders()]).then(([ps, fs]) => { + const pick = (names: string[], cur: string) => { + const opts = names.map((n) => ({ label: n, value: n })); + if (cur && !names.includes(cur)) opts.unshift({ label: cur + " (not on the server)", value: cur }); + if (!cur) opts.unshift({ label: "(first)", value: "" }); + return opts; + }; + f.profile.setOptions(pick(ps.map((p) => p.name), f.profile.value), f.profile.value); + f.folder.setOptions(pick(fs.map((x) => x.path), f.folder.value), f.folder.value); + }, () => undefined); + }; + for (const input of [f.url, f.key]) input.addEventListener("input", () => { + clearTimeout(f.timer); + f.timer = window.setTimeout(load, 700); + }); + load(); + this.form.append(h("h2", name), url.el, key.el, f.profile.el, f.folder.el); + return f; + } + + private build(c: Config): void { + replace(this.form); + replace(this.result); + this.radarr = this.arrSection("Radarr", c.radarr, (u, k) => new RadarrClient(u, k)); + this.sonarr = this.arrSection("Sonarr", c.sonarr, (u, k) => new SonarrClient(u, k)); + const sabURL = textField("Address", c.sabnzbd.url, { placeholder: "https://sabnzbd.example.com" }); + const sabKey = textField("API key", c.sabnzbd.apikey, { secret: true }); + this.sab = { url: sabURL.input, key: sabKey.input }; + const trakt = textField("Client ID", c.trakt.client_id ?? "", { secret: true, placeholder: "from trakt.tv/oauth/applications" }); + this.trakt = trakt.input; + this.theme = new Choice("Theme", [{ label: "auto", value: "auto" }, { label: "dark", value: "dark" }, { label: "light", value: "light" }], c.ui.theme || "auto", + (v) => applyTheme(v)); // live, saved with Save + this.refresh = new Choice("Downloads refresh", [1, 2, 3, 5, 10, 30].map((n) => ({ label: n + " s", value: n })), c.ui.refresh_seconds || 3); + this.covers = new Toggle("Show posters", c.ui.covers !== false); + this.notify = new Toggle("Announce finished downloads", c.ui.notify_downloads !== false); + this.form.append( + h("h2", "SABnzbd"), sabURL.el, sabKey.el, + h("h2", "Trakt"), h("p.muted", "For the trending, most watched and anticipated lists and the cast. Optional."), trakt.el, + h("h2", "Interface"), this.theme.el, this.refresh.el, this.covers.el, this.notify.el, + h("div.row-buttons", + button("Save", () => this.save()), + button("Test connections", () => this.test()), + button("Reset to built-in", () => confirmDialog("Forget the changes made on this TV and use the config the app was built with?", "Reset", () => { + resetConfig(); + app.apply(builtIn()); + applyTheme(app.cfg.ui.theme); + this.build(app.cfg); + toast("Settings reset to the built-in config", "ok"); + }), "ghost"), + ), + h("p.muted", hasLocalChanges() ? "Changed on this TV (kept in its storage)." : "As built into the app."), + ); + } + + private collect(): Config { + const arr = (f: ArrFields) => ({ url: f.url.value.trim(), apikey: f.key.value.trim(), default_quality_profile: f.profile.value || undefined, default_root_folder: f.folder.value || undefined }); + return { + radarr: arr(this.radarr), + sonarr: arr(this.sonarr), + sabnzbd: { url: this.sab.url.value.trim(), apikey: this.sab.key.value.trim() }, + trakt: { client_id: this.trakt.value.trim() || undefined }, + ui: { theme: this.theme.value, refresh_seconds: this.refresh.value, covers: this.covers.value, notify_downloads: this.notify.value }, + }; + } + + private save(): void { + const c = this.collect(); + for (const [name, s] of [["Radarr", c.radarr], ["Sonarr", c.sonarr], ["SABnzbd", c.sabnzbd]] as const) { + if (!!s.url !== !!s.apikey) { + toast(`${name}: enter both the address and the API key, or neither`, "error"); + return; + } + } + saveConfig(c); + app.apply(c); + applyTheme(c.ui.theme); + toast("Settings saved", "ok"); + this.build(c); + } + + // test checks every service with the values in the fields. + private test(): void { + const c = this.collect(); + replace(this.result, h("h2", "Test")); + const line = (name: string) => { + const el = h("div.test-line", name + ": testing…"); + this.result.appendChild(el); + return (ok: boolean, text: string) => { + el.className = "test-line " + (ok ? "ok" : "bad"); + el.textContent = `${name}: ${text}`; + }; + }; + const arrTest = (name: string, client: ArrClient) => { + if (!client.configured()) return line(name)(true, "not configured"); + const done = line(name); + client.systemStatus().then((s) => done(true, `${s.appName || name} ${s.version}`), (e) => done(false, errorText(e))); + }; + arrTest("Radarr", new RadarrClient(c.radarr.url, c.radarr.apikey)); + arrTest("Sonarr", new SonarrClient(c.sonarr.url, c.sonarr.apikey)); + const sab = new SABnzbdClient(c.sabnzbd.url, c.sabnzbd.apikey); + if (!sab.configured()) line("SABnzbd")(true, "not configured"); + else { + const done = line("SABnzbd"); + sab.version().then((v) => done(true, "SABnzbd " + v), (e) => done(false, errorText(e))); + } + const trakt = new TraktClient(c.trakt.client_id ?? "", app.trakt.baseURL); + if (!trakt.configured()) line("Trakt")(true, "not configured"); + else { + const done = line("Trakt"); + trakt.entries(movieLists[0], 1).then(() => done(true, "client ID accepted"), (e) => done(false, errorText(e))); + } + } +} diff --git a/webos/src/views/view.ts b/webos/src/views/view.ts new file mode 100644 index 0000000..5813180 --- /dev/null +++ b/webos/src/views/view.ts @@ -0,0 +1,13 @@ +// A top-level view (Movies, Series, Library, Downloads, Calendar, Settings, Log). + +// A hint names a remote button and what it does here: "ok", "back", or a +// colour button ("red", "green", "yellow", "blue"). +export type Hint = [button: string, text: string]; + +export interface View { + el: HTMLElement; + hints: Hint[]; + show(): void; // the view became visible + hide(): void; + keys?(keyCode: number): boolean; // colour buttons and the like +} diff --git a/webos/static/appinfo.json b/webos/static/appinfo.json new file mode 100644 index 0000000..b8f1c6f --- /dev/null +++ b/webos/static/appinfo.json @@ -0,0 +1,14 @@ +{ + "id": "com.stinnesbeck.arrtui", + "version": "0.1.0", + "vendor": "Nils Stinnesbeck", + "type": "web", + "main": "index.html", + "title": "arr-tui", + "icon": "icon.png", + "largeIcon": "largeIcon.png", + "bgColor": "#14161b", + "resolution": "1920x1080", + "disableBackHistoryAPI": true, + "requiredACG": [] +} diff --git a/webos/static/index.html b/webos/static/index.html new file mode 100644 index 0000000..1917a8b --- /dev/null +++ b/webos/static/index.html @@ -0,0 +1,16 @@ + + + + + +arr-tui + + + + +
+
+
+ + + diff --git a/webos/static/style.css b/webos/static/style.css new file mode 100644 index 0000000..563cd38 --- /dev/null +++ b/webos/static/style.css @@ -0,0 +1,250 @@ +/* arr-tui for webOS: 1920×1080, read from the couch. Kept to CSS that + webOS 5 (Chromium 68) supports: no flex gap, aspect-ratio or inset. */ + +:root, :root[data-theme="dark"] { + --bg: #14161b; + --panel: #1d2027; + --panel-2: #262a33; + --line: #333844; + --text: #e9eaee; + --muted: #8f94a0; + --accent: #f2b632; + --accent-text: #14161b; + --sel: #2f3542; + --ok: #7fcc6e; + --warn: #e8b552; + --bad: #f07272; + --dl: #5fadf2; + --shadow: rgba(0, 0, 0, 0.6); +} + +:root[data-theme="light"] { + --bg: #f1f2f4; + --panel: #ffffff; + --panel-2: #e8eaee; + --line: #d3d6dd; + --text: #1c1e23; + --muted: #6a6f7b; + --accent: #c98a00; + --accent-text: #ffffff; + --sel: #dfe3ea; + --ok: #2e8b2e; + --warn: #a86b00; + --bad: #c43a3a; + --dl: #1f6fc4; + --shadow: rgba(0, 0, 0, 0.25); +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + width: 1920px; + height: 1080px; + overflow: hidden; + background: var(--bg); + color: var(--text); + font: 26px/1.35 "LG Smart UI", "Museo Sans", Roboto, "Segoe UI", Arial, sans-serif; +} + +button, input { font: inherit; color: inherit; } +h1, h2 { margin: 0; font-weight: 600; } +.ok { color: var(--ok); } +.warn { color: var(--warn); } +.bad { color: var(--bad); } +.dl { color: var(--dl); } +.muted { color: var(--muted); } +.plain { color: var(--text); } +.bold { font-weight: 600; } +.hidden { display: none !important; } +.num { text-align: right; font-variant-numeric: tabular-nums; } +.ellipsis { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } + +.f { outline: none; } + +/* --- Shell --- */ + +.app { display: flex; flex-direction: column; height: 1080px; } +.topbar { display: flex; align-items: center; height: 88px; padding: 0 48px; background: var(--panel); border-bottom: 2px solid var(--line); } +.brand { font-size: 34px; font-weight: 700; color: var(--accent); margin-right: 40px; } +.tabs { display: flex; flex: 1; } +.tabs .tab { margin-right: 6px; } +.status-dots { display: flex; } +.status-dot { margin-left: 22px; font-size: 22px; color: var(--muted); } +.status-dot::before { content: "●"; margin-right: 8px; } +.status-dot.ok::before { color: var(--ok); } +.status-dot.bad::before { color: var(--bad); } +.status-dot.off::before { color: var(--line); } +.content { flex: 1; min-height: 0; padding: 28px 48px 16px; } +.footer { height: 60px; padding: 0 48px; display: flex; align-items: center; background: var(--panel); border-top: 2px solid var(--line); color: var(--muted); font-size: 22px; } +.footer-hints { display: flex; align-items: center; } +.hint { margin-right: 36px; display: inline-flex; align-items: center; } +.key { display: inline-block; min-width: 26px; height: 26px; border-radius: 13px; margin-right: 10px; font-size: 16px; line-height: 26px; text-align: center; font-weight: 700; } +.key.red { background: #e04848; } +.key.green { background: #3fae4a; } +.key.yellow { background: #e3c02c; } +.key.blue { background: #3d7fe0; } +.key.ok, .key.back { background: var(--panel-2); color: var(--text); padding: 0 10px; border-radius: 6px; } + +/* --- Buttons, chips, choices, toggles, fields --- */ + +.btn, .chip, .tab { + border: 2px solid transparent; background: var(--panel-2); border-radius: 10px; + padding: 10px 24px; cursor: pointer; white-space: nowrap; +} +.tab { background: transparent; color: var(--muted); } +.tab.on { color: var(--text); border-bottom-color: var(--accent); border-radius: 10px 10px 0 0; } +.btn.danger { color: var(--bad); } +.btn.ghost { background: transparent; border-color: var(--line); } +.btn:focus, .chip:focus, .tab:focus { background: var(--accent); color: var(--accent-text); border-color: var(--accent); } +.btn:disabled { opacity: 0.5; } +.btn-hint { margin-left: 14px; color: var(--muted); font-size: 20px; } +.btn:focus .btn-hint { color: var(--accent-text); } +.chip { border-radius: 30px; margin-right: 12px; font-size: 22px; padding: 8px 22px; } +.chip.on { border-color: var(--accent); } + +.toolbar { display: flex; align-items: center; margin-bottom: 18px; flex-wrap: wrap; } +.toolbar > * { margin-right: 14px; } + +.choice, .toggle { + display: flex; align-items: center; justify-content: space-between; + padding: 10px 20px; background: var(--panel-2); border: 2px solid transparent; border-radius: 10px; cursor: pointer; +} +.choice-label { color: var(--muted); margin-right: 18px; } +.choice-arrows { color: var(--muted); } +.choice-value { color: var(--text); margin: 0 12px; } +.choice:focus, .toggle:focus { border-color: var(--accent); } +.choice:focus .choice-arrows { color: var(--accent); } +.toggle-box { width: 28px; height: 28px; border: 2px solid var(--muted); border-radius: 6px; margin-right: 16px; position: relative; } +.toggle.on .toggle-box { background: var(--accent); border-color: var(--accent); } +.toggle.on .toggle-box::after { content: "✓"; position: absolute; left: 4px; top: -4px; color: var(--accent-text); font-weight: 700; } +.toggle { justify-content: flex-start; } +.toggle.f-off { opacity: 0.4; } + +.field { display: flex; align-items: center; } +.field-label { color: var(--muted); margin-right: 16px; white-space: nowrap; } +input.f, input.search { + background: var(--panel-2); border: 2px solid transparent; border-radius: 10px; padding: 10px 18px; min-width: 260px; +} +input.f:focus { border-color: var(--accent); } +input.search { width: 100%; font-size: 30px; padding: 14px 24px; } + +/* --- Lists --- */ + +.list { display: flex; flex-direction: column; flex: 1; min-height: 0; background: var(--panel); border: 2px solid var(--line); border-radius: 12px; position: relative; } +.list:focus { border-color: var(--accent); } +.list-body { flex: 1; min-height: 0; position: relative; overflow: hidden; padding: 6px 0; } +.list-cols { color: var(--accent); font-size: 22px; border-bottom: 2px solid var(--line); padding: 10px 20px !important; flex-shrink: 0; } +.list-row { display: grid; align-items: center; padding: 0 20px; white-space: nowrap; } +.list-row > * { margin-right: 18px; min-width: 0; } +.list-row.sel-idle { background: var(--panel-2); } +.list-row.sel { background: var(--sel); box-shadow: inset 6px 0 0 var(--accent); } +.list-heading, .cal-day { display: flex; align-items: flex-end; padding: 0 20px 6px !important; color: var(--accent); font-weight: 600; } +.list-placeholder { padding: 24px; color: var(--muted); } +.list-scroll { position: absolute; right: 4px; top: 8px; bottom: 8px; width: 6px; } +.list-thumb { position: absolute; left: 0; right: 0; background: var(--line); border-radius: 3px; } + +.dot { color: var(--muted); } +.dot.on { color: var(--ok); } +.dot.dl { color: var(--dl); } + +/* --- Views with a details pane on the right --- */ + +.view { display: flex; height: 100%; } +.search-main, .lib-main, .cal-main { flex: 1; min-width: 0; display: flex; flex-direction: column; margin-right: 32px; } +.detail { width: 600px; flex-shrink: 0; overflow: hidden; background: var(--panel); border-radius: 12px; padding: 28px; position: relative; } +.detail-poster { float: right; width: 220px; margin: 0 0 18px 22px; border-radius: 8px; background: var(--panel-2); min-height: 330px; } +.detail-poster.none { display: none; } +.detail-poster.loading { opacity: 0.3; } +.detail-title { font-size: 34px; line-height: 1.2; margin-bottom: 10px; } +.detail-sub { font-weight: 600; margin-bottom: 8px; } +.detail-facts { color: var(--muted); margin-bottom: 10px; } +.detail-line { font-size: 24px; } +.detail-plot { font-size: 24px; color: var(--text); margin: 16px 0 0; } +.detail-cast { clear: both; padding-top: 16px; font-size: 22px; } +.cast-head { font-weight: 600; margin-top: 8px; } + +/* Movies / Series */ +.search-bar { margin-bottom: 18px; } +.chips { display: flex; margin-bottom: 14px; } +.grid-status { color: var(--muted); min-height: 36px; } +.grid-scroll { flex: 1; min-height: 0; overflow: hidden; padding: 12px 8px; margin: 0 -8px; } +.grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); grid-column-gap: 22px; grid-row-gap: 26px; } +.card { cursor: pointer; border-radius: 10px; transition: transform 0.12s; } +.card:focus { transform: scale(1.06); } +.card:focus .card-art { box-shadow: 0 0 0 5px var(--accent), 0 10px 24px var(--shadow); } +.card-art { position: relative; padding-top: 150%; background: var(--panel-2); border-radius: 10px; overflow: hidden; } +.card-poster { position: absolute; left: 0; top: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; } +.card-poster.none { display: none; } +.card-fallback { position: absolute; left: 0; top: 0; right: 0; bottom: 0; padding: 16px; display: flex; align-items: center; justify-content: center; text-align: center; color: var(--muted); } +.card-badge { position: absolute; z-index: 2; right: 10px; top: 10px; width: 40px; height: 40px; border-radius: 20px; background: var(--ok); color: #10200d; font-weight: 700; text-align: center; line-height: 40px; } +.card-title { margin-top: 10px; font-size: 22px; font-weight: 600; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +.card-sub { display: flex; justify-content: space-between; font-size: 20px; color: var(--muted); } +.card-note { color: var(--muted); } + +/* Library */ +.lib-count { color: var(--muted); margin-left: auto; margin-right: 0 !important; } +.library-view .field input { min-width: 0; width: 230px; } +.library-view .field-label:empty { display: none; } +.lib-row { grid-template-columns: 30px 1fr 300px 140px; } + +/* Releases, episodes, calendar, downloads, log */ +.release-row { grid-template-columns: 30px 1fr 220px 130px 80px 200px 90px; font-size: 22px; } +.mark.ok { color: var(--ok); } .mark.warn { color: var(--warn); } .mark.bad { color: var(--bad); } +.release-info { min-height: 110px; padding: 12px 4px 0; font-size: 22px; } +.release-title { color: var(--muted); word-break: break-all; } +.ep-row { grid-template-columns: 30px 60px 1fr 160px 260px; } +.cal-row { grid-template-columns: 80px 30px 360px 1fr 200px; } +.cal-range { color: var(--muted); margin-left: auto; } +.downloads-view { flex-direction: column; } +.downloads-view .list { margin-bottom: 16px; } +.dl-queue { flex: 3; } .dl-arr { flex: 2; } .dl-history { flex: 3; } +.dl-row { grid-template-columns: 1fr 210px 280px 150px 120px; font-size: 22px; } +.dl-problem { color: var(--warn); } +.progress { display: flex; align-items: center; } +.bar { flex: 1; height: 10px; background: var(--panel-2); border-radius: 5px; overflow: hidden; margin-right: 12px; } +.bar-fill { display: block; height: 100%; background: var(--dl); } +.sab-head { font-size: 24px; } +.log-row { grid-template-columns: 140px 1fr; font-size: 22px; } + +/* Settings */ +.settings-view { display: block; } +.settings-scroll { height: 100%; overflow: hidden; } +.settings-form { max-width: 1100px; } +.settings-form h2 { color: var(--accent); margin: 26px 0 12px; font-size: 28px; } +.settings-form h2:first-child { margin-top: 0; } +.settings-form > .field, .settings-form > .choice, .settings-form > .toggle { margin-bottom: 10px; } +.settings-form .field input { flex: 1; } +.settings-result { max-width: 1100px; margin-top: 20px; } +.test-line { font-size: 24px; } + +/* --- Pages (releases, episodes): full screen over the shell --- */ + +.page { position: fixed; left: 0; top: 0; width: 1920px; height: 1080px; background: var(--bg); z-index: 10; display: flex; flex-direction: column; padding: 36px 48px 24px; } +.page-header { display: flex; align-items: baseline; margin-bottom: 20px; } +.page-header h1 { font-size: 38px; margin-right: 28px; } +.page-status { color: var(--muted); } +.split { flex: 1; min-height: 0; display: flex; } +.split .list { margin-right: 32px; } +.hints { color: var(--muted); font-size: 22px; padding-top: 12px; } + +/* --- Dialogs --- */ + +.dialog { position: fixed; left: 0; top: 0; width: 1920px; height: 1080px; background: rgba(0, 0, 0, 0.6); z-index: 20; display: flex; align-items: center; justify-content: center; } +.dialog-box { width: 900px; max-height: 960px; overflow: hidden; background: var(--panel); border: 2px solid var(--line); border-radius: 16px; padding: 36px; box-shadow: 0 20px 60px var(--shadow); } +.dialog-box h2 { margin-bottom: 20px; font-size: 32px; } +.dialog-box > .btn, .dialog-box > .choice, .dialog-box > .toggle { display: flex; align-items: baseline; width: 100%; margin-bottom: 10px; text-align: left; } +.dialog-box > .choice, .dialog-box > .toggle { align-items: center; } +.dialog-text { white-space: pre-line; margin: 0 0 20px; } +.dialog-hint { color: var(--muted); font-size: 20px; margin: 16px 0 0; } +.row-buttons { display: flex; margin-top: 18px; } +.row-buttons .btn { margin-right: 14px; } + +/* --- Toasts --- */ + +#toasts { position: fixed; right: 48px; bottom: 84px; width: 760px; z-index: 30; } +.toast { background: var(--panel-2); border-left: 6px solid var(--muted); border-radius: 8px; padding: 14px 20px; margin-top: 10px; box-shadow: 0 8px 24px var(--shadow); transition: opacity 0.5s; font-size: 22px; } +.toast-ok { border-left-color: var(--ok); } +.toast-warn { border-left-color: var(--warn); } +.toast-error { border-left-color: var(--bad); } +.toast.gone { opacity: 0; } diff --git a/webos/test/api.test.ts b/webos/test/api.test.ts new file mode 100644 index 0000000..15758af --- /dev/null +++ b/webos/test/api.test.ts @@ -0,0 +1,56 @@ +import { deepStrictEqual, strictEqual } from "node:assert"; +import { test } from "node:test"; +import { errorMessage, queueName, queueProgress, scaledVariant, tmdbSized, type QueueItem } from "../src/api/arr.ts"; +import { compact, decodeList, movieLists, refKey } from "../src/api/trakt.ts"; +import { formatReleaseAge, formatSize } from "../src/format.ts"; +import { episodeStatus, summarizeSeasons } from "../src/seasons.ts"; +import type { Episode } from "../src/api/sonarr.ts"; + +test("poster URLs", () => { + strictEqual(scaledVariant("/MediaCover/1/poster.jpg?lastWrite=5", 500), "/MediaCover/1/poster-500.jpg?lastWrite=5"); + strictEqual(scaledVariant("/MediaCover/1/fanart.jpg", 500), "/MediaCover/1/fanart.jpg"); + strictEqual(tmdbSized("https://image.tmdb.org/t/p/original/abc.jpg", true), "https://image.tmdb.org/t/p/w780/abc.jpg"); + strictEqual(tmdbSized("https://image.tmdb.org/t/p/original/abc.jpg", false), "https://image.tmdb.org/t/p/w342/abc.jpg"); + strictEqual(tmdbSized("https://artworks.thetvdb.com/banners/1.jpg", true), "https://artworks.thetvdb.com/banners/1.jpg"); +}); + +test("error messages", () => { + strictEqual(errorMessage('{"message":"Movie exists"}'), "Movie exists"); + strictEqual(errorMessage('[{"propertyName":"Path","errorMessage":"Bad path"},{"errorMessage":"Other"}]'), "Bad path; Other"); + strictEqual(errorMessage(" plain "), "plain"); +}); + +test("queue items", () => { + const q = { id: 1, title: "rel", status: "", size: 100, sizeleft: 25, series: { title: "Show" }, episode: { seasonNumber: 1, episodeNumber: 2, title: "x" } } as QueueItem; + strictEqual(queueName(q), "Show S01E02"); + strictEqual(queueProgress(q), 75); +}); + +test("trakt", () => { + strictEqual(compact(950), "950"); + strictEqual(compact(1234), "1.2k"); + strictEqual(compact(2000), "2k"); + strictEqual(compact(34567), "34k"); + strictEqual(compact(1_500_000), "1.5M"); + strictEqual(compact(48_000_000), "48M"); + const entries = decodeList([{ watchers: 1234, movie: { title: "Dune", year: 2021, ids: { trakt: 1, tmdb: 438631 } } }], movieLists[0]); + deepStrictEqual(entries, [{ title: "Dune", year: 2021, ids: { trakt: 1, tmdb: 438631 }, count: 1234 }]); + const bare = decodeList([{ title: "Heat", year: 1995, ids: { trakt: 2, tmdb: 949 } }], movieLists[3]); + strictEqual(bare[0].title, "Heat"); + strictEqual(refKey({ kind: "show", tvdb: 81189 }), "show:tvdb:81189"); +}); + +test("formatting", () => { + strictEqual(formatSize(1.5 * 1024 ** 3), "1.5 GB"); + strictEqual(formatSize(0), "–"); + strictEqual(formatReleaseAge(30), "30m"); + strictEqual(formatReleaseAge(300), "5h"); + strictEqual(formatReleaseAge(5000), "3d"); +}); + +test("seasons", () => { + const ep = (s: number, n: number, hasFile: boolean) => ({ id: s * 100 + n, seriesId: 1, seasonNumber: s, episodeNumber: n, title: "", hasFile, monitored: true }) as Episode; + deepStrictEqual(summarizeSeasons([ep(0, 1, false), ep(1, 1, true), ep(2, 1, false), ep(2, 2, true)]).map((s) => [s.number, s.have, s.total]), [[2, 1, 2], [1, 1, 1], [0, 0, 1]]); + deepStrictEqual(episodeStatus({ ...ep(1, 1, false), airDate: "2026-01-01" }, "2026-02-01"), ["missing", "warn"]); + deepStrictEqual(episodeStatus({ ...ep(1, 1, false), airDate: "2026-03-01" }, "2026-02-01"), ["unaired", "muted"]); +}); diff --git a/webos/test/media.test.ts b/webos/test/media.test.ts new file mode 100644 index 0000000..b0c4840 --- /dev/null +++ b/webos/test/media.test.ts @@ -0,0 +1,100 @@ +// Ported from internal/arr/mediafile_test.go and the filter tests. + +import { deepStrictEqual, strictEqual, ok } from "node:assert"; +import { test } from "node:test"; +import type { MediaFile, Release } from "../src/api/arr.ts"; +import { audioTags, facets, fileAttrs, listedValues, matchFile, noFilter, releaseAttrs, visibleReleases, type FileAttrs } from "../src/media.ts"; + +const attrs = (f: Partial) => fileAttrs({ size: 0, ...f }); + +test("attributes from media info", () => { + deepStrictEqual(attrs({ + quality: { quality: { name: "Bluray-2160p", resolution: 2160 } }, + mediaInfo: { videoCodec: "x265", videoDynamicRange: "HDR", videoDynamicRangeType: "DV HDR10" }, + languages: [{ name: "English" }, { name: "German" }], + }), { resolution: 2160, codec: "x265", dv: true, hdr: true, audio: [], languages: ["English", "German"] }); +}); + +test("attribute variants", () => { + const cases: [string, Partial, (a: FileAttrs) => boolean][] = [ + ["hevc", { mediaInfo: { videoCodec: "HEVC" } }, (a) => a.codec === "x265"], + ["h264", { mediaInfo: { videoCodec: "h264" } }, (a) => a.codec === "x264"], + ["avc", { mediaInfo: { videoCodec: "AVC" } }, (a) => a.codec === "x264"], + ["av1", { mediaInfo: { videoCodec: "AV1" } }, (a) => a.codec === "AV1"], + ["unoffered codec not guessed", { sceneName: "x264.Movie", mediaInfo: { videoCodec: "VC-1" } }, (a) => a.codec === ""], + ["codec from scene name", { sceneName: "Movie.2020.1080p.BluRay.x265-GRP" }, (a) => a.codec === "x265" && a.resolution === 1080], + ["dv from path", { relativePath: "Movie.2160p.DV.HDR10.mkv" }, (a) => a.dv && a.hdr && a.resolution === 2160], + ["dv only", { mediaInfo: { videoDynamicRangeType: "DV" } }, (a) => a.dv && !a.hdr], + ["hlg", { mediaInfo: { videoDynamicRangeType: "HLG" } }, (a) => a.hdr && !a.dv], + ["sdr", { mediaInfo: { videoCodec: "x264" }, sceneName: "Movie.HDRip.x264" }, (a) => !a.hdr && !a.dv], + ["hdr word with media info", { relativePath: "Hdr.Story.mkv", mediaInfo: { videoDynamicRange: "SDR" } }, (a) => !a.hdr], + ]; + for (const [name, f, check] of cases) ok(check(attrs(f)), `${name}: ${JSON.stringify(attrs(f))}`); +}); + +test("release attributes", () => { + const r = { + title: "Dune.2021.2160p.UHD.BluRay.DV.HDR10.x265-GRP", quality: { quality: { name: "Bluray-2160p", resolution: 2160 } }, + languages: [{ name: "English" }], customFormats: [{ name: "HDR" }], + } as Release; + deepStrictEqual(releaseAttrs(r), { resolution: 2160, codec: "x265", dv: true, hdr: true, audio: [], languages: ["English"] }); + const r2 = releaseAttrs({ title: "Show.S01E01.720p.WEB.H.264-GRP" } as Release); + ok(r2.resolution === 720 && r2.codec === "x264" && !r2.hdr && !r2.dv); + const r3 = releaseAttrs({ title: "Show.S01E01.HDTV.x264-GRP" } as Release); + ok(!r3.hdr && !r3.dv, "HDTV is not HDR"); +}); + +test("audio tags", () => { + const cases: [string, string[]][] = [ + ["Movie.2160p.UHD.BluRay.TrueHD.7.1.Atmos.x265-G", ["Atmos", "TrueHD"]], + ["Movie.1080p.DDP5.1.Atmos.x265", ["Atmos", "DD+"]], + ["Movie.1080p.DTS-HD.MA.5.1.x264", ["DTS-HD", "DTS"]], + ["Movie.2160p.DTS-X.7.1", ["DTS:X", "DTS"]], + ["Movie.1080p.DTS.5.1", ["DTS"]], + ["Movie.1080p.EAC3.5.1", ["DD+"]], + ["Movie.1080p.E-AC-3.5.1", ["DD+"]], + ["Movie.1080p.AC3.5.1", ["DD"]], + ["Movie.720p.DD5.1.x264", ["DD"]], + ["Show.S01E01.AAC.2.0", ["AAC"]], + ["Album.FLAC", ["FLAC"]], + ["Show.Opus", ["Opus"]], + ["Movie.1080p.BluRay.x264-DTSAG", []], + ["Movie.1080p.x264", []], + ]; + for (const [input, want] of cases) deepStrictEqual(audioTags(input), want, input); + deepStrictEqual(attrs({ sceneName: "Movie.x264.AC3", mediaInfo: { audioCodec: "TrueHD Atmos" } }).audio, ["Atmos", "TrueHD"]); + deepStrictEqual(attrs({ mediaInfo: { audioCodec: "EAC3", audioAdditionalFeatures: "Atmos" } }).audio, ["Atmos", "DD+"]); +}); + +test("filter matching", () => { + const a: FileAttrs = { resolution: 2160, codec: "x265", hdr: true, dv: false, audio: ["Atmos", "TrueHD"], languages: ["English"] }; + ok(matchFile({ ...noFilter(), res: "2160p", lang: "english", hdr: "HDR", audio: "Atmos" }, a)); + ok(!matchFile({ ...noFilter(), hdr: "SDR" }, a)); + ok(!matchFile({ ...noFilter(), hdr: "DV" }, a)); + ok(matchFile({ ...noFilter(), res: "SD" }, { ...a, resolution: 480 })); + ok(!matchFile({ ...noFilter(), res: "SD" }, { ...a, resolution: 0 })); +}); + +test("the dialog offers only options that leave something", () => { + const pool = { files: [[{ resolution: 1080, codec: "x264", hdr: false, dv: false, audio: ["DD"], languages: ["German"] }]], unknown: false }; + const quality = facets[0]; + strictEqual(quality.label, "Quality"); + deepStrictEqual(listedValues(quality, noFilter(), pool), ["", "1080p"]); + strictEqual(facets[1].label, "Language"); + deepStrictEqual(listedValues(facets[1], noFilter(), pool), ["", "German"]); + // Unknown files (series not read yet): everything is offered. + deepStrictEqual(listedValues(quality, noFilter(), { ...pool, unknown: true }), ["", "2160p", "1080p", "720p", "SD"]); +}); + +test("release order: approved first, then score, quality and age", () => { + const r = (guid: string, o: Partial) => ({ guid, title: guid, rejected: false, customFormatScore: 0, qualityWeight: 0, ageMinutes: 0, size: 0, ...o }) as Release; + const all = [ + r("rejected", { rejected: true, customFormatScore: 900 }), + r("old", { customFormatScore: 100, ageMinutes: 500 }), + r("new", { customFormatScore: 100, ageMinutes: 5 }), + r("best", { customFormatScore: 200 }), + ]; + deepStrictEqual(visibleReleases(all, false, "best", noFilter()).map((x) => x.guid), ["best", "new", "old", "rejected"]); + deepStrictEqual(visibleReleases(all, true, "best", noFilter()).map((x) => x.guid), ["best", "new", "old"]); + deepStrictEqual(visibleReleases([r("s", { size: 1 }), r("l", { size: 9 })], false, "size", noFilter()).map((x) => x.guid), ["l", "s"]); +}); diff --git a/webos/tsconfig.json b/webos/tsconfig.json new file mode 100644 index 0000000..b4ce682 --- /dev/null +++ b/webos/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["es2022", "dom", "dom.iterable"], + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src", "test", "build.mjs"] +}