@quicktvui/ai-cli 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +14 -10
  2. package/lib/index.js +206 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -60,6 +60,8 @@ quicktvui-aicreate-project quick-tv-app
60
60
  - `--auto-emulator <true|false>`: auto create/start emulator when no adb device
61
61
  - `--adb-path <path>`: custom adb path/command (or use env `QUICKTVUI_ADB_PATH`)
62
62
  - `--device-ip <ip[:port]>`: preferred real device endpoint for `adb connect`
63
+ - `--device <serial>`: explicit target adb serial for `setup-android-env` / `run-esapp`
64
+ - `--allow-non-tv-device <true|false>`: allow phone/tablet target for TV run flow (default `false`)
63
65
  - `--avd-name <name>`: custom AVD name for `setup-android-env`
64
66
  - `--headless`: start emulator with `-no-window -no-audio`
65
67
  - `--runtime-version <version>`: pin runtime version in `direct` mode
@@ -71,7 +73,6 @@ quicktvui-aicreate-project quick-tv-app
71
73
  - `--port <n>`: dev server port used by `run-dev` auto load (default `38989`)
72
74
  - `--skip-env-check`: skip environment stage in `run-dev`
73
75
  - `--runtime-package <pkg>`: runtime package for `run-esapp` (default `com.extscreen.runtime`)
74
- - `--device <serial>`: target adb serial for `run-esapp`
75
76
  - `--esapp-uri <uri>`: raw launch URI (`esapp://`, `quicktv://`, `appcast://`)
76
77
  - `--esapp-query <json>`: extra query params JSON merged in structured mode
77
78
  - `--pkg --ver --min-ver --repository --uri --from --args --exp --flags --use-latest`: structured `esapp://action/start` params
@@ -115,15 +116,18 @@ This command:
115
116
 
116
117
  1. Detects Android SDK root (auto creates a default SDK root when missing).
117
118
  2. Detects Android SDK/adb/emulator tools. `@quicktvui/ai-cli` does not bundle adb.
118
- 3. If sdkmanager/avdmanager is missing, it auto-downloads and installs official Android Command-line Tools.
119
- 4. If adb is missing, it auto-installs `platform-tools` (with size estimate + sdkmanager progress).
120
- 5. Detects connected adb devices, and asks whether to use connected device.
121
- 6. If user doesn't use connected device (or no device exists), it asks for real-device IP and runs `adb connect`.
122
- 7. If still no real device, asks whether to download/start official Google Android emulator.
123
- 8. Checks Google repository reachability before emulator download; if unavailable, asks user to install emulator manually.
124
- 9. Prints estimated download size and then starts sdkmanager download (progress shown by sdkmanager).
125
- 10. Installs runtime APK and configures debug server host (direct mode by default).
126
- 11. Launches runtime app and waits it enters running state.
119
+ 3. Verifies target device looks like TV (`leanback` / `television` feature). Use `--allow-non-tv-device true` only if you intentionally target phone/tablet.
120
+ 4. In non-interactive mode, requires explicit `--device <serial>` when adb devices are connected.
121
+ 5. If sdkmanager/avdmanager is missing, it auto-downloads and installs official Android Command-line Tools.
122
+ 6. If adb is missing, it auto-installs `platform-tools` (with size estimate + sdkmanager progress).
123
+ 7. Detects connected adb devices, and asks whether to use connected device.
124
+ 8. If user doesn't use connected device (or no device exists), it asks for real-device IP and runs `adb connect`.
125
+ 9. If still no real device, asks whether to download/start official Google Android emulator.
126
+ 10. Checks Google repository reachability before emulator download; if unavailable, asks user to install emulator manually.
127
+ 11. Prints estimated download size and then starts sdkmanager download (progress shown by sdkmanager).
128
+ 12. Installs runtime APK and configures debug server host.
129
+ 13. If runtime already exists, asks whether to reinstall runtime before run.
130
+ 14. Launches runtime app and waits it enters running state.
127
131
 
128
132
  ## Configure Vue env (Node + package manager)
129
133
 
package/lib/index.js CHANGED
@@ -31,6 +31,8 @@ const RUNTIME_LAUNCH_ACTIVITY =
31
31
  "com.extscreen.runtime/com.extscreen.runtime.LauncherAlias";
32
32
  const RUNTIME_DEBUG_BROADCAST_ACTION =
33
33
  "com.extscreen.runtime.ACTION_CHANGE_DEBUG_SERVER";
34
+ const RUNTIME_DEBUG_BROADCAST_ACTION_FALLBACK =
35
+ "tv.eskit.debugger.ACTION_CHANGE_DEBUG_SERVER";
34
36
  const RUNTIME_REPOSITORY_ROOT =
35
37
  "http://hub.quicktvui.com/repository/maven-files/apk/runtime/dev";
36
38
  const RUNTIME_REPOSITORY_METADATA_URL = `${RUNTIME_REPOSITORY_ROOT}/maven-metadata.xml`;
@@ -1212,6 +1214,105 @@ function listConnectedDevices(adbPath) {
1212
1214
  return parseAdbDevices(result.stdout);
1213
1215
  }
1214
1216
 
1217
+ function runAdbShellCapture(adbPath, serial, shellArgs) {
1218
+ try {
1219
+ const result = runCommandCapture(adbPath, [
1220
+ "-s",
1221
+ serial,
1222
+ "shell",
1223
+ ...shellArgs,
1224
+ ]);
1225
+ return (result.stdout || "").trim();
1226
+ } catch (error) {
1227
+ return "";
1228
+ }
1229
+ }
1230
+
1231
+ function parseAndroidWmSize(rawText) {
1232
+ const text = String(rawText || "");
1233
+ const match = text.match(/(\d+)\s*x\s*(\d+)/);
1234
+ if (!match) return null;
1235
+ const width = Number.parseInt(match[1], 10);
1236
+ const height = Number.parseInt(match[2], 10);
1237
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
1238
+ return { width, height };
1239
+ }
1240
+
1241
+ function inspectAndroidDeviceProfile(adbPath, serial) {
1242
+ const model =
1243
+ runAdbShellCapture(adbPath, serial, ["getprop", "ro.product.model"]) ||
1244
+ "unknown";
1245
+ const characteristics =
1246
+ runAdbShellCapture(adbPath, serial, [
1247
+ "getprop",
1248
+ "ro.build.characteristics",
1249
+ ]) || "";
1250
+ const featureText = runAdbShellCapture(adbPath, serial, [
1251
+ "pm",
1252
+ "list",
1253
+ "features",
1254
+ ]);
1255
+ const wmSizeText = runAdbShellCapture(adbPath, serial, ["wm", "size"]);
1256
+ const wmSize = parseAndroidWmSize(wmSizeText);
1257
+
1258
+ const featureLines = featureText
1259
+ .split(/\r?\n/)
1260
+ .map((line) => line.trim().toLowerCase())
1261
+ .filter(Boolean);
1262
+ const hasLeanbackFeature = featureLines.some(
1263
+ (line) =>
1264
+ line.includes("feature:android.software.leanback") ||
1265
+ line.includes("feature:android.hardware.type.television"),
1266
+ );
1267
+ const characteristicText = characteristics.toLowerCase();
1268
+ const hasTvCharacteristic =
1269
+ characteristicText.includes("tv") ||
1270
+ characteristicText.includes("television");
1271
+ const isLikelyTv = hasLeanbackFeature || hasTvCharacteristic;
1272
+
1273
+ return {
1274
+ serial,
1275
+ model,
1276
+ characteristics,
1277
+ wmSize,
1278
+ hasLeanbackFeature,
1279
+ hasTvCharacteristic,
1280
+ isLikelyTv,
1281
+ };
1282
+ }
1283
+
1284
+ async function ensureTvSuitableDevice(adbPath, serial, args) {
1285
+ const allowNonTvDevice = toBooleanFlag(args["allow-non-tv-device"], false);
1286
+ const profile = inspectAndroidDeviceProfile(adbPath, serial);
1287
+ const sizeText = profile.wmSize
1288
+ ? `${profile.wmSize.width}x${profile.wmSize.height}`
1289
+ : "unknown";
1290
+ console.log(
1291
+ `Target device profile: serial=${serial}, model=${profile.model}, size=${sizeText}, tvFeature=${profile.hasLeanbackFeature}, tvCharacteristic=${profile.hasTvCharacteristic}`,
1292
+ );
1293
+
1294
+ if (profile.isLikelyTv || allowNonTvDevice) {
1295
+ if (!profile.isLikelyTv && allowNonTvDevice) {
1296
+ console.log(
1297
+ "Warning: selected device does not look like a TV device. Continue because --allow-non-tv-device is enabled.",
1298
+ );
1299
+ }
1300
+ return profile;
1301
+ }
1302
+
1303
+ const continueWithNonTv = await askYesNo(
1304
+ `Selected device ${serial} (${profile.model}) does not look like a TV/box device. Continue with this device anyway?`,
1305
+ false,
1306
+ args,
1307
+ );
1308
+ if (!continueWithNonTv) {
1309
+ throw new Error(
1310
+ `Selected device is likely non-TV. Connect/select a TV device or pass --allow-non-tv-device true to continue with ${serial}.`,
1311
+ );
1312
+ }
1313
+ return profile;
1314
+ }
1315
+
1215
1316
  function listAvds(emulatorPath) {
1216
1317
  const result = runCommandCapture(emulatorPath, ["-list-avds"]);
1217
1318
  return result.stdout
@@ -1391,19 +1492,54 @@ async function waitForRuntimeRunning(adbPath, serial, timeoutMs) {
1391
1492
  return false;
1392
1493
  }
1393
1494
 
1495
+ function broadcastDebugServerHost(adbPath, serial, action, hostIp) {
1496
+ try {
1497
+ const result = runCommandCapture(adbPath, [
1498
+ "-s",
1499
+ serial,
1500
+ "shell",
1501
+ "am",
1502
+ "broadcast",
1503
+ "-a",
1504
+ action,
1505
+ "--es",
1506
+ "ip",
1507
+ hostIp,
1508
+ ]);
1509
+ const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
1510
+ const hasError = /(error|exception|failed)/i.test(output);
1511
+ const hasCompletion = /broadcast completed/i.test(output);
1512
+ return {
1513
+ action,
1514
+ success: hasCompletion && !hasError,
1515
+ output,
1516
+ };
1517
+ } catch (error) {
1518
+ return {
1519
+ action,
1520
+ success: false,
1521
+ output: error.message || "unknown broadcast error",
1522
+ };
1523
+ }
1524
+ }
1525
+
1394
1526
  function setRuntimeDebugServerHost(adbPath, serial, hostIp) {
1395
- runCommandCapture(adbPath, [
1396
- "-s",
1527
+ const primary = broadcastDebugServerHost(
1528
+ adbPath,
1397
1529
  serial,
1398
- "shell",
1399
- "am",
1400
- "broadcast",
1401
- "-a",
1402
1530
  RUNTIME_DEBUG_BROADCAST_ACTION,
1403
- "--es",
1404
- "ip",
1405
1531
  hostIp,
1406
- ]);
1532
+ );
1533
+ const fallback = broadcastDebugServerHost(
1534
+ adbPath,
1535
+ serial,
1536
+ RUNTIME_DEBUG_BROADCAST_ACTION_FALLBACK,
1537
+ hostIp,
1538
+ );
1539
+ return {
1540
+ success: primary.success || fallback.success,
1541
+ details: [primary, fallback],
1542
+ };
1407
1543
  }
1408
1544
 
1409
1545
  async function ensureRuntimeInstalledAndConfigured(adbPath, serial, args) {
@@ -1436,7 +1572,26 @@ async function ensureRuntimeInstalledAndConfigured(adbPath, serial, args) {
1436
1572
  Boolean(desiredRuntimeVersion) &&
1437
1573
  (!installedVersion || installedVersion !== desiredRuntimeVersion);
1438
1574
 
1439
- if (forceRuntimeInstall || !runtimeInfo.installed || shouldInstallByVersion) {
1575
+ if (runtimeInfo.installed) {
1576
+ console.log(
1577
+ `Detected runtime: installed version=${installedVersion || "unknown"}`,
1578
+ );
1579
+ } else {
1580
+ console.log("Detected runtime: not installed");
1581
+ }
1582
+
1583
+ let shouldInstallRuntime =
1584
+ forceRuntimeInstall || !runtimeInfo.installed || shouldInstallByVersion;
1585
+ if (!shouldInstallRuntime && runtimeInfo.installed) {
1586
+ const reinstallNow = await askYesNo(
1587
+ `Runtime already exists on ${serial}. Reinstall runtime now?`,
1588
+ false,
1589
+ args,
1590
+ );
1591
+ shouldInstallRuntime = reinstallNow;
1592
+ }
1593
+
1594
+ if (shouldInstallRuntime) {
1440
1595
  let targetVersion = desiredRuntimeVersion;
1441
1596
  if (!targetVersion && !overrideRuntimeUrl) {
1442
1597
  const versions = await fetchRuntimeVersions();
@@ -1483,7 +1638,15 @@ async function ensureRuntimeInstalledAndConfigured(adbPath, serial, args) {
1483
1638
  throw new Error("Runtime app did not enter running state in time.");
1484
1639
  }
1485
1640
 
1486
- setRuntimeDebugServerHost(adbPath, serial, hostIp);
1641
+ const debugServerResult = setRuntimeDebugServerHost(adbPath, serial, hostIp);
1642
+ if (!debugServerResult.success) {
1643
+ const details = debugServerResult.details
1644
+ .map((item) => `${item.action}: ${item.output || "no output"}`)
1645
+ .join(" | ");
1646
+ throw new Error(
1647
+ `Failed to configure runtime debug server host: ${hostIp}. Broadcast details: ${details}`,
1648
+ );
1649
+ }
1487
1650
  console.log(`Runtime debug server host configured: ${hostIp}`);
1488
1651
 
1489
1652
  return {
@@ -1862,6 +2025,10 @@ async function runSetupAndroidEnv(args) {
1862
2025
  const projectRoot = args.project ? path.resolve(args.project) : process.cwd();
1863
2026
  const skipRuntimeSetup = toBooleanFlag(args["skip-runtime-setup"], false);
1864
2027
  const autoEmulator = toBooleanFlag(args["auto-emulator"], true);
2028
+ const requestedSerial =
2029
+ typeof args.device === "string" && args.device.trim()
2030
+ ? args.device.trim()
2031
+ : "";
1865
2032
  const avdName =
1866
2033
  typeof args["avd-name"] === "string" && args["avd-name"].trim()
1867
2034
  ? args["avd-name"].trim()
@@ -1881,12 +2048,29 @@ async function runSetupAndroidEnv(args) {
1881
2048
  console.log(
1882
2049
  `ADB devices: connected=${deviceState.devices.length}, unauthorized=${deviceState.unauthorized.length}, offline=${deviceState.offline.length}`,
1883
2050
  );
1884
- let useConnectedDevice = deviceState.devices.length > 0;
2051
+ let useConnectedDevice = false;
1885
2052
  let shouldSetupEmulator = false;
1886
2053
  let preferredRealDeviceSerial = null;
1887
2054
 
1888
- if (deviceState.devices.length > 0) {
2055
+ if (requestedSerial) {
2056
+ if (!deviceState.devices.includes(requestedSerial)) {
2057
+ const connectedList = deviceState.devices.join(", ") || "none";
2058
+ throw new Error(
2059
+ `Requested --device ${requestedSerial} is not connected. Connected devices: ${connectedList}`,
2060
+ );
2061
+ }
2062
+ useConnectedDevice = true;
2063
+ preferredRealDeviceSerial = requestedSerial;
2064
+ console.log(`Using requested device: ${requestedSerial}`);
2065
+ }
2066
+
2067
+ if (!useConnectedDevice && deviceState.devices.length > 0) {
1889
2068
  const connectedList = deviceState.devices.join(", ");
2069
+ if (!isInteractivePromptEnabled(args)) {
2070
+ throw new Error(
2071
+ `Detected connected Android device(s): ${connectedList}. Non-interactive mode requires explicit --device <serial> to avoid selecting the wrong device.`,
2072
+ );
2073
+ }
1890
2074
  useConnectedDevice = await askYesNo(
1891
2075
  `Detected connected Android device(s): ${connectedList}. Use this device for setup?`,
1892
2076
  true,
@@ -2015,6 +2199,7 @@ async function runSetupAndroidEnv(args) {
2015
2199
  if (!targetSerial) {
2016
2200
  throw new Error("Unable to resolve target adb device serial.");
2017
2201
  }
2202
+ await ensureTvSuitableDevice(adbPath, targetSerial, args);
2018
2203
 
2019
2204
  let hostIp = getLocalIPv4Address();
2020
2205
  if (!skipRuntimeSetup) {
@@ -2179,6 +2364,7 @@ async function runRunEsapp(args) {
2179
2364
  "No Android device available. Use --device/--device-ip or run setup-android-env first.",
2180
2365
  );
2181
2366
  }
2367
+ await ensureTvSuitableDevice(adbPath, serial, args);
2182
2368
 
2183
2369
  const launchUri = buildEsappLaunchUri(args, projectRoot, {
2184
2370
  pkg: resolveProjectAppPackage(projectRoot),
@@ -2198,7 +2384,7 @@ async function runRunEsapp(args) {
2198
2384
  console.log("ES app launch command sent.");
2199
2385
  }
2200
2386
 
2201
- function parseVersionSegments(version) {
2387
+ function parseSemverSegments(version) {
2202
2388
  const normalized = String(version || "0.0.0")
2203
2389
  .trim()
2204
2390
  .split("-")[0];
@@ -2211,9 +2397,9 @@ function parseVersionSegments(version) {
2211
2397
  });
2212
2398
  }
2213
2399
 
2214
- function compareVersionStrings(left, right) {
2215
- const leftParts = parseVersionSegments(left);
2216
- const rightParts = parseVersionSegments(right);
2400
+ function compareSemverStrings(left, right) {
2401
+ const leftParts = parseSemverSegments(left);
2402
+ const rightParts = parseSemverSegments(right);
2217
2403
  const total = Math.max(leftParts.length, rightParts.length);
2218
2404
  for (let i = 0; i < total; i += 1) {
2219
2405
  const leftValue = leftParts[i] || 0;
@@ -2341,7 +2527,7 @@ function resolveSkillsSource() {
2341
2527
  }
2342
2528
 
2343
2529
  candidates.sort((left, right) => {
2344
- const versionOrder = compareVersionStrings(right.version, left.version);
2530
+ const versionOrder = compareSemverStrings(right.version, left.version);
2345
2531
  if (versionOrder !== 0) return versionOrder;
2346
2532
  return left.sourceDir.localeCompare(right.sourceDir);
2347
2533
  });
@@ -2426,6 +2612,8 @@ Options:
2426
2612
  --auto-emulator <true|false> Auto create/start emulator when no adb device
2427
2613
  --adb-path <path> Custom adb binary path/command (or set QUICKTVUI_ADB_PATH)
2428
2614
  --device-ip <ip[:port]> Preferred real device endpoint for adb connect
2615
+ --device <serial> Explicit target adb serial for setup-android-env/run-esapp
2616
+ --allow-non-tv-device <true|false> Allow phone/tablet device for TV run flow (default: false)
2429
2617
  --avd-name <name> Custom AVD name for setup-android-env
2430
2618
  --headless Start emulator with -no-window -no-audio
2431
2619
  --runtime-version <version> Pin runtime version when direct mode is used
@@ -2437,7 +2625,6 @@ Options:
2437
2625
  --port <n> Dev server port used by run-dev auto load (default: 38989)
2438
2626
  --skip-env-check Skip setup-android-env stage in run-dev
2439
2627
  --runtime-package <pkg> Runtime package name for run-esapp (default: com.extscreen.runtime)
2440
- --device <serial> Target adb device serial for run-esapp
2441
2628
  --esapp-uri <uri> Raw esapp:// / quicktv:// / appcast:// URI for run-esapp
2442
2629
  --esapp-query <json> Extra query params JSON merged into action/start URI
2443
2630
  --pkg <pkg> ES app package for run-esapp structured mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quicktvui/ai-cli",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "CLI for installing and validating QuickTVUI AI skills",
5
5
  "bin": {
6
6
  "quicktvui-ai": "bin/quicktvui-ai.js",