@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.
- package/README.md +14 -10
- package/lib/index.js +206 -19
- 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.
|
|
119
|
-
4.
|
|
120
|
-
5.
|
|
121
|
-
6. If
|
|
122
|
-
7.
|
|
123
|
-
8.
|
|
124
|
-
9.
|
|
125
|
-
10.
|
|
126
|
-
11.
|
|
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
|
-
|
|
1396
|
-
|
|
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 (
|
|
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 =
|
|
2051
|
+
let useConnectedDevice = false;
|
|
1885
2052
|
let shouldSetupEmulator = false;
|
|
1886
2053
|
let preferredRealDeviceSerial = null;
|
|
1887
2054
|
|
|
1888
|
-
if (
|
|
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
|
|
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
|
|
2215
|
-
const leftParts =
|
|
2216
|
-
const rightParts =
|
|
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 =
|
|
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
|