@quicktvui/ai-cli 1.1.2 → 1.1.5

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
@@ -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,8 +73,7 @@ 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
- - `--esapp-uri <uri>`: raw launch URI (`esapp://`, `quicktv://`, `appcast://`)
76
+ - `--esapp-uri <uri>`: raw launch URI (`esapp://`)
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
78
79
 
@@ -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
 
@@ -1,4 +1,4 @@
1
- # ESApp Protocol Reference (`esapp://`, `quicktv://`, `appcast://`)
1
+ # ESApp Protocol Reference (`esapp://`)
2
2
 
3
3
  This document summarizes runtime launch protocol behavior from:
4
4
 
@@ -12,8 +12,6 @@ This document summarizes runtime launch protocol behavior from:
12
12
  Runtime entry activity accepts:
13
13
 
14
14
  - `esapp://`
15
- - `quicktv://`
16
- - `appcast://`
17
15
 
18
16
  ## 2) Parsing Modes
19
17
 
@@ -29,13 +27,11 @@ Behavior:
29
27
  - `/start` maps to `action=start_es`.
30
28
  - All query params are forwarded to protocol JSON.
31
29
 
32
- ### Mode B: V2 package URI (esapp/quicktv/appcast)
30
+ ### Mode B: V2 package URI (esapp)
33
31
 
34
32
  ```text
35
33
  esapp://es.hello.world?from=cmd
36
34
  esapp://es.hello.world/1.0.3?from=cmd
37
- quicktv://es.hello.world?from=cmd
38
- appcast://es.hello.world/1.0.3?from=cmd
39
35
  ```
40
36
 
41
37
  Behavior:
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 {
@@ -1551,18 +1714,18 @@ function ensureSupportedEsappUri(uri) {
1551
1714
  const normalized = String(uri || "")
1552
1715
  .trim()
1553
1716
  .toLowerCase();
1554
- if (
1555
- normalized.startsWith("esapp://") ||
1556
- normalized.startsWith("quicktv://") ||
1557
- normalized.startsWith("appcast://")
1558
- ) {
1717
+ if (normalized.startsWith("esapp://")) {
1559
1718
  return;
1560
1719
  }
1561
1720
  throw new Error(
1562
- `Unsupported URI scheme for runtime launch: ${uri}. Use esapp://, quicktv://, or appcast://.`,
1721
+ `Unsupported URI scheme for runtime launch: ${uri}. Use esapp:// only.`,
1563
1722
  );
1564
1723
  }
1565
1724
 
1725
+ function quotePosixShellArg(value) {
1726
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
1727
+ }
1728
+
1566
1729
  function buildEsappLaunchUri(args, projectRoot, defaults = {}) {
1567
1730
  const positional =
1568
1731
  args._ && typeof args._[1] === "string" ? args._[1].trim() : "";
@@ -1644,18 +1807,19 @@ function buildEsappLaunchUri(args, projectRoot, defaults = {}) {
1644
1807
 
1645
1808
  function startEsappOnRuntimeByUri(adbPath, serial, runtimePackage, launchUri) {
1646
1809
  ensureSupportedEsappUri(launchUri);
1810
+ // Use explicit POSIX shell quoting so `&` in URI query is not truncated by
1811
+ // remote Android shell parsing (e.g. `...from=cmd&pkg=...`).
1812
+ const shellCommand = [
1813
+ "am start",
1814
+ "-a android.intent.action.VIEW",
1815
+ `-p ${quotePosixShellArg(runtimePackage)}`,
1816
+ `-d ${quotePosixShellArg(launchUri)}`,
1817
+ ].join(" ");
1647
1818
  const result = runCommandCapture(adbPath, [
1648
1819
  "-s",
1649
1820
  serial,
1650
1821
  "shell",
1651
- "am",
1652
- "start",
1653
- "-a",
1654
- "android.intent.action.VIEW",
1655
- "-p",
1656
- runtimePackage,
1657
- "-d",
1658
- launchUri,
1822
+ shellCommand,
1659
1823
  ]);
1660
1824
  const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
1661
1825
  if (/(^|\s)error[:\s]/i.test(output) || /exception/i.test(output)) {
@@ -1862,6 +2026,10 @@ async function runSetupAndroidEnv(args) {
1862
2026
  const projectRoot = args.project ? path.resolve(args.project) : process.cwd();
1863
2027
  const skipRuntimeSetup = toBooleanFlag(args["skip-runtime-setup"], false);
1864
2028
  const autoEmulator = toBooleanFlag(args["auto-emulator"], true);
2029
+ const requestedSerial =
2030
+ typeof args.device === "string" && args.device.trim()
2031
+ ? args.device.trim()
2032
+ : "";
1865
2033
  const avdName =
1866
2034
  typeof args["avd-name"] === "string" && args["avd-name"].trim()
1867
2035
  ? args["avd-name"].trim()
@@ -1881,12 +2049,29 @@ async function runSetupAndroidEnv(args) {
1881
2049
  console.log(
1882
2050
  `ADB devices: connected=${deviceState.devices.length}, unauthorized=${deviceState.unauthorized.length}, offline=${deviceState.offline.length}`,
1883
2051
  );
1884
- let useConnectedDevice = deviceState.devices.length > 0;
2052
+ let useConnectedDevice = false;
1885
2053
  let shouldSetupEmulator = false;
1886
2054
  let preferredRealDeviceSerial = null;
1887
2055
 
1888
- if (deviceState.devices.length > 0) {
2056
+ if (requestedSerial) {
2057
+ if (!deviceState.devices.includes(requestedSerial)) {
2058
+ const connectedList = deviceState.devices.join(", ") || "none";
2059
+ throw new Error(
2060
+ `Requested --device ${requestedSerial} is not connected. Connected devices: ${connectedList}`,
2061
+ );
2062
+ }
2063
+ useConnectedDevice = true;
2064
+ preferredRealDeviceSerial = requestedSerial;
2065
+ console.log(`Using requested device: ${requestedSerial}`);
2066
+ }
2067
+
2068
+ if (!useConnectedDevice && deviceState.devices.length > 0) {
1889
2069
  const connectedList = deviceState.devices.join(", ");
2070
+ if (!isInteractivePromptEnabled(args)) {
2071
+ throw new Error(
2072
+ `Detected connected Android device(s): ${connectedList}. Non-interactive mode requires explicit --device <serial> to avoid selecting the wrong device.`,
2073
+ );
2074
+ }
1890
2075
  useConnectedDevice = await askYesNo(
1891
2076
  `Detected connected Android device(s): ${connectedList}. Use this device for setup?`,
1892
2077
  true,
@@ -2015,6 +2200,7 @@ async function runSetupAndroidEnv(args) {
2015
2200
  if (!targetSerial) {
2016
2201
  throw new Error("Unable to resolve target adb device serial.");
2017
2202
  }
2203
+ await ensureTvSuitableDevice(adbPath, targetSerial, args);
2018
2204
 
2019
2205
  let hostIp = getLocalIPv4Address();
2020
2206
  if (!skipRuntimeSetup) {
@@ -2179,6 +2365,7 @@ async function runRunEsapp(args) {
2179
2365
  "No Android device available. Use --device/--device-ip or run setup-android-env first.",
2180
2366
  );
2181
2367
  }
2368
+ await ensureTvSuitableDevice(adbPath, serial, args);
2182
2369
 
2183
2370
  const launchUri = buildEsappLaunchUri(args, projectRoot, {
2184
2371
  pkg: resolveProjectAppPackage(projectRoot),
@@ -2198,7 +2385,7 @@ async function runRunEsapp(args) {
2198
2385
  console.log("ES app launch command sent.");
2199
2386
  }
2200
2387
 
2201
- function parseVersionSegments(version) {
2388
+ function parseSemverSegments(version) {
2202
2389
  const normalized = String(version || "0.0.0")
2203
2390
  .trim()
2204
2391
  .split("-")[0];
@@ -2211,9 +2398,9 @@ function parseVersionSegments(version) {
2211
2398
  });
2212
2399
  }
2213
2400
 
2214
- function compareVersionStrings(left, right) {
2215
- const leftParts = parseVersionSegments(left);
2216
- const rightParts = parseVersionSegments(right);
2401
+ function compareSemverStrings(left, right) {
2402
+ const leftParts = parseSemverSegments(left);
2403
+ const rightParts = parseSemverSegments(right);
2217
2404
  const total = Math.max(leftParts.length, rightParts.length);
2218
2405
  for (let i = 0; i < total; i += 1) {
2219
2406
  const leftValue = leftParts[i] || 0;
@@ -2341,7 +2528,7 @@ function resolveSkillsSource() {
2341
2528
  }
2342
2529
 
2343
2530
  candidates.sort((left, right) => {
2344
- const versionOrder = compareVersionStrings(right.version, left.version);
2531
+ const versionOrder = compareSemverStrings(right.version, left.version);
2345
2532
  if (versionOrder !== 0) return versionOrder;
2346
2533
  return left.sourceDir.localeCompare(right.sourceDir);
2347
2534
  });
@@ -2426,6 +2613,8 @@ Options:
2426
2613
  --auto-emulator <true|false> Auto create/start emulator when no adb device
2427
2614
  --adb-path <path> Custom adb binary path/command (or set QUICKTVUI_ADB_PATH)
2428
2615
  --device-ip <ip[:port]> Preferred real device endpoint for adb connect
2616
+ --device <serial> Explicit target adb serial for setup-android-env/run-esapp
2617
+ --allow-non-tv-device <true|false> Allow phone/tablet device for TV run flow (default: false)
2429
2618
  --avd-name <name> Custom AVD name for setup-android-env
2430
2619
  --headless Start emulator with -no-window -no-audio
2431
2620
  --runtime-version <version> Pin runtime version when direct mode is used
@@ -2437,8 +2626,7 @@ Options:
2437
2626
  --port <n> Dev server port used by run-dev auto load (default: 38989)
2438
2627
  --skip-env-check Skip setup-android-env stage in run-dev
2439
2628
  --runtime-package <pkg> Runtime package name for run-esapp (default: com.extscreen.runtime)
2440
- --device <serial> Target adb device serial for run-esapp
2441
- --esapp-uri <uri> Raw esapp:// / quicktv:// / appcast:// URI for run-esapp
2629
+ --esapp-uri <uri> Raw esapp:// URI for run-esapp
2442
2630
  --esapp-query <json> Extra query params JSON merged into action/start URI
2443
2631
  --pkg <pkg> ES app package for run-esapp structured mode
2444
2632
  --ver <version> ES app version 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.5",
4
4
  "description": "CLI for installing and validating QuickTVUI AI skills",
5
5
  "bin": {
6
6
  "quicktvui-ai": "bin/quicktvui-ai.js",
@@ -1,16 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { runCli } = require("../lib/index");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
4
7
 
5
8
  function isTruthy(value) {
6
9
  return value === true || value === "true" || value === "1";
7
10
  }
8
11
 
12
+ function exists(filePath) {
13
+ return fs.existsSync(filePath);
14
+ }
15
+
9
16
  function isGlobalInstall() {
10
17
  if (isTruthy(process.env.npm_config_global)) return true;
11
18
  return process.env.npm_config_location === "global";
12
19
  }
13
20
 
21
+ function resolveCodexHome() {
22
+ const fromEnv = process.env.CODEX_HOME
23
+ ? String(process.env.CODEX_HOME).trim()
24
+ : "";
25
+ if (fromEnv) {
26
+ return path.resolve(fromEnv);
27
+ }
28
+ return path.join(os.homedir(), ".codex");
29
+ }
30
+
31
+ function shouldSyncCodex() {
32
+ const fromEnv = process.env.CODEX_HOME
33
+ ? String(process.env.CODEX_HOME).trim()
34
+ : "";
35
+ if (fromEnv) {
36
+ return true;
37
+ }
38
+ return exists(path.join(os.homedir(), ".codex"));
39
+ }
40
+
14
41
  async function main() {
15
42
  if (isTruthy(process.env.QUICKTVUI_AI_SKIP_POSTINSTALL)) {
16
43
  return;
@@ -19,16 +46,51 @@ async function main() {
19
46
  return;
20
47
  }
21
48
 
49
+ const syncCodex = shouldSyncCodex();
50
+ const codexSkillsDir = path.join(resolveCodexHome(), "skills", "quicktvui");
51
+ const failures = [];
52
+
22
53
  try {
23
- await runCli(["update"]);
54
+ if (syncCodex) {
55
+ await runCli(["update", "--skip-gemini-config"]);
56
+ } else {
57
+ await runCli(["update"]);
58
+ }
59
+ } catch (error) {
60
+ failures.push(`agents: ${error.message}`);
61
+ }
62
+
63
+ if (syncCodex) {
64
+ try {
65
+ await runCli(["update", "--dir", codexSkillsDir]);
66
+ } catch (error) {
67
+ failures.push(`codex: ${error.message}`);
68
+ }
69
+ }
70
+
71
+ if (failures.length === 0) {
72
+ if (syncCodex) {
73
+ console.log(
74
+ `[quicktvui-ai] postinstall: synced latest skill files into ~/.agents/skills/quicktvui and ${codexSkillsDir}.`,
75
+ );
76
+ return;
77
+ }
24
78
  console.log(
25
79
  "[quicktvui-ai] postinstall: synced latest skill files into ~/.agents/skills/quicktvui.",
26
80
  );
27
- } catch (error) {
81
+ return;
82
+ }
83
+
84
+ if (syncCodex) {
28
85
  console.warn(
29
- `[quicktvui-ai] postinstall: unable to auto-sync skill files (${error.message}). Run 'quicktvui-ai update' manually.`,
86
+ `[quicktvui-ai] postinstall: unable to fully auto-sync skill files (${failures.join("; ")}). Run 'quicktvui-ai update' and 'quicktvui-ai update --dir ${codexSkillsDir}' manually.`,
30
87
  );
88
+ return;
31
89
  }
90
+
91
+ console.warn(
92
+ `[quicktvui-ai] postinstall: unable to auto-sync skill files (${failures.join("; ")}). Run 'quicktvui-ai update' manually.`,
93
+ );
32
94
  }
33
95
 
34
96
  main();