@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 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. If sdkmanager/avdmanager is missing, it auto-downloads and installs official Android Command-line Tools.
112
- 4. If adb is missing, it auto-installs `platform-tools` (with size estimate + sdkmanager progress).
113
- 5. Detects connected adb devices, and asks whether to use connected device.
114
- 6. If user doesn't use connected device (or no device exists), it asks for real-device IP and runs `adb connect`.
115
- 7. If still no real device, asks whether to download/start official Google Android emulator.
116
- 8. Checks Google repository reachability before emulator download; if unavailable, asks user to install emulator manually.
117
- 9. Prints estimated download size and then starts sdkmanager download (progress shown by sdkmanager).
118
- 10. Installs runtime APK and configures debug server host (direct mode by default).
119
- 11. Launches runtime app and waits it enters running state.
120
-
121
- To use official interactive setup flow instead of direct mode:
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` and `@quicktvui/cli` are installed globally.
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 = "0.1.4";
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
- runCommandCapture(adbPath, [
1388
- "-s",
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 (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) {
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 runtimeSetupMode =
1869
- typeof args["runtime-setup-mode"] === "string"
1870
- ? String(args["runtime-setup-mode"]).trim().toLowerCase()
1871
- : "direct";
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 = deviceState.devices.length > 0;
2051
+ let useConnectedDevice = false;
1892
2052
  let shouldSetupEmulator = false;
1893
2053
  let preferredRealDeviceSerial = null;
1894
2054
 
1895
- 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) {
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
- if (runtimeSetupMode === "qui") {
2029
- if (!commandCanRun("qui", ["--help"])) {
2030
- throw new Error(
2031
- "QuickTVUI CLI 'qui' is unavailable. Run: npm install -g @quicktvui/cli@latest",
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 resolveSkillsSource() {
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
- const resolved = path.join(getSkillsRoot(), "quicktvui");
2224
- if (exists(resolved)) return resolved;
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
- // Fallback for monorepo local development
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
- const localFallback = path.resolve(
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
- "skills",
2237
- "quicktvui",
2518
+ );
2519
+ addCandidate(
2520
+ buildSkillsSourceCandidateFromPackageRoot(monorepoPackageRoot, "monorepo"),
2238
2521
  );
2239
2522
 
2240
- if (!exists(localFallback)) {
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
- return localFallback;
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/quicktvui cli)
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: if not configured, run 'npm install -g @quicktvui/cli@latest' and 'qui setup'",
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.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": "^0.1.0"
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();