@quicktvui/ai-cli 1.1.1 → 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 +24 -19
- package/lib/index.js +349 -61
- package/package.json +5 -2
- package/scripts/postinstall-sync.js +34 -0
package/README.md
CHANGED
|
@@ -8,6 +8,15 @@ QuickTVUI skill runtime CLI.
|
|
|
8
8
|
npm install -g @quicktvui/ai-cli @quicktvui/ai-skills
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Starting from `@quicktvui/ai-cli@1.1.2`, global install/upgrade auto-runs
|
|
12
|
+
`quicktvui-ai update` to sync:
|
|
13
|
+
|
|
14
|
+
- `~/.agents/skills/quicktvui`
|
|
15
|
+
- `~/.gemini/GEMINI.md` bridge block
|
|
16
|
+
- `~/.gemini/settings.json` context file names
|
|
17
|
+
|
|
18
|
+
Set `QUICKTVUI_AI_SKIP_POSTINSTALL=1` to disable auto-sync.
|
|
19
|
+
|
|
11
20
|
## Commands
|
|
12
21
|
|
|
13
22
|
```bash
|
|
@@ -47,14 +56,14 @@ quicktvui-aicreate-project quick-tv-app
|
|
|
47
56
|
- `--skip-node-install`: skip Node.js install stage in `setup-vue-env`
|
|
48
57
|
- `--force-node-install`: force Node.js install stage in `setup-vue-env`
|
|
49
58
|
- `--skip-yarn-install`: skip yarn global install in `setup-vue-env`
|
|
50
|
-
- `--skip-quicktvui-cli-install`: skip `@quicktvui/cli` global install in `setup-vue-env`
|
|
51
59
|
- `--skip-project-install`: skip project dependency install in `setup-vue-env`
|
|
52
60
|
- `--auto-emulator <true|false>`: auto create/start emulator when no adb device
|
|
53
61
|
- `--adb-path <path>`: custom adb path/command (or use env `QUICKTVUI_ADB_PATH`)
|
|
54
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`)
|
|
55
65
|
- `--avd-name <name>`: custom AVD name for `setup-android-env`
|
|
56
66
|
- `--headless`: start emulator with `-no-window -no-audio`
|
|
57
|
-
- `--runtime-setup-mode <direct|qui>`: runtime setup mode (default `direct`)
|
|
58
67
|
- `--runtime-version <version>`: pin runtime version in `direct` mode
|
|
59
68
|
- `--runtime-url <url>`: use custom runtime apk url in `direct` mode
|
|
60
69
|
- `--server-host <ip>`: override debug server host IP
|
|
@@ -64,7 +73,6 @@ quicktvui-aicreate-project quick-tv-app
|
|
|
64
73
|
- `--port <n>`: dev server port used by `run-dev` auto load (default `38989`)
|
|
65
74
|
- `--skip-env-check`: skip environment stage in `run-dev`
|
|
66
75
|
- `--runtime-package <pkg>`: runtime package for `run-esapp` (default `com.extscreen.runtime`)
|
|
67
|
-
- `--device <serial>`: target adb serial for `run-esapp`
|
|
68
76
|
- `--esapp-uri <uri>`: raw launch URI (`esapp://`, `quicktv://`, `appcast://`)
|
|
69
77
|
- `--esapp-query <json>`: extra query params JSON merged in structured mode
|
|
70
78
|
- `--pkg --ver --min-ver --repository --uri --from --args --exp --flags --use-latest`: structured `esapp://action/start` params
|
|
@@ -108,21 +116,18 @@ This command:
|
|
|
108
116
|
|
|
109
117
|
1. Detects Android SDK root (auto creates a default SDK root when missing).
|
|
110
118
|
2. Detects Android SDK/adb/emulator tools. `@quicktvui/ai-cli` does not bundle adb.
|
|
111
|
-
3.
|
|
112
|
-
4.
|
|
113
|
-
5.
|
|
114
|
-
6. If
|
|
115
|
-
7.
|
|
116
|
-
8.
|
|
117
|
-
9.
|
|
118
|
-
10.
|
|
119
|
-
11.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
quicktvui-ai setup-android-env --project ./quick-tv-app --runtime-setup-mode qui
|
|
125
|
-
```
|
|
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.
|
|
126
131
|
|
|
127
132
|
## Configure Vue env (Node + package manager)
|
|
128
133
|
|
|
@@ -133,7 +138,7 @@ quicktvui-ai setup-vue-env --project ./quick-tv-app
|
|
|
133
138
|
This command:
|
|
134
139
|
|
|
135
140
|
1. Ensures Node.js LTS (macOS/Windows auto install).
|
|
136
|
-
2. Ensures `yarn`
|
|
141
|
+
2. Ensures `yarn` is installed globally.
|
|
137
142
|
3. Installs project dependencies (`yarn install` or `npm install` fallback).
|
|
138
143
|
|
|
139
144
|
## Configure All Dev Envs
|
package/lib/index.js
CHANGED
|
@@ -7,7 +7,13 @@ const https = require("https");
|
|
|
7
7
|
const net = require("net");
|
|
8
8
|
const { spawnSync, spawn } = require("child_process");
|
|
9
9
|
|
|
10
|
-
const PACKAGE_VERSION =
|
|
10
|
+
const PACKAGE_VERSION = (() => {
|
|
11
|
+
try {
|
|
12
|
+
return require("../package.json").version || "0.0.0";
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return "0.0.0";
|
|
15
|
+
}
|
|
16
|
+
})();
|
|
11
17
|
const DEFAULT_INSTALL_DIR = path.join(
|
|
12
18
|
os.homedir(),
|
|
13
19
|
".agents",
|
|
@@ -25,6 +31,8 @@ const RUNTIME_LAUNCH_ACTIVITY =
|
|
|
25
31
|
"com.extscreen.runtime/com.extscreen.runtime.LauncherAlias";
|
|
26
32
|
const RUNTIME_DEBUG_BROADCAST_ACTION =
|
|
27
33
|
"com.extscreen.runtime.ACTION_CHANGE_DEBUG_SERVER";
|
|
34
|
+
const RUNTIME_DEBUG_BROADCAST_ACTION_FALLBACK =
|
|
35
|
+
"tv.eskit.debugger.ACTION_CHANGE_DEBUG_SERVER";
|
|
28
36
|
const RUNTIME_REPOSITORY_ROOT =
|
|
29
37
|
"http://hub.quicktvui.com/repository/maven-files/apk/runtime/dev";
|
|
30
38
|
const RUNTIME_REPOSITORY_METADATA_URL = `${RUNTIME_REPOSITORY_ROOT}/maven-metadata.xml`;
|
|
@@ -33,6 +41,8 @@ const REQUIRED_SKILL_FILES = [
|
|
|
33
41
|
"SKILL.md",
|
|
34
42
|
path.join("references", "scaffold-checklist.md"),
|
|
35
43
|
path.join("references", "create-project-checklist.md"),
|
|
44
|
+
path.join("references", "dev-env-checklist.md"),
|
|
45
|
+
path.join("references", "esapp-protocol-cheatsheet.md"),
|
|
36
46
|
path.join("references", "lookup-checklist.md"),
|
|
37
47
|
path.join("references", "bug-report-template.md"),
|
|
38
48
|
];
|
|
@@ -1204,6 +1214,105 @@ function listConnectedDevices(adbPath) {
|
|
|
1204
1214
|
return parseAdbDevices(result.stdout);
|
|
1205
1215
|
}
|
|
1206
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
|
+
|
|
1207
1316
|
function listAvds(emulatorPath) {
|
|
1208
1317
|
const result = runCommandCapture(emulatorPath, ["-list-avds"]);
|
|
1209
1318
|
return result.stdout
|
|
@@ -1383,19 +1492,54 @@ async function waitForRuntimeRunning(adbPath, serial, timeoutMs) {
|
|
|
1383
1492
|
return false;
|
|
1384
1493
|
}
|
|
1385
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
|
+
|
|
1386
1526
|
function setRuntimeDebugServerHost(adbPath, serial, hostIp) {
|
|
1387
|
-
|
|
1388
|
-
|
|
1527
|
+
const primary = broadcastDebugServerHost(
|
|
1528
|
+
adbPath,
|
|
1389
1529
|
serial,
|
|
1390
|
-
"shell",
|
|
1391
|
-
"am",
|
|
1392
|
-
"broadcast",
|
|
1393
|
-
"-a",
|
|
1394
1530
|
RUNTIME_DEBUG_BROADCAST_ACTION,
|
|
1395
|
-
"--es",
|
|
1396
|
-
"ip",
|
|
1397
1531
|
hostIp,
|
|
1398
|
-
|
|
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
|
+
};
|
|
1399
1543
|
}
|
|
1400
1544
|
|
|
1401
1545
|
async function ensureRuntimeInstalledAndConfigured(adbPath, serial, args) {
|
|
@@ -1428,7 +1572,26 @@ async function ensureRuntimeInstalledAndConfigured(adbPath, serial, args) {
|
|
|
1428
1572
|
Boolean(desiredRuntimeVersion) &&
|
|
1429
1573
|
(!installedVersion || installedVersion !== desiredRuntimeVersion);
|
|
1430
1574
|
|
|
1431
|
-
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) {
|
|
1432
1595
|
let targetVersion = desiredRuntimeVersion;
|
|
1433
1596
|
if (!targetVersion && !overrideRuntimeUrl) {
|
|
1434
1597
|
const versions = await fetchRuntimeVersions();
|
|
@@ -1475,7 +1638,15 @@ async function ensureRuntimeInstalledAndConfigured(adbPath, serial, args) {
|
|
|
1475
1638
|
throw new Error("Runtime app did not enter running state in time.");
|
|
1476
1639
|
}
|
|
1477
1640
|
|
|
1478
|
-
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
|
+
}
|
|
1479
1650
|
console.log(`Runtime debug server host configured: ${hostIp}`);
|
|
1480
1651
|
|
|
1481
1652
|
return {
|
|
@@ -1783,10 +1954,6 @@ async function runSetupVueEnv(args) {
|
|
|
1783
1954
|
);
|
|
1784
1955
|
const skipNodeInstall = toBooleanFlag(args["skip-node-install"], false);
|
|
1785
1956
|
const skipYarnInstall = toBooleanFlag(args["skip-yarn-install"], false);
|
|
1786
|
-
const skipQuicktvuiCliInstall = toBooleanFlag(
|
|
1787
|
-
args["skip-quicktvui-cli-install"],
|
|
1788
|
-
false,
|
|
1789
|
-
);
|
|
1790
1957
|
const skipProjectInstall = toBooleanFlag(args["skip-project-install"], false);
|
|
1791
1958
|
|
|
1792
1959
|
if (
|
|
@@ -1818,13 +1985,6 @@ async function runSetupVueEnv(args) {
|
|
|
1818
1985
|
runCommand("npm", ["install", "-g", "yarn"], { stdio: "inherit" });
|
|
1819
1986
|
}
|
|
1820
1987
|
|
|
1821
|
-
if (!skipQuicktvuiCliInstall && !commandCanRun("qui", ["--help"])) {
|
|
1822
|
-
console.log("Installing @quicktvui/cli globally...");
|
|
1823
|
-
runCommand("npm", ["install", "-g", "@quicktvui/cli@latest"], {
|
|
1824
|
-
stdio: "inherit",
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
1988
|
if (!skipProjectInstall) {
|
|
1829
1989
|
if (!exists(path.join(projectRoot, "package.json"))) {
|
|
1830
1990
|
throw new Error(`Missing package.json in project root: ${projectRoot}`);
|
|
@@ -1865,10 +2025,10 @@ async function runSetupAndroidEnv(args) {
|
|
|
1865
2025
|
const projectRoot = args.project ? path.resolve(args.project) : process.cwd();
|
|
1866
2026
|
const skipRuntimeSetup = toBooleanFlag(args["skip-runtime-setup"], false);
|
|
1867
2027
|
const autoEmulator = toBooleanFlag(args["auto-emulator"], true);
|
|
1868
|
-
const
|
|
1869
|
-
typeof args
|
|
1870
|
-
?
|
|
1871
|
-
: "
|
|
2028
|
+
const requestedSerial =
|
|
2029
|
+
typeof args.device === "string" && args.device.trim()
|
|
2030
|
+
? args.device.trim()
|
|
2031
|
+
: "";
|
|
1872
2032
|
const avdName =
|
|
1873
2033
|
typeof args["avd-name"] === "string" && args["avd-name"].trim()
|
|
1874
2034
|
? args["avd-name"].trim()
|
|
@@ -1888,12 +2048,29 @@ async function runSetupAndroidEnv(args) {
|
|
|
1888
2048
|
console.log(
|
|
1889
2049
|
`ADB devices: connected=${deviceState.devices.length}, unauthorized=${deviceState.unauthorized.length}, offline=${deviceState.offline.length}`,
|
|
1890
2050
|
);
|
|
1891
|
-
let useConnectedDevice =
|
|
2051
|
+
let useConnectedDevice = false;
|
|
1892
2052
|
let shouldSetupEmulator = false;
|
|
1893
2053
|
let preferredRealDeviceSerial = null;
|
|
1894
2054
|
|
|
1895
|
-
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) {
|
|
1896
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
|
+
}
|
|
1897
2074
|
useConnectedDevice = await askYesNo(
|
|
1898
2075
|
`Detected connected Android device(s): ${connectedList}. Use this device for setup?`,
|
|
1899
2076
|
true,
|
|
@@ -2022,26 +2199,16 @@ async function runSetupAndroidEnv(args) {
|
|
|
2022
2199
|
if (!targetSerial) {
|
|
2023
2200
|
throw new Error("Unable to resolve target adb device serial.");
|
|
2024
2201
|
}
|
|
2202
|
+
await ensureTvSuitableDevice(adbPath, targetSerial, args);
|
|
2025
2203
|
|
|
2026
2204
|
let hostIp = getLocalIPv4Address();
|
|
2027
2205
|
if (!skipRuntimeSetup) {
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
console.log("Running 'qui setup' to configure runtime APK...");
|
|
2035
|
-
runCommand("qui", ["setup"], { cwd: projectRoot });
|
|
2036
|
-
hostIp = getLocalIPv4Address();
|
|
2037
|
-
} else {
|
|
2038
|
-
const runtimeResult = await ensureRuntimeInstalledAndConfigured(
|
|
2039
|
-
adbPath,
|
|
2040
|
-
targetSerial,
|
|
2041
|
-
args,
|
|
2042
|
-
);
|
|
2043
|
-
hostIp = runtimeResult.hostIp;
|
|
2044
|
-
}
|
|
2206
|
+
const runtimeResult = await ensureRuntimeInstalledAndConfigured(
|
|
2207
|
+
adbPath,
|
|
2208
|
+
targetSerial,
|
|
2209
|
+
args,
|
|
2210
|
+
);
|
|
2211
|
+
hostIp = runtimeResult.hostIp;
|
|
2045
2212
|
} else {
|
|
2046
2213
|
console.log("Skip runtime setup due to --skip-runtime-setup.");
|
|
2047
2214
|
}
|
|
@@ -2197,6 +2364,7 @@ async function runRunEsapp(args) {
|
|
|
2197
2364
|
"No Android device available. Use --device/--device-ip or run setup-android-env first.",
|
|
2198
2365
|
);
|
|
2199
2366
|
}
|
|
2367
|
+
await ensureTvSuitableDevice(adbPath, serial, args);
|
|
2200
2368
|
|
|
2201
2369
|
const launchUri = buildEsappLaunchUri(args, projectRoot, {
|
|
2202
2370
|
pkg: resolveProjectAppPackage(projectRoot),
|
|
@@ -2216,34 +2384,155 @@ async function runRunEsapp(args) {
|
|
|
2216
2384
|
console.log("ES app launch command sent.");
|
|
2217
2385
|
}
|
|
2218
2386
|
|
|
2219
|
-
function
|
|
2387
|
+
function parseSemverSegments(version) {
|
|
2388
|
+
const normalized = String(version || "0.0.0")
|
|
2389
|
+
.trim()
|
|
2390
|
+
.split("-")[0];
|
|
2391
|
+
const parts = normalized ? normalized.split(".") : [];
|
|
2392
|
+
if (parts.length === 0) return [0, 0, 0];
|
|
2393
|
+
|
|
2394
|
+
return parts.map((part) => {
|
|
2395
|
+
const parsed = Number.parseInt(part, 10);
|
|
2396
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function compareSemverStrings(left, right) {
|
|
2401
|
+
const leftParts = parseSemverSegments(left);
|
|
2402
|
+
const rightParts = parseSemverSegments(right);
|
|
2403
|
+
const total = Math.max(leftParts.length, rightParts.length);
|
|
2404
|
+
for (let i = 0; i < total; i += 1) {
|
|
2405
|
+
const leftValue = leftParts[i] || 0;
|
|
2406
|
+
const rightValue = rightParts[i] || 0;
|
|
2407
|
+
if (leftValue > rightValue) return 1;
|
|
2408
|
+
if (leftValue < rightValue) return -1;
|
|
2409
|
+
}
|
|
2410
|
+
return 0;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
function buildSkillsSourceCandidateFromPackageRoot(packageRoot, sourceLabel) {
|
|
2414
|
+
if (!packageRoot) return null;
|
|
2415
|
+
const resolvedPackageRoot = path.resolve(packageRoot);
|
|
2416
|
+
const sourceDir = path.join(resolvedPackageRoot, "skills", "quicktvui");
|
|
2417
|
+
if (!exists(sourceDir)) return null;
|
|
2418
|
+
|
|
2419
|
+
let version = "0.0.0";
|
|
2420
|
+
const packageJsonPath = path.join(resolvedPackageRoot, "package.json");
|
|
2421
|
+
if (exists(packageJsonPath)) {
|
|
2422
|
+
try {
|
|
2423
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
2424
|
+
if (
|
|
2425
|
+
packageJson &&
|
|
2426
|
+
typeof packageJson.version === "string" &&
|
|
2427
|
+
packageJson.version.trim()
|
|
2428
|
+
) {
|
|
2429
|
+
version = packageJson.version.trim();
|
|
2430
|
+
}
|
|
2431
|
+
} catch (error) {
|
|
2432
|
+
// Ignore parse errors; keep default version for ordering.
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
return {
|
|
2437
|
+
sourceLabel,
|
|
2438
|
+
sourceDir,
|
|
2439
|
+
packageRoot: resolvedPackageRoot,
|
|
2440
|
+
version,
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
function buildSkillsSourceCandidateFromRequire() {
|
|
2220
2445
|
try {
|
|
2221
|
-
// Preferred: installed dependency
|
|
2222
2446
|
const { getSkillsRoot } = require("@quicktvui/ai-skills");
|
|
2223
|
-
|
|
2224
|
-
|
|
2447
|
+
if (typeof getSkillsRoot !== "function") return null;
|
|
2448
|
+
const resolvedSkillsRoot = getSkillsRoot();
|
|
2449
|
+
const sourceDir = path.join(resolvedSkillsRoot, "quicktvui");
|
|
2450
|
+
if (!exists(sourceDir)) return null;
|
|
2451
|
+
|
|
2452
|
+
let packageRoot = null;
|
|
2453
|
+
let version = "0.0.0";
|
|
2454
|
+
try {
|
|
2455
|
+
const packageJsonPath =
|
|
2456
|
+
require.resolve("@quicktvui/ai-skills/package.json");
|
|
2457
|
+
packageRoot = path.dirname(packageJsonPath);
|
|
2458
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
2459
|
+
if (
|
|
2460
|
+
packageJson &&
|
|
2461
|
+
typeof packageJson.version === "string" &&
|
|
2462
|
+
packageJson.version.trim()
|
|
2463
|
+
) {
|
|
2464
|
+
version = packageJson.version.trim();
|
|
2465
|
+
}
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
// Keep fallback metadata if package.json cannot be resolved.
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
return {
|
|
2471
|
+
sourceLabel: "dependency-resolved",
|
|
2472
|
+
sourceDir: path.resolve(sourceDir),
|
|
2473
|
+
packageRoot: packageRoot ? path.resolve(packageRoot) : null,
|
|
2474
|
+
version,
|
|
2475
|
+
};
|
|
2225
2476
|
} catch (error) {
|
|
2226
|
-
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function resolveSkillsSource() {
|
|
2482
|
+
const candidates = [];
|
|
2483
|
+
const seenSourceDirs = new Set();
|
|
2484
|
+
|
|
2485
|
+
function addCandidate(candidate) {
|
|
2486
|
+
if (!candidate || !candidate.sourceDir) return;
|
|
2487
|
+
const key = path.resolve(candidate.sourceDir);
|
|
2488
|
+
if (seenSourceDirs.has(key)) return;
|
|
2489
|
+
seenSourceDirs.add(key);
|
|
2490
|
+
candidates.push(candidate);
|
|
2227
2491
|
}
|
|
2228
2492
|
|
|
2229
|
-
|
|
2493
|
+
// Prefer globally upgraded sibling package if present.
|
|
2494
|
+
const globalSiblingPackageRoot = path.resolve(
|
|
2495
|
+
__dirname,
|
|
2496
|
+
"..",
|
|
2497
|
+
"..",
|
|
2498
|
+
"ai-skills",
|
|
2499
|
+
);
|
|
2500
|
+
addCandidate(
|
|
2501
|
+
buildSkillsSourceCandidateFromPackageRoot(
|
|
2502
|
+
globalSiblingPackageRoot,
|
|
2503
|
+
"global-sibling",
|
|
2504
|
+
),
|
|
2505
|
+
);
|
|
2506
|
+
|
|
2507
|
+
// Dependency resolution keeps compatibility when only @quicktvui/ai-cli is installed.
|
|
2508
|
+
addCandidate(buildSkillsSourceCandidateFromRequire());
|
|
2509
|
+
|
|
2510
|
+
// Monorepo fallback for local development.
|
|
2511
|
+
const monorepoPackageRoot = path.resolve(
|
|
2230
2512
|
__dirname,
|
|
2231
2513
|
"..",
|
|
2232
2514
|
"..",
|
|
2233
2515
|
"..",
|
|
2234
2516
|
"packages",
|
|
2235
2517
|
"quicktvui-ai-skills",
|
|
2236
|
-
|
|
2237
|
-
|
|
2518
|
+
);
|
|
2519
|
+
addCandidate(
|
|
2520
|
+
buildSkillsSourceCandidateFromPackageRoot(monorepoPackageRoot, "monorepo"),
|
|
2238
2521
|
);
|
|
2239
2522
|
|
|
2240
|
-
if (
|
|
2523
|
+
if (candidates.length === 0) {
|
|
2241
2524
|
throw new Error(
|
|
2242
2525
|
"Unable to locate QuickTVUI skill assets. Install @quicktvui/ai-skills or use repository local layout.",
|
|
2243
2526
|
);
|
|
2244
2527
|
}
|
|
2245
2528
|
|
|
2246
|
-
|
|
2529
|
+
candidates.sort((left, right) => {
|
|
2530
|
+
const versionOrder = compareSemverStrings(right.version, left.version);
|
|
2531
|
+
if (versionOrder !== 0) return versionOrder;
|
|
2532
|
+
return left.sourceDir.localeCompare(right.sourceDir);
|
|
2533
|
+
});
|
|
2534
|
+
|
|
2535
|
+
return candidates[0].sourceDir;
|
|
2247
2536
|
}
|
|
2248
2537
|
|
|
2249
2538
|
function getInstallDir(args) {
|
|
@@ -2295,7 +2584,7 @@ Commands:
|
|
|
2295
2584
|
validate Strict check for required skill files (non-zero exit if missing)
|
|
2296
2585
|
update Reinstall skill assets to target directory
|
|
2297
2586
|
create-project Create a QuickTVUI project (remote clone, local fallback)
|
|
2298
|
-
setup-vue-env Setup Vue development environment (Node/yarn
|
|
2587
|
+
setup-vue-env Setup Vue development environment (Node/yarn)
|
|
2299
2588
|
setup-android-env Configure Android device/emulator + runtime environment
|
|
2300
2589
|
setup-all-env Setup both Vue and Android development environments
|
|
2301
2590
|
run-dev Run project dev command (optionally checks Android env first)
|
|
@@ -2319,14 +2608,14 @@ Options:
|
|
|
2319
2608
|
--skip-node-install Skip Node.js auto-install stage in setup-vue-env
|
|
2320
2609
|
--force-node-install Force Node.js auto-install even if current version is ok
|
|
2321
2610
|
--skip-yarn-install Skip yarn global install in setup-vue-env
|
|
2322
|
-
--skip-quicktvui-cli-install Skip @quicktvui/cli global install in setup-vue-env
|
|
2323
2611
|
--skip-project-install Skip project dependency install in setup-vue-env
|
|
2324
2612
|
--auto-emulator <true|false> Auto create/start emulator when no adb device
|
|
2325
2613
|
--adb-path <path> Custom adb binary path/command (or set QUICKTVUI_ADB_PATH)
|
|
2326
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)
|
|
2327
2617
|
--avd-name <name> Custom AVD name for setup-android-env
|
|
2328
2618
|
--headless Start emulator with -no-window -no-audio
|
|
2329
|
-
--runtime-setup-mode <direct|qui> Runtime setup mode in setup-android-env (default: direct)
|
|
2330
2619
|
--runtime-version <version> Pin runtime version when direct mode is used
|
|
2331
2620
|
--runtime-url <url> Use custom runtime apk url in direct mode
|
|
2332
2621
|
--server-host <ip> Override local debug server host IP
|
|
@@ -2336,7 +2625,6 @@ Options:
|
|
|
2336
2625
|
--port <n> Dev server port used by run-dev auto load (default: 38989)
|
|
2337
2626
|
--skip-env-check Skip setup-android-env stage in run-dev
|
|
2338
2627
|
--runtime-package <pkg> Runtime package name for run-esapp (default: com.extscreen.runtime)
|
|
2339
|
-
--device <serial> Target adb device serial for run-esapp
|
|
2340
2628
|
--esapp-uri <uri> Raw esapp:// / quicktv:// / appcast:// URI for run-esapp
|
|
2341
2629
|
--esapp-query <json> Extra query params JSON merged into action/start URI
|
|
2342
2630
|
--pkg <pkg> ES app package for run-esapp structured mode
|
|
@@ -2771,7 +3059,7 @@ async function runCreateProject(args) {
|
|
|
2771
3059
|
"- ensured: package.json name/version updated and @quicktvui/ai added to devDependencies",
|
|
2772
3060
|
);
|
|
2773
3061
|
console.log(
|
|
2774
|
-
"- next:
|
|
3062
|
+
"- next: run 'quicktvui-ai setup-all-env --project <project-path>' to prepare development and runtime environments",
|
|
2775
3063
|
);
|
|
2776
3064
|
}
|
|
2777
3065
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quicktvui/ai-cli",
|
|
3
|
-
"version": "1.1.
|
|
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",
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
"quicktvui-aicreate-project": "bin/quicktvui-aicreate-project.js"
|
|
9
9
|
},
|
|
10
10
|
"main": "lib/index.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"postinstall": "node scripts/postinstall-sync.js"
|
|
13
|
+
},
|
|
11
14
|
"files": [
|
|
12
15
|
"bin",
|
|
13
16
|
"lib",
|
|
@@ -25,6 +28,6 @@
|
|
|
25
28
|
"author": "QuickTVUI",
|
|
26
29
|
"license": "MIT",
|
|
27
30
|
"dependencies": {
|
|
28
|
-
"@quicktvui/ai-skills": "^
|
|
31
|
+
"@quicktvui/ai-skills": "^1.1.1"
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { runCli } = require("../lib/index");
|
|
4
|
+
|
|
5
|
+
function isTruthy(value) {
|
|
6
|
+
return value === true || value === "true" || value === "1";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isGlobalInstall() {
|
|
10
|
+
if (isTruthy(process.env.npm_config_global)) return true;
|
|
11
|
+
return process.env.npm_config_location === "global";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
if (isTruthy(process.env.QUICKTVUI_AI_SKIP_POSTINSTALL)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!isGlobalInstall()) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await runCli(["update"]);
|
|
24
|
+
console.log(
|
|
25
|
+
"[quicktvui-ai] postinstall: synced latest skill files into ~/.agents/skills/quicktvui.",
|
|
26
|
+
);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn(
|
|
29
|
+
`[quicktvui-ai] postinstall: unable to auto-sync skill files (${error.message}). Run 'quicktvui-ai update' manually.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
main();
|