@quicktvui/ai-cli 0.1.0 → 1.1.1
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 +137 -0
- package/bin/quicktvui-ai-create-project.js +8 -0
- package/bin/quicktvui-aicreate-project.js +8 -0
- package/docs/esapp-protocol.md +134 -0
- package/lib/index.js +2556 -16
- package/package.json +7 -2
- package/scripts/install +45 -0
- package/scripts/install.ps1 +29 -0
- package/templates/quicktvui-template/.eslintrc.cjs +15 -0
- package/templates/quicktvui-template/.husky/pre-commit +4 -0
- package/templates/quicktvui-template/.prettierrc.json +8 -0
- package/templates/quicktvui-template/.vscode/extensions.json +7 -0
- package/templates/quicktvui-template/LICENSE +201 -0
- package/templates/quicktvui-template/README.md +36 -0
- package/templates/quicktvui-template/package-lock.json +12108 -0
- package/templates/quicktvui-template/package.json +29 -0
- package/templates/quicktvui-template/src/App.vue +30 -0
- package/templates/quicktvui-template/src/assets/logo.png +0 -0
- package/templates/quicktvui-template/src/components/WebRTCPlayer.ts +48 -0
- package/templates/quicktvui-template/src/main.ts +29 -0
- package/templates/quicktvui-template/src/routes.ts +21 -0
- package/templates/quicktvui-template/src/views/cast.vue +42 -0
- package/templates/quicktvui-template/src/views/error.vue +32 -0
- package/templates/quicktvui-template/src/views/home.vue +60 -0
- package/templates/quicktvui-template/tsconfig.json +19 -0
- package/templates/quicktvui-template/yarn.lock +6077 -0
package/lib/index.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const os = require("os");
|
|
3
3
|
const path = require("path");
|
|
4
|
+
const readline = require("node:readline/promises");
|
|
5
|
+
const http = require("http");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const net = require("net");
|
|
8
|
+
const { spawnSync, spawn } = require("child_process");
|
|
4
9
|
|
|
5
|
-
const PACKAGE_VERSION = "0.1.
|
|
10
|
+
const PACKAGE_VERSION = "0.1.4";
|
|
6
11
|
const DEFAULT_INSTALL_DIR = path.join(
|
|
7
12
|
os.homedir(),
|
|
8
13
|
".agents",
|
|
9
14
|
"skills",
|
|
10
15
|
"quicktvui",
|
|
11
16
|
);
|
|
17
|
+
const TEMPLATE_REPO_URL = "https://github.com/quicktvui/quicktvui-template.git";
|
|
18
|
+
const DEFAULT_QUICKTVUI_AI_VERSION = "^1.1.0";
|
|
19
|
+
const DEFAULT_ANDROID_AVD_NAME = "quicktvui_tv";
|
|
20
|
+
const DEFAULT_ANDROID_API_LEVEL = "android-35";
|
|
21
|
+
const DEFAULT_NODE_LTS_MAJOR = 20;
|
|
22
|
+
const DEFAULT_DEV_SERVER_PORT = 38989;
|
|
23
|
+
const RUNTIME_PACKAGE_NAME = "com.extscreen.runtime";
|
|
24
|
+
const RUNTIME_LAUNCH_ACTIVITY =
|
|
25
|
+
"com.extscreen.runtime/com.extscreen.runtime.LauncherAlias";
|
|
26
|
+
const RUNTIME_DEBUG_BROADCAST_ACTION =
|
|
27
|
+
"com.extscreen.runtime.ACTION_CHANGE_DEBUG_SERVER";
|
|
28
|
+
const RUNTIME_REPOSITORY_ROOT =
|
|
29
|
+
"http://hub.quicktvui.com/repository/maven-files/apk/runtime/dev";
|
|
30
|
+
const RUNTIME_REPOSITORY_METADATA_URL = `${RUNTIME_REPOSITORY_ROOT}/maven-metadata.xml`;
|
|
12
31
|
|
|
13
32
|
const REQUIRED_SKILL_FILES = [
|
|
14
33
|
"SKILL.md",
|
|
@@ -17,27 +36,2184 @@ const REQUIRED_SKILL_FILES = [
|
|
|
17
36
|
path.join("references", "lookup-checklist.md"),
|
|
18
37
|
path.join("references", "bug-report-template.md"),
|
|
19
38
|
];
|
|
39
|
+
const QUICKTVUI_GEMINI_BRIDGE_START = "<!-- QUICKTVUI_SKILL_BRIDGE_START -->";
|
|
40
|
+
const QUICKTVUI_GEMINI_BRIDGE_END = "<!-- QUICKTVUI_SKILL_BRIDGE_END -->";
|
|
41
|
+
const DEFAULT_GEMINI_CONTEXT_FILENAMES = [
|
|
42
|
+
"GEMINI.md",
|
|
43
|
+
"AGENTS.md",
|
|
44
|
+
"SKILL.md",
|
|
45
|
+
"CONTEXT.md",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function exists(filePath) {
|
|
49
|
+
return fs.existsSync(filePath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureDir(dirPath) {
|
|
53
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function copyDirectoryRecursive(src, dest) {
|
|
57
|
+
ensureDir(dest);
|
|
58
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const srcPath = path.join(src, entry.name);
|
|
61
|
+
const destPath = path.join(dest, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
copyDirectoryRecursive(srcPath, destPath);
|
|
64
|
+
} else {
|
|
65
|
+
fs.copyFileSync(srcPath, destPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeDirectoryIfExists(dirPath) {
|
|
71
|
+
if (exists(dirPath)) {
|
|
72
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function backupFile(filePath) {
|
|
77
|
+
if (!exists(filePath)) return null;
|
|
78
|
+
const backupPath = `${filePath}.bak`;
|
|
79
|
+
try {
|
|
80
|
+
fs.copyFileSync(filePath, backupPath);
|
|
81
|
+
return backupPath;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function escapeRegExp(value) {
|
|
88
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function needsWindowsShell(command) {
|
|
92
|
+
return process.platform === "win32" && /\.(bat|cmd)$/i.test(String(command));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function withSpawnOptions(command, options) {
|
|
96
|
+
if (needsWindowsShell(command)) {
|
|
97
|
+
return { shell: true, ...options };
|
|
98
|
+
}
|
|
99
|
+
return options;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runCommand(command, args, options = {}) {
|
|
103
|
+
const result = spawnSync(
|
|
104
|
+
command,
|
|
105
|
+
args,
|
|
106
|
+
withSpawnOptions(command, {
|
|
107
|
+
stdio: "inherit",
|
|
108
|
+
...options,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
if (result.error) {
|
|
112
|
+
throw new Error(`Failed to run '${command}': ${result.error.message}`);
|
|
113
|
+
}
|
|
114
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
115
|
+
throw new Error(`Command failed: ${command} ${args.join(" ")}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function runCommandCapture(command, args, options = {}) {
|
|
120
|
+
const result = spawnSync(
|
|
121
|
+
command,
|
|
122
|
+
args,
|
|
123
|
+
withSpawnOptions(command, {
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
126
|
+
...options,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (result.error) {
|
|
131
|
+
throw new Error(`Failed to run '${command}': ${result.error.message}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
135
|
+
const stderr = (result.stderr || "").trim();
|
|
136
|
+
const stdout = (result.stdout || "").trim();
|
|
137
|
+
const reason = stderr || stdout || "unknown error";
|
|
138
|
+
throw new Error(`Command failed: ${command} ${args.join(" ")}\n${reason}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
stdout: result.stdout || "",
|
|
143
|
+
stderr: result.stderr || "",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function runCommandDetached(command, args, options = {}) {
|
|
148
|
+
let child;
|
|
149
|
+
try {
|
|
150
|
+
child = spawn(
|
|
151
|
+
command,
|
|
152
|
+
args,
|
|
153
|
+
withSpawnOptions(command, {
|
|
154
|
+
detached: true,
|
|
155
|
+
stdio: "ignore",
|
|
156
|
+
...options,
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw new Error(`Failed to run '${command}': ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
child.unref();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function commandExists(command) {
|
|
166
|
+
const result = spawnSync(command, ["--version"], { stdio: "ignore" });
|
|
167
|
+
return !result.error && result.status === 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function commandCanRun(command, args) {
|
|
171
|
+
const result = spawnSync(
|
|
172
|
+
command,
|
|
173
|
+
args,
|
|
174
|
+
withSpawnOptions(command, { stdio: "ignore" }),
|
|
175
|
+
);
|
|
176
|
+
return !result.error && result.status === 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readJsonFile(filePath) {
|
|
180
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function writeJsonFile(filePath, value) {
|
|
184
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveBundledTemplateSource() {
|
|
188
|
+
const packageTemplate = path.resolve(
|
|
189
|
+
__dirname,
|
|
190
|
+
"..",
|
|
191
|
+
"templates",
|
|
192
|
+
"quicktvui-template",
|
|
193
|
+
);
|
|
194
|
+
if (exists(packageTemplate)) return packageTemplate;
|
|
195
|
+
|
|
196
|
+
const monorepoFallback = path.resolve(
|
|
197
|
+
__dirname,
|
|
198
|
+
"..",
|
|
199
|
+
"..",
|
|
200
|
+
"..",
|
|
201
|
+
"packages",
|
|
202
|
+
"quicktvui-ai-cli",
|
|
203
|
+
"templates",
|
|
204
|
+
"quicktvui-template",
|
|
205
|
+
);
|
|
206
|
+
if (exists(monorepoFallback)) return monorepoFallback;
|
|
207
|
+
|
|
208
|
+
throw new Error(
|
|
209
|
+
"Bundled quicktvui-template snapshot is missing from @quicktvui/ai-cli.",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function ensureProjectPackageRules(projectDir, projectName) {
|
|
214
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
215
|
+
if (!exists(packageJsonPath)) {
|
|
216
|
+
throw new Error(`Missing package.json in created project: ${projectDir}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
220
|
+
packageJson.name = projectName;
|
|
221
|
+
packageJson.version = "1.0.0";
|
|
222
|
+
|
|
223
|
+
if (!packageJson.devDependencies) {
|
|
224
|
+
packageJson.devDependencies = {};
|
|
225
|
+
}
|
|
226
|
+
if (!packageJson.devDependencies["@quicktvui/ai"]) {
|
|
227
|
+
packageJson.devDependencies["@quicktvui/ai"] = DEFAULT_QUICKTVUI_AI_VERSION;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
writeJsonFile(packageJsonPath, packageJson);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function installProjectDependencies(projectDir, args) {
|
|
234
|
+
if (args["skip-install"]) {
|
|
235
|
+
console.log("Skip dependency installation due to --skip-install.");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (commandExists("yarn")) {
|
|
240
|
+
runCommand("yarn", ["install"], { cwd: projectDir });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (commandExists("npm")) {
|
|
245
|
+
runCommand("npm", ["install"], { cwd: projectDir });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(
|
|
250
|
+
"Warning: neither yarn nor npm found. Install dependencies manually in project directory.",
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isDirectoryEmpty(dirPath) {
|
|
255
|
+
if (!exists(dirPath)) return true;
|
|
256
|
+
const entries = fs.readdirSync(dirPath);
|
|
257
|
+
return entries.length === 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function delay(ms) {
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
setTimeout(resolve, ms);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function pickPrimaryDeviceSerial(deviceState) {
|
|
267
|
+
if (!deviceState || !Array.isArray(deviceState.devices)) return null;
|
|
268
|
+
if (deviceState.devices.length === 0) return null;
|
|
269
|
+
const emulator = deviceState.devices.find((serial) =>
|
|
270
|
+
serial.startsWith("emulator-"),
|
|
271
|
+
);
|
|
272
|
+
return emulator || deviceState.devices[0];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function getLocalIPv4Address() {
|
|
276
|
+
const interfaces = os.networkInterfaces();
|
|
277
|
+
for (const name of Object.keys(interfaces)) {
|
|
278
|
+
const records = interfaces[name] || [];
|
|
279
|
+
for (const item of records) {
|
|
280
|
+
if (
|
|
281
|
+
item &&
|
|
282
|
+
item.family === "IPv4" &&
|
|
283
|
+
item.address !== "127.0.0.1" &&
|
|
284
|
+
!item.internal
|
|
285
|
+
) {
|
|
286
|
+
return item.address;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function parseVersionSegments(version) {
|
|
294
|
+
return String(version)
|
|
295
|
+
.trim()
|
|
296
|
+
.split(".")
|
|
297
|
+
.map((segment) => {
|
|
298
|
+
const numberValue = Number(segment);
|
|
299
|
+
if (Number.isFinite(numberValue)) return numberValue;
|
|
300
|
+
return segment;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function compareVersionDesc(a, b) {
|
|
305
|
+
const aSeg = parseVersionSegments(a);
|
|
306
|
+
const bSeg = parseVersionSegments(b);
|
|
307
|
+
const max = Math.max(aSeg.length, bSeg.length);
|
|
308
|
+
for (let i = 0; i < max; i += 1) {
|
|
309
|
+
const left = typeof aSeg[i] === "undefined" ? 0 : aSeg[i];
|
|
310
|
+
const right = typeof bSeg[i] === "undefined" ? 0 : bSeg[i];
|
|
311
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
312
|
+
if (left > right) return -1;
|
|
313
|
+
if (left < right) return 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const cmp = String(right).localeCompare(String(left), undefined, {
|
|
317
|
+
numeric: true,
|
|
318
|
+
sensitivity: "base",
|
|
319
|
+
});
|
|
320
|
+
if (cmp !== 0) return cmp;
|
|
321
|
+
}
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function fetchWithRedirect(url, redirectCount = 0, timeoutMs = 20000) {
|
|
326
|
+
return new Promise((resolve, reject) => {
|
|
327
|
+
if (redirectCount > 8) {
|
|
328
|
+
reject(new Error(`Too many redirects: ${url}`));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const client = url.startsWith("https://") ? https : http;
|
|
332
|
+
const req = client.get(url, (res) => {
|
|
333
|
+
const status = res.statusCode || 0;
|
|
334
|
+
if ([301, 302, 303, 307, 308].includes(status) && res.headers.location) {
|
|
335
|
+
const nextUrl = new URL(res.headers.location, url).toString();
|
|
336
|
+
res.resume();
|
|
337
|
+
resolve(fetchWithRedirect(nextUrl, redirectCount + 1, timeoutMs));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (status >= 400) {
|
|
341
|
+
const errorChunks = [];
|
|
342
|
+
res.on("data", (chunk) => errorChunks.push(chunk));
|
|
343
|
+
res.on("end", () => {
|
|
344
|
+
const detail = Buffer.concat(errorChunks).toString("utf8").trim();
|
|
345
|
+
reject(
|
|
346
|
+
new Error(
|
|
347
|
+
`Request failed (${status}) ${url}${detail ? `: ${detail}` : ""}`,
|
|
348
|
+
),
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
resolve(res);
|
|
354
|
+
});
|
|
355
|
+
req.setTimeout(timeoutMs, () => {
|
|
356
|
+
req.destroy(new Error(`Request timeout after ${timeoutMs}ms: ${url}`));
|
|
357
|
+
});
|
|
358
|
+
req.on("error", reject);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function fetchText(url, timeoutMs = 20000) {
|
|
363
|
+
const response = await fetchWithRedirect(url, 0, timeoutMs);
|
|
364
|
+
return await new Promise((resolve, reject) => {
|
|
365
|
+
const chunks = [];
|
|
366
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
367
|
+
response.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
368
|
+
response.on("error", reject);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function downloadToFile(url, filePath, timeoutMs = 60000) {
|
|
373
|
+
const response = await fetchWithRedirect(url, 0, timeoutMs);
|
|
374
|
+
await new Promise((resolve, reject) => {
|
|
375
|
+
const writer = fs.createWriteStream(filePath);
|
|
376
|
+
response.pipe(writer);
|
|
377
|
+
response.on("error", reject);
|
|
378
|
+
writer.on("error", reject);
|
|
379
|
+
writer.on("finish", resolve);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function parseRuntimeVersionList(metadataText) {
|
|
384
|
+
const matches = metadataText.match(/<version>[^<]+<\/version>/g) || [];
|
|
385
|
+
const versions = matches
|
|
386
|
+
.map((entry) =>
|
|
387
|
+
entry.replace("<version>", "").replace("</version>", "").trim(),
|
|
388
|
+
)
|
|
389
|
+
.filter(Boolean);
|
|
390
|
+
return Array.from(new Set(versions)).sort(compareVersionDesc);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function fetchRuntimeVersions() {
|
|
394
|
+
const metadata = await fetchText(RUNTIME_REPOSITORY_METADATA_URL);
|
|
395
|
+
return parseRuntimeVersionList(metadata);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function buildRuntimeApkUrl(version) {
|
|
399
|
+
return `${RUNTIME_REPOSITORY_ROOT}/${version}/dev-${version}.apk`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function waitForPort(host, port, timeoutMs) {
|
|
403
|
+
return new Promise((resolve, reject) => {
|
|
404
|
+
const startedAt = Date.now();
|
|
405
|
+
|
|
406
|
+
const tryConnect = () => {
|
|
407
|
+
const socket = new net.Socket();
|
|
408
|
+
let settled = false;
|
|
409
|
+
socket.setTimeout(2000);
|
|
410
|
+
|
|
411
|
+
socket.on("connect", () => {
|
|
412
|
+
if (settled) return;
|
|
413
|
+
settled = true;
|
|
414
|
+
socket.destroy();
|
|
415
|
+
resolve(true);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
socket.on("timeout", () => {
|
|
419
|
+
socket.destroy();
|
|
420
|
+
});
|
|
421
|
+
socket.on("error", () => {
|
|
422
|
+
socket.destroy();
|
|
423
|
+
});
|
|
424
|
+
socket.on("close", () => {
|
|
425
|
+
if (settled) return;
|
|
426
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
427
|
+
settled = true;
|
|
428
|
+
reject(new Error(`Timed out waiting for ${host}:${port}`));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
setTimeout(tryConnect, 1000);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
socket.connect(port, host);
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
tryConnect();
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function resolveNodeMajorVersion() {
|
|
442
|
+
const raw = process.versions && process.versions.node;
|
|
443
|
+
if (!raw) return null;
|
|
444
|
+
const major = Number(String(raw).split(".")[0]);
|
|
445
|
+
if (!Number.isFinite(major)) return null;
|
|
446
|
+
return major;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function commandExistsViaShell(shellCmd) {
|
|
450
|
+
const result = spawnSync(shellCmd, [], { stdio: "ignore", shell: true });
|
|
451
|
+
return !result.error && result.status === 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function formatBytes(bytes) {
|
|
455
|
+
const value = Number(bytes);
|
|
456
|
+
if (!Number.isFinite(value) || value <= 0) return "unknown";
|
|
457
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
458
|
+
let current = value;
|
|
459
|
+
let index = 0;
|
|
460
|
+
while (current >= 1024 && index < units.length - 1) {
|
|
461
|
+
current /= 1024;
|
|
462
|
+
index += 1;
|
|
463
|
+
}
|
|
464
|
+
return `${current.toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isInteractivePromptEnabled(args) {
|
|
468
|
+
const autoYes = toBooleanFlag(args.yes, false);
|
|
469
|
+
const noInteractive = toBooleanFlag(args["no-interactive"], false);
|
|
470
|
+
if (autoYes || noInteractive) return false;
|
|
471
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function askYesNo(question, defaultValue, args) {
|
|
475
|
+
if (!isInteractivePromptEnabled(args)) {
|
|
476
|
+
return defaultValue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const rl = readline.createInterface({
|
|
480
|
+
input: process.stdin,
|
|
481
|
+
output: process.stdout,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
486
|
+
const answer = (await rl.question(`${question} ${suffix} `))
|
|
487
|
+
.trim()
|
|
488
|
+
.toLowerCase();
|
|
489
|
+
if (!answer) return defaultValue;
|
|
490
|
+
if (answer === "y" || answer === "yes") return true;
|
|
491
|
+
if (answer === "n" || answer === "no") return false;
|
|
492
|
+
return defaultValue;
|
|
493
|
+
} finally {
|
|
494
|
+
rl.close();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function askText(question, defaultValue, args) {
|
|
499
|
+
if (!isInteractivePromptEnabled(args)) {
|
|
500
|
+
return defaultValue || "";
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const rl = readline.createInterface({
|
|
504
|
+
input: process.stdin,
|
|
505
|
+
output: process.stdout,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
const suffix = defaultValue ? ` [default: ${defaultValue}]` : "";
|
|
510
|
+
const answer = (await rl.question(`${question}${suffix} `)).trim();
|
|
511
|
+
return answer || defaultValue || "";
|
|
512
|
+
} finally {
|
|
513
|
+
rl.close();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function normalizeDeviceEndpoint(input) {
|
|
518
|
+
const raw = String(input || "").trim();
|
|
519
|
+
if (!raw) return "";
|
|
520
|
+
if (raw.includes(":")) return raw;
|
|
521
|
+
return `${raw}:5555`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function tryAdbConnect(adbPath, endpoint) {
|
|
525
|
+
const result = spawnSync(adbPath, ["connect", endpoint], {
|
|
526
|
+
encoding: "utf8",
|
|
527
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
528
|
+
});
|
|
529
|
+
if (result.error) {
|
|
530
|
+
return {
|
|
531
|
+
connected: false,
|
|
532
|
+
output: result.error.message,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const stdout = (result.stdout || "").trim();
|
|
536
|
+
const stderr = (result.stderr || "").trim();
|
|
537
|
+
const output = [stdout, stderr].filter(Boolean).join("\n");
|
|
538
|
+
const connected = /connected to|already connected to/i.test(output);
|
|
539
|
+
return { connected, output };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function connectRealDeviceByIp(adbPath, args) {
|
|
543
|
+
let defaultInput =
|
|
544
|
+
typeof args["device-ip"] === "string" ? args["device-ip"].trim() : "";
|
|
545
|
+
if (!isInteractivePromptEnabled(args) && !defaultInput) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
let attempt = 0;
|
|
549
|
+
while (attempt < 3) {
|
|
550
|
+
attempt += 1;
|
|
551
|
+
const rawInput = await askText(
|
|
552
|
+
"Enter Android device IP (example: 192.168.1.100 or 192.168.1.100:5555):",
|
|
553
|
+
defaultInput,
|
|
554
|
+
args,
|
|
555
|
+
);
|
|
556
|
+
const endpoint = normalizeDeviceEndpoint(rawInput);
|
|
557
|
+
if (!endpoint) {
|
|
558
|
+
const retryEmpty = await askYesNo(
|
|
559
|
+
"Device IP is empty. Do you want to retry input?",
|
|
560
|
+
attempt < 3,
|
|
561
|
+
args,
|
|
562
|
+
);
|
|
563
|
+
if (!retryEmpty) return null;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
defaultInput = endpoint;
|
|
568
|
+
console.log(`Trying to connect device: ${endpoint}`);
|
|
569
|
+
const connectResult = tryAdbConnect(adbPath, endpoint);
|
|
570
|
+
if (connectResult.connected) {
|
|
571
|
+
console.log(`Device connected: ${endpoint}`);
|
|
572
|
+
return endpoint;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
console.log(
|
|
576
|
+
`Connect failed: ${connectResult.output || "unknown adb connect error"}`,
|
|
577
|
+
);
|
|
578
|
+
const retry = await askYesNo(
|
|
579
|
+
"Failed to connect by IP. Try another IP?",
|
|
580
|
+
attempt < 3,
|
|
581
|
+
args,
|
|
582
|
+
);
|
|
583
|
+
if (!retry) return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function parseRemotePackageSizeMap(xmlText) {
|
|
590
|
+
const sizeMap = new Map();
|
|
591
|
+
const packageRegex =
|
|
592
|
+
/<remotePackage\s+path="([^"]+)">([\s\S]*?)<\/remotePackage>/g;
|
|
593
|
+
let packageMatch = packageRegex.exec(xmlText);
|
|
594
|
+
while (packageMatch) {
|
|
595
|
+
const pkgPath = packageMatch[1];
|
|
596
|
+
const pkgBody = packageMatch[2];
|
|
597
|
+
const sizeMatches = pkgBody.match(/<size>(\d+)<\/size>/g) || [];
|
|
598
|
+
let maxSize = 0;
|
|
599
|
+
for (const raw of sizeMatches) {
|
|
600
|
+
const num = Number(raw.replace("<size>", "").replace("</size>", ""));
|
|
601
|
+
if (Number.isFinite(num) && num > maxSize) {
|
|
602
|
+
maxSize = num;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (maxSize > 0) {
|
|
606
|
+
sizeMap.set(pkgPath, maxSize);
|
|
607
|
+
}
|
|
608
|
+
packageMatch = packageRegex.exec(xmlText);
|
|
609
|
+
}
|
|
610
|
+
return sizeMap;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function canReachGoogleAndroidRepo() {
|
|
614
|
+
try {
|
|
615
|
+
const res = await fetchWithRedirect(
|
|
616
|
+
"https://dl.google.com/android/repository/repository2-1.xml",
|
|
617
|
+
0,
|
|
618
|
+
8000,
|
|
619
|
+
);
|
|
620
|
+
res.resume();
|
|
621
|
+
return true;
|
|
622
|
+
} catch (error) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function fetchAndroidPackageSizeMap() {
|
|
628
|
+
const xml = await fetchText(
|
|
629
|
+
"https://dl.google.com/android/repository/repository2-1.xml",
|
|
630
|
+
);
|
|
631
|
+
return parseRemotePackageSizeMap(xml);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function printEmulatorDownloadEstimate(sizeMap, systemImageCandidates) {
|
|
635
|
+
const basePackages = ["platform-tools", "emulator"];
|
|
636
|
+
const packageLines = [];
|
|
637
|
+
let total = 0;
|
|
638
|
+
|
|
639
|
+
for (const pkg of basePackages) {
|
|
640
|
+
const size = sizeMap.get(pkg);
|
|
641
|
+
if (size) {
|
|
642
|
+
total += size;
|
|
643
|
+
packageLines.push(`- ${pkg}: ${formatBytes(size)}`);
|
|
644
|
+
} else {
|
|
645
|
+
packageLines.push(`- ${pkg}: unknown`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const matchedSystemImage = systemImageCandidates.find((candidate) =>
|
|
650
|
+
sizeMap.has(candidate),
|
|
651
|
+
);
|
|
652
|
+
if (matchedSystemImage) {
|
|
653
|
+
const size = sizeMap.get(matchedSystemImage);
|
|
654
|
+
total += size;
|
|
655
|
+
packageLines.push(`- ${matchedSystemImage}: ${formatBytes(size)}`);
|
|
656
|
+
} else {
|
|
657
|
+
packageLines.push(`- system-image: unknown (candidate not found in index)`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
console.log("Estimated Android download size:");
|
|
661
|
+
for (const line of packageLines) {
|
|
662
|
+
console.log(line);
|
|
663
|
+
}
|
|
664
|
+
if (total > 0) {
|
|
665
|
+
console.log(`- total (known): ${formatBytes(total)}`);
|
|
666
|
+
}
|
|
667
|
+
console.log("Download progress will be shown by sdkmanager below.");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function printSdkPackageDownloadEstimate(sizeMap, packageNames, title) {
|
|
671
|
+
const packageLines = [];
|
|
672
|
+
let total = 0;
|
|
673
|
+
for (const packageName of packageNames) {
|
|
674
|
+
const size = sizeMap.get(packageName);
|
|
675
|
+
if (size) {
|
|
676
|
+
total += size;
|
|
677
|
+
packageLines.push(`- ${packageName}: ${formatBytes(size)}`);
|
|
678
|
+
} else {
|
|
679
|
+
packageLines.push(`- ${packageName}: unknown`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
console.log(title);
|
|
683
|
+
for (const line of packageLines) {
|
|
684
|
+
console.log(line);
|
|
685
|
+
}
|
|
686
|
+
if (total > 0) {
|
|
687
|
+
console.log(`- total (known): ${formatBytes(total)}`);
|
|
688
|
+
}
|
|
689
|
+
console.log("Download progress will be shown by sdkmanager below.");
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function getDefaultAndroidSdkRoots() {
|
|
693
|
+
const home = os.homedir();
|
|
694
|
+
if (process.platform === "darwin") {
|
|
695
|
+
return [path.join(home, "Library", "Android", "sdk")];
|
|
696
|
+
}
|
|
697
|
+
if (process.platform === "win32") {
|
|
698
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
699
|
+
const roots = [];
|
|
700
|
+
if (localAppData) {
|
|
701
|
+
roots.push(path.join(localAppData, "Android", "sdk"));
|
|
702
|
+
}
|
|
703
|
+
roots.push(path.join(home, "AppData", "Local", "Android", "sdk"));
|
|
704
|
+
return roots;
|
|
705
|
+
}
|
|
706
|
+
return [path.join(home, "Android", "Sdk"), path.join(home, "Android", "sdk")];
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function getPreferredAndroidSdkRootPath() {
|
|
710
|
+
const fromEnv =
|
|
711
|
+
process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME || "";
|
|
712
|
+
if (fromEnv) {
|
|
713
|
+
return path.resolve(fromEnv);
|
|
714
|
+
}
|
|
715
|
+
const defaults = getDefaultAndroidSdkRoots();
|
|
716
|
+
if (defaults.length > 0) {
|
|
717
|
+
return path.resolve(defaults[0]);
|
|
718
|
+
}
|
|
719
|
+
return path.resolve(path.join(os.homedir(), "Android", "Sdk"));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function resolveAndroidSdkRoot() {
|
|
723
|
+
const fromEnv =
|
|
724
|
+
process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME || "";
|
|
725
|
+
if (fromEnv && exists(fromEnv)) {
|
|
726
|
+
return {
|
|
727
|
+
sdkRoot: path.resolve(fromEnv),
|
|
728
|
+
source: "environment",
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
for (const sdkPath of getDefaultAndroidSdkRoots()) {
|
|
733
|
+
if (exists(sdkPath)) {
|
|
734
|
+
return {
|
|
735
|
+
sdkRoot: path.resolve(sdkPath),
|
|
736
|
+
source: "default-path",
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
sdkRoot: null,
|
|
743
|
+
source: "missing",
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function resolveAndroidSdkRootForSetup() {
|
|
748
|
+
const resolved = resolveAndroidSdkRoot();
|
|
749
|
+
if (resolved.sdkRoot) {
|
|
750
|
+
return resolved;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const autoPath = getPreferredAndroidSdkRootPath();
|
|
754
|
+
ensureDir(autoPath);
|
|
755
|
+
return {
|
|
756
|
+
sdkRoot: path.resolve(autoPath),
|
|
757
|
+
source: "auto-created",
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function binaryExists(binaryPath, checkArgs = ["--version"]) {
|
|
762
|
+
if (!binaryPath) return false;
|
|
763
|
+
if (!exists(binaryPath)) return false;
|
|
764
|
+
const result = spawnSync(
|
|
765
|
+
binaryPath,
|
|
766
|
+
checkArgs,
|
|
767
|
+
withSpawnOptions(binaryPath, { stdio: "ignore" }),
|
|
768
|
+
);
|
|
769
|
+
return !result.error && result.status === 0;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function resolveAndroidTool(toolName, sdkRoot) {
|
|
773
|
+
const toolNameVariants = [];
|
|
774
|
+
if (process.platform === "win32") {
|
|
775
|
+
if (toolName === "sdkmanager" || toolName === "avdmanager") {
|
|
776
|
+
toolNameVariants.push(`${toolName}.bat`, `${toolName}.exe`);
|
|
777
|
+
} else {
|
|
778
|
+
toolNameVariants.push(`${toolName}.exe`);
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
toolNameVariants.push(toolName);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (toolName === "adb" && commandCanRun("adb", ["version"])) {
|
|
785
|
+
return "adb";
|
|
786
|
+
}
|
|
787
|
+
if (toolName === "emulator" && commandCanRun("emulator", ["-version"])) {
|
|
788
|
+
return "emulator";
|
|
789
|
+
}
|
|
790
|
+
if (toolName === "avdmanager" && commandCanRun("avdmanager", ["--help"])) {
|
|
791
|
+
return "avdmanager";
|
|
792
|
+
}
|
|
793
|
+
if (toolName === "sdkmanager" && commandCanRun("sdkmanager", ["--help"])) {
|
|
794
|
+
return "sdkmanager";
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (!sdkRoot) return null;
|
|
798
|
+
|
|
799
|
+
const candidates = [];
|
|
800
|
+
if (toolName === "adb") {
|
|
801
|
+
for (const name of toolNameVariants) {
|
|
802
|
+
candidates.push(path.join(sdkRoot, "platform-tools", name));
|
|
803
|
+
}
|
|
804
|
+
} else if (toolName === "emulator") {
|
|
805
|
+
for (const name of toolNameVariants) {
|
|
806
|
+
candidates.push(path.join(sdkRoot, "emulator", name));
|
|
807
|
+
}
|
|
808
|
+
} else if (toolName === "sdkmanager" || toolName === "avdmanager") {
|
|
809
|
+
for (const name of toolNameVariants) {
|
|
810
|
+
candidates.push(
|
|
811
|
+
path.join(sdkRoot, "cmdline-tools", "latest", "bin", name),
|
|
812
|
+
);
|
|
813
|
+
candidates.push(path.join(sdkRoot, "tools", "bin", name));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
for (const candidate of candidates) {
|
|
818
|
+
let checkArgs = ["--help"];
|
|
819
|
+
if (toolName === "adb") checkArgs = ["version"];
|
|
820
|
+
if (toolName === "emulator") checkArgs = ["-version"];
|
|
821
|
+
if (binaryExists(candidate, checkArgs)) {
|
|
822
|
+
return candidate;
|
|
823
|
+
}
|
|
824
|
+
if (
|
|
825
|
+
(toolName === "sdkmanager" || toolName === "avdmanager") &&
|
|
826
|
+
exists(candidate)
|
|
827
|
+
) {
|
|
828
|
+
return candidate;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function getAndroidRepoHostOsTag() {
|
|
836
|
+
if (process.platform === "darwin") return "macosx";
|
|
837
|
+
if (process.platform === "win32") return "windows";
|
|
838
|
+
if (process.platform === "linux") return "linux";
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function parseCommandLineToolsPathRank(packagePath) {
|
|
843
|
+
const token = String(packagePath || "").split(";")[1] || "";
|
|
844
|
+
if (token === "latest") return 1_000_000;
|
|
845
|
+
const num = Number(token.replace(/[^\d.]/g, ""));
|
|
846
|
+
if (Number.isFinite(num)) return num;
|
|
847
|
+
return 0;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function parseAndroidCommandLineToolsArchiveInfo(xmlText) {
|
|
851
|
+
const hostOs = getAndroidRepoHostOsTag();
|
|
852
|
+
if (!hostOs) {
|
|
853
|
+
throw new Error(
|
|
854
|
+
`Unsupported platform for Android Command-line Tools: ${process.platform}`,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const packageRegex =
|
|
859
|
+
/<remotePackage\s+path="(cmdline-tools;[^"]+)">([\s\S]*?)<\/remotePackage>/g;
|
|
860
|
+
const packageEntries = [];
|
|
861
|
+
let packageMatch = packageRegex.exec(xmlText);
|
|
862
|
+
while (packageMatch) {
|
|
863
|
+
const packagePath = packageMatch[1];
|
|
864
|
+
const body = packageMatch[2];
|
|
865
|
+
packageEntries.push({
|
|
866
|
+
packagePath,
|
|
867
|
+
body,
|
|
868
|
+
rank: parseCommandLineToolsPathRank(packagePath),
|
|
869
|
+
});
|
|
870
|
+
packageMatch = packageRegex.exec(xmlText);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (packageEntries.length === 0) {
|
|
874
|
+
throw new Error("Unable to find Android cmdline-tools package metadata.");
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
packageEntries.sort((a, b) => b.rank - a.rank);
|
|
878
|
+
for (const entry of packageEntries) {
|
|
879
|
+
const archiveRegex = /<archive>([\s\S]*?)<\/archive>/g;
|
|
880
|
+
const archives = [];
|
|
881
|
+
let archiveMatch = archiveRegex.exec(entry.body);
|
|
882
|
+
while (archiveMatch) {
|
|
883
|
+
const archiveBody = archiveMatch[1];
|
|
884
|
+
const completeMatch = archiveBody.match(
|
|
885
|
+
/<complete>([\s\S]*?)<\/complete>/,
|
|
886
|
+
);
|
|
887
|
+
if (completeMatch) {
|
|
888
|
+
const completeBody = completeMatch[1];
|
|
889
|
+
const urlMatch = completeBody.match(/<url>([^<]+)<\/url>/);
|
|
890
|
+
const sizeMatch = completeBody.match(/<size>(\d+)<\/size>/);
|
|
891
|
+
const hostOsMatch = archiveBody.match(/<host-os>([^<]+)<\/host-os>/);
|
|
892
|
+
if (urlMatch) {
|
|
893
|
+
archives.push({
|
|
894
|
+
hostOs: hostOsMatch ? hostOsMatch[1] : "",
|
|
895
|
+
url: urlMatch[1],
|
|
896
|
+
sizeBytes: sizeMatch ? Number(sizeMatch[1]) : 0,
|
|
897
|
+
packagePath: entry.packagePath,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
archiveMatch = archiveRegex.exec(entry.body);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const exactHost = archives.find((item) => item.hostOs === hostOs);
|
|
905
|
+
if (exactHost) return exactHost;
|
|
906
|
+
|
|
907
|
+
const noHost = archives.find((item) => !item.hostOs);
|
|
908
|
+
if (noHost) return noHost;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
throw new Error(
|
|
912
|
+
`Unable to find Android cmdline-tools archive for host OS '${hostOs}'.`,
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function buildAndroidRepositoryUrl(relativeOrAbsolutePath) {
|
|
917
|
+
const raw = String(relativeOrAbsolutePath || "");
|
|
918
|
+
if (!raw) return "";
|
|
919
|
+
if (/^https?:\/\//i.test(raw)) return raw;
|
|
920
|
+
return `https://dl.google.com/android/repository/${raw.replace(/^\/+/, "")}`;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function escapePowerShellLiteral(value) {
|
|
924
|
+
return String(value).replace(/'/g, "''");
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function extractZipArchive(zipPath, destination) {
|
|
928
|
+
ensureDir(destination);
|
|
929
|
+
if (process.platform === "win32") {
|
|
930
|
+
if (
|
|
931
|
+
!commandCanRun("powershell", [
|
|
932
|
+
"-NoProfile",
|
|
933
|
+
"-Command",
|
|
934
|
+
"$PSVersionTable.PSVersion.ToString()",
|
|
935
|
+
])
|
|
936
|
+
) {
|
|
937
|
+
throw new Error(
|
|
938
|
+
"PowerShell is required to extract Android command-line tools zip on Windows.",
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const escapedZip = escapePowerShellLiteral(path.resolve(zipPath));
|
|
942
|
+
const escapedDest = escapePowerShellLiteral(path.resolve(destination));
|
|
943
|
+
runCommand("powershell", [
|
|
944
|
+
"-NoProfile",
|
|
945
|
+
"-Command",
|
|
946
|
+
`Expand-Archive -Path '${escapedZip}' -DestinationPath '${escapedDest}' -Force`,
|
|
947
|
+
]);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (commandCanRun("unzip", ["-v"])) {
|
|
952
|
+
runCommand("unzip", ["-q", zipPath, "-d", destination]);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (process.platform === "darwin" && commandCanRun("ditto", ["-h"])) {
|
|
957
|
+
runCommand("ditto", ["-x", "-k", zipPath, destination]);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
throw new Error(
|
|
962
|
+
"No zip extraction tool found. Install unzip (or use PowerShell on Windows).",
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function findCommandLineToolsSourceDir(extractRoot) {
|
|
967
|
+
const sdkmanagerName =
|
|
968
|
+
process.platform === "win32" ? "sdkmanager.bat" : "sdkmanager";
|
|
969
|
+
const queue = [{ dir: extractRoot, depth: 0 }];
|
|
970
|
+
while (queue.length > 0) {
|
|
971
|
+
const current = queue.shift();
|
|
972
|
+
if (!current || current.depth > 5) continue;
|
|
973
|
+
const sdkmanagerPath = path.join(current.dir, "bin", sdkmanagerName);
|
|
974
|
+
if (exists(sdkmanagerPath)) {
|
|
975
|
+
return current.dir;
|
|
976
|
+
}
|
|
977
|
+
const entries = fs.readdirSync(current.dir, { withFileTypes: true });
|
|
978
|
+
for (const entry of entries) {
|
|
979
|
+
if (entry.isDirectory()) {
|
|
980
|
+
queue.push({
|
|
981
|
+
dir: path.join(current.dir, entry.name),
|
|
982
|
+
depth: current.depth + 1,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function installAndroidCommandLineTools(sdkRoot) {
|
|
991
|
+
if (!sdkRoot) {
|
|
992
|
+
throw new Error(
|
|
993
|
+
"Android SDK root is missing, unable to install command-line tools.",
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
console.log(
|
|
997
|
+
"Android sdkmanager is unavailable. Try to auto-install Android Command-line Tools...",
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
const googleReachable = await canReachGoogleAndroidRepo();
|
|
1001
|
+
if (!googleReachable) {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
"Unable to connect to Google Android repository (dl.google.com). Please manually install Android command-line tools and set ANDROID_SDK_ROOT.",
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const repositoryXml = await fetchText(
|
|
1008
|
+
"https://dl.google.com/android/repository/repository2-1.xml",
|
|
1009
|
+
);
|
|
1010
|
+
const archive = parseAndroidCommandLineToolsArchiveInfo(repositoryXml);
|
|
1011
|
+
const downloadUrl = buildAndroidRepositoryUrl(archive.url);
|
|
1012
|
+
if (!downloadUrl) {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
"Unable to resolve Android command-line tools download URL.",
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (archive.sizeBytes > 0) {
|
|
1019
|
+
console.log("Estimated Android download size for command-line tools:");
|
|
1020
|
+
console.log(`- ${archive.packagePath}: ${formatBytes(archive.sizeBytes)}`);
|
|
1021
|
+
} else {
|
|
1022
|
+
console.log(
|
|
1023
|
+
"Estimated Android download size for command-line tools: unknown",
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const tempRoot = fs.mkdtempSync(
|
|
1028
|
+
path.join(os.tmpdir(), "quicktvui-cmdline-tools-"),
|
|
1029
|
+
);
|
|
1030
|
+
const zipPath = path.join(tempRoot, "commandlinetools.zip");
|
|
1031
|
+
const extractDir = path.join(tempRoot, "extract");
|
|
1032
|
+
ensureDir(extractDir);
|
|
1033
|
+
|
|
1034
|
+
try {
|
|
1035
|
+
console.log(`Downloading Android command-line tools: ${downloadUrl}`);
|
|
1036
|
+
await downloadToFile(downloadUrl, zipPath);
|
|
1037
|
+
console.log("Extracting Android command-line tools...");
|
|
1038
|
+
extractZipArchive(zipPath, extractDir);
|
|
1039
|
+
|
|
1040
|
+
const sourceDir = findCommandLineToolsSourceDir(extractDir);
|
|
1041
|
+
if (!sourceDir) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
"Extracted command-line tools are invalid (sdkmanager not found).",
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const cmdlineToolsDir = path.join(sdkRoot, "cmdline-tools");
|
|
1048
|
+
const latestDir = path.join(cmdlineToolsDir, "latest");
|
|
1049
|
+
ensureDir(cmdlineToolsDir);
|
|
1050
|
+
removeDirectoryIfExists(latestDir);
|
|
1051
|
+
copyDirectoryRecursive(sourceDir, latestDir);
|
|
1052
|
+
if (process.platform !== "win32") {
|
|
1053
|
+
const sdkmanagerScript = path.join(latestDir, "bin", "sdkmanager");
|
|
1054
|
+
const avdmanagerScript = path.join(latestDir, "bin", "avdmanager");
|
|
1055
|
+
if (exists(sdkmanagerScript)) fs.chmodSync(sdkmanagerScript, 0o755);
|
|
1056
|
+
if (exists(avdmanagerScript)) fs.chmodSync(avdmanagerScript, 0o755);
|
|
1057
|
+
}
|
|
1058
|
+
console.log(`Installed Android command-line tools: ${latestDir}`);
|
|
1059
|
+
} finally {
|
|
1060
|
+
removeDirectoryIfExists(tempRoot);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function ensureAndroidCommandLineToolsAvailable(sdkRoot) {
|
|
1065
|
+
let sdkmanagerPath = resolveAndroidTool("sdkmanager", sdkRoot);
|
|
1066
|
+
let avdmanagerPath = resolveAndroidTool("avdmanager", sdkRoot);
|
|
1067
|
+
if (sdkmanagerPath && avdmanagerPath) {
|
|
1068
|
+
return { sdkmanagerPath, avdmanagerPath };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
await installAndroidCommandLineTools(sdkRoot);
|
|
1072
|
+
sdkmanagerPath = resolveAndroidTool("sdkmanager", sdkRoot);
|
|
1073
|
+
avdmanagerPath = resolveAndroidTool("avdmanager", sdkRoot);
|
|
1074
|
+
return { sdkmanagerPath, avdmanagerPath };
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function resolveAdbOverride(rawValue) {
|
|
1078
|
+
const value = String(rawValue || "").trim();
|
|
1079
|
+
if (!value) return null;
|
|
1080
|
+
const looksLikePath =
|
|
1081
|
+
value.startsWith(".") ||
|
|
1082
|
+
value.includes("/") ||
|
|
1083
|
+
value.includes("\\") ||
|
|
1084
|
+
path.isAbsolute(value);
|
|
1085
|
+
if (looksLikePath) {
|
|
1086
|
+
const candidate = path.resolve(value);
|
|
1087
|
+
return binaryExists(candidate, ["version"]) ? candidate : null;
|
|
1088
|
+
}
|
|
1089
|
+
return commandCanRun(value, ["version"]) ? value : null;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function resolveAdbOverrideFromArgs(args) {
|
|
1093
|
+
const fromArg =
|
|
1094
|
+
typeof args["adb-path"] === "string" ? args["adb-path"].trim() : "";
|
|
1095
|
+
if (fromArg) {
|
|
1096
|
+
const resolved = resolveAdbOverride(fromArg);
|
|
1097
|
+
if (!resolved) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`Invalid --adb-path: ${fromArg}. Please provide a valid adb binary path or command.`,
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
return { adbPath: resolved, source: "--adb-path" };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const fromEnv =
|
|
1106
|
+
typeof process.env.QUICKTVUI_ADB_PATH === "string"
|
|
1107
|
+
? process.env.QUICKTVUI_ADB_PATH.trim()
|
|
1108
|
+
: "";
|
|
1109
|
+
if (fromEnv) {
|
|
1110
|
+
const resolved = resolveAdbOverride(fromEnv);
|
|
1111
|
+
if (resolved) {
|
|
1112
|
+
return { adbPath: resolved, source: "QUICKTVUI_ADB_PATH" };
|
|
1113
|
+
}
|
|
1114
|
+
console.log(
|
|
1115
|
+
`Warning: QUICKTVUI_ADB_PATH is set but invalid: ${fromEnv}. Ignore and continue auto detection.`,
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function resolveAdbPathForSetup(args, sdkRoot) {
|
|
1123
|
+
const override = resolveAdbOverrideFromArgs(args);
|
|
1124
|
+
if (override) {
|
|
1125
|
+
console.log(`Using adb from ${override.source}: ${override.adbPath}`);
|
|
1126
|
+
return override.adbPath;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
let adbPath = resolveAndroidTool("adb", sdkRoot);
|
|
1130
|
+
if (adbPath) {
|
|
1131
|
+
return adbPath;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (!sdkRoot) {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
let sdkmanagerPath = resolveAndroidTool("sdkmanager", sdkRoot);
|
|
1139
|
+
if (!sdkmanagerPath) {
|
|
1140
|
+
const toolPaths = await ensureAndroidCommandLineToolsAvailable(sdkRoot);
|
|
1141
|
+
sdkmanagerPath = toolPaths.sdkmanagerPath;
|
|
1142
|
+
}
|
|
1143
|
+
if (!sdkmanagerPath) return null;
|
|
1144
|
+
|
|
1145
|
+
console.log(
|
|
1146
|
+
"adb is unavailable. Try to auto-install Android SDK Platform-Tools...",
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
const googleReachable = await canReachGoogleAndroidRepo();
|
|
1150
|
+
if (!googleReachable) {
|
|
1151
|
+
throw new Error(
|
|
1152
|
+
"adb is missing and Google repository (dl.google.com) is unreachable. Please manually install Android SDK Platform-Tools, then rerun (or pass --adb-path / QUICKTVUI_ADB_PATH).",
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
try {
|
|
1157
|
+
const sizeMap = await fetchAndroidPackageSizeMap();
|
|
1158
|
+
printSdkPackageDownloadEstimate(
|
|
1159
|
+
sizeMap,
|
|
1160
|
+
["platform-tools"],
|
|
1161
|
+
"Estimated Android download size for adb:",
|
|
1162
|
+
);
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
console.log(
|
|
1165
|
+
`Unable to fetch package size metadata: ${error.message}. Continue with sdkmanager download.`,
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
console.log("Installing required Android SDK package: platform-tools...");
|
|
1170
|
+
ensureAndroidSdkPackages(sdkmanagerPath, sdkRoot, ["platform-tools"]);
|
|
1171
|
+
adbPath = resolveAndroidTool("adb", sdkRoot);
|
|
1172
|
+
return adbPath;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function parseAdbDevices(rawOutput) {
|
|
1176
|
+
const lines = rawOutput
|
|
1177
|
+
.split(/\r?\n/)
|
|
1178
|
+
.map((line) => line.trim())
|
|
1179
|
+
.filter(Boolean);
|
|
1180
|
+
const devices = [];
|
|
1181
|
+
const unauthorized = [];
|
|
1182
|
+
const offline = [];
|
|
1183
|
+
|
|
1184
|
+
for (const line of lines) {
|
|
1185
|
+
if (line.startsWith("List of devices attached")) continue;
|
|
1186
|
+
const columns = line.split(/\s+/);
|
|
1187
|
+
if (columns.length < 2) continue;
|
|
1188
|
+
const serial = columns[0];
|
|
1189
|
+
const state = columns[1];
|
|
1190
|
+
if (state === "device") {
|
|
1191
|
+
devices.push(serial);
|
|
1192
|
+
} else if (state === "unauthorized") {
|
|
1193
|
+
unauthorized.push(serial);
|
|
1194
|
+
} else if (state === "offline") {
|
|
1195
|
+
offline.push(serial);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return { devices, unauthorized, offline };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function listConnectedDevices(adbPath) {
|
|
1203
|
+
const result = runCommandCapture(adbPath, ["devices"]);
|
|
1204
|
+
return parseAdbDevices(result.stdout);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function listAvds(emulatorPath) {
|
|
1208
|
+
const result = runCommandCapture(emulatorPath, ["-list-avds"]);
|
|
1209
|
+
return result.stdout
|
|
1210
|
+
.split(/\r?\n/)
|
|
1211
|
+
.map((line) => line.trim())
|
|
1212
|
+
.filter(Boolean);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function getPreferredSystemImageCandidates() {
|
|
1216
|
+
const apiLevel =
|
|
1217
|
+
process.env.QUICKTVUI_ANDROID_API || DEFAULT_ANDROID_API_LEVEL;
|
|
1218
|
+
const hostArch = os.arch();
|
|
1219
|
+
const abi = hostArch === "arm64" ? "arm64-v8a" : "x86_64";
|
|
1220
|
+
return [
|
|
1221
|
+
`system-images;${apiLevel};google_apis;${abi}`,
|
|
1222
|
+
`system-images;${apiLevel};google_apis_playstore;${abi}`,
|
|
1223
|
+
`system-images;${apiLevel};default;${abi}`,
|
|
1224
|
+
];
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function ensureAndroidSdkPackages(sdkmanagerPath, sdkRoot, packages) {
|
|
1228
|
+
runCommand(sdkmanagerPath, ["--licenses"], {
|
|
1229
|
+
input: "y\n".repeat(80),
|
|
1230
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
for (const packageName of packages) {
|
|
1234
|
+
runCommand(sdkmanagerPath, [`--sdk_root=${sdkRoot}`, packageName], {
|
|
1235
|
+
stdio: "inherit",
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function installSystemImagePackage(sdkmanagerPath, sdkRoot) {
|
|
1241
|
+
const candidates = getPreferredSystemImageCandidates();
|
|
1242
|
+
let lastError;
|
|
1243
|
+
|
|
1244
|
+
for (const candidate of candidates) {
|
|
1245
|
+
try {
|
|
1246
|
+
console.log(`Trying Android system image: ${candidate}`);
|
|
1247
|
+
runCommand(sdkmanagerPath, [`--sdk_root=${sdkRoot}`, candidate], {
|
|
1248
|
+
stdio: "inherit",
|
|
1249
|
+
});
|
|
1250
|
+
return candidate;
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
lastError = error;
|
|
1253
|
+
console.log(`- skipped: ${candidate}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Unable to install Android system image automatically. ${
|
|
1259
|
+
lastError ? lastError.message : ""
|
|
1260
|
+
}`.trim(),
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function createAvd(avdmanagerPath, avdName, imagePackage) {
|
|
1265
|
+
runCommand(
|
|
1266
|
+
avdmanagerPath,
|
|
1267
|
+
["create", "avd", "-n", avdName, "-k", imagePackage, "-f"],
|
|
1268
|
+
{
|
|
1269
|
+
input: "no\n",
|
|
1270
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
1271
|
+
},
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function startAvd(emulatorPath, avdName, args) {
|
|
1276
|
+
const emulatorArgs = [
|
|
1277
|
+
"-avd",
|
|
1278
|
+
avdName,
|
|
1279
|
+
"-netdelay",
|
|
1280
|
+
"none",
|
|
1281
|
+
"-netspeed",
|
|
1282
|
+
"full",
|
|
1283
|
+
];
|
|
1284
|
+
if (args.headless) {
|
|
1285
|
+
emulatorArgs.push("-no-window", "-no-audio");
|
|
1286
|
+
}
|
|
1287
|
+
runCommandDetached(emulatorPath, emulatorArgs);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async function waitForAdbDevice(adbPath, timeoutMs) {
|
|
1291
|
+
const deadline = Date.now() + timeoutMs;
|
|
1292
|
+
while (Date.now() < deadline) {
|
|
1293
|
+
try {
|
|
1294
|
+
const status = listConnectedDevices(adbPath);
|
|
1295
|
+
if (status.devices.length > 0) {
|
|
1296
|
+
return status.devices[0];
|
|
1297
|
+
}
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
// ignore transient adb boot race
|
|
1300
|
+
}
|
|
1301
|
+
await delay(2000);
|
|
1302
|
+
}
|
|
1303
|
+
throw new Error("Timed out waiting for Android device connection.");
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async function waitForBootComplete(adbPath, serial, timeoutMs) {
|
|
1307
|
+
const deadline = Date.now() + timeoutMs;
|
|
1308
|
+
while (Date.now() < deadline) {
|
|
1309
|
+
try {
|
|
1310
|
+
const result = runCommandCapture(adbPath, [
|
|
1311
|
+
"-s",
|
|
1312
|
+
serial,
|
|
1313
|
+
"shell",
|
|
1314
|
+
"getprop",
|
|
1315
|
+
"sys.boot_completed",
|
|
1316
|
+
]);
|
|
1317
|
+
if ((result.stdout || "").trim() === "1") {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
// ignore transient shell failures during emulator boot
|
|
1322
|
+
}
|
|
1323
|
+
await delay(2500);
|
|
1324
|
+
}
|
|
1325
|
+
throw new Error(`Timed out waiting for device boot completion: ${serial}`);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function getInstalledRuntimeInfo(adbPath, serial) {
|
|
1329
|
+
try {
|
|
1330
|
+
const result = runCommandCapture(adbPath, [
|
|
1331
|
+
"-s",
|
|
1332
|
+
serial,
|
|
1333
|
+
"shell",
|
|
1334
|
+
"dumpsys",
|
|
1335
|
+
"package",
|
|
1336
|
+
RUNTIME_PACKAGE_NAME,
|
|
1337
|
+
]);
|
|
1338
|
+
const text = result.stdout || "";
|
|
1339
|
+
if (text.includes("Unable to find package")) {
|
|
1340
|
+
return { installed: false, versionName: null, versionCode: null };
|
|
1341
|
+
}
|
|
1342
|
+
const versionNameLine = text
|
|
1343
|
+
.split(/\r?\n/)
|
|
1344
|
+
.find((line) => line.includes("versionName="));
|
|
1345
|
+
const versionCodeLine = text
|
|
1346
|
+
.split(/\r?\n/)
|
|
1347
|
+
.find((line) => line.includes("versionCode="));
|
|
1348
|
+
const versionName = versionNameLine
|
|
1349
|
+
? versionNameLine.split("=").slice(1).join("=").trim()
|
|
1350
|
+
: null;
|
|
1351
|
+
const versionCode = versionCodeLine
|
|
1352
|
+
? versionCodeLine.split("=").slice(1).join("=").trim()
|
|
1353
|
+
: null;
|
|
1354
|
+
return {
|
|
1355
|
+
installed: true,
|
|
1356
|
+
versionName,
|
|
1357
|
+
versionCode,
|
|
1358
|
+
};
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
return { installed: false, versionName: null, versionCode: null };
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
async function waitForRuntimeRunning(adbPath, serial, timeoutMs) {
|
|
1365
|
+
const deadline = Date.now() + timeoutMs;
|
|
1366
|
+
while (Date.now() < deadline) {
|
|
1367
|
+
try {
|
|
1368
|
+
const result = runCommandCapture(adbPath, [
|
|
1369
|
+
"-s",
|
|
1370
|
+
serial,
|
|
1371
|
+
"shell",
|
|
1372
|
+
"pidof",
|
|
1373
|
+
RUNTIME_PACKAGE_NAME,
|
|
1374
|
+
]);
|
|
1375
|
+
if ((result.stdout || "").trim()) {
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
// ignore during startup
|
|
1380
|
+
}
|
|
1381
|
+
await delay(1000);
|
|
1382
|
+
}
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function setRuntimeDebugServerHost(adbPath, serial, hostIp) {
|
|
1387
|
+
runCommandCapture(adbPath, [
|
|
1388
|
+
"-s",
|
|
1389
|
+
serial,
|
|
1390
|
+
"shell",
|
|
1391
|
+
"am",
|
|
1392
|
+
"broadcast",
|
|
1393
|
+
"-a",
|
|
1394
|
+
RUNTIME_DEBUG_BROADCAST_ACTION,
|
|
1395
|
+
"--es",
|
|
1396
|
+
"ip",
|
|
1397
|
+
hostIp,
|
|
1398
|
+
]);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async function ensureRuntimeInstalledAndConfigured(adbPath, serial, args) {
|
|
1402
|
+
const forceRuntimeInstall = toBooleanFlag(
|
|
1403
|
+
args["force-runtime-install"],
|
|
1404
|
+
false,
|
|
1405
|
+
);
|
|
1406
|
+
const desiredRuntimeVersion =
|
|
1407
|
+
typeof args["runtime-version"] === "string" &&
|
|
1408
|
+
args["runtime-version"].trim()
|
|
1409
|
+
? args["runtime-version"].trim()
|
|
1410
|
+
: "";
|
|
1411
|
+
const overrideRuntimeUrl =
|
|
1412
|
+
typeof args["runtime-url"] === "string" && args["runtime-url"].trim()
|
|
1413
|
+
? args["runtime-url"].trim()
|
|
1414
|
+
: "";
|
|
1415
|
+
const hostIp =
|
|
1416
|
+
(typeof args["server-host"] === "string" && args["server-host"].trim()) ||
|
|
1417
|
+
getLocalIPv4Address();
|
|
1418
|
+
|
|
1419
|
+
if (!hostIp) {
|
|
1420
|
+
throw new Error(
|
|
1421
|
+
"Unable to detect local IPv4 address for runtime debug server host.",
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
let runtimeInfo = getInstalledRuntimeInfo(adbPath, serial);
|
|
1426
|
+
let installedVersion = runtimeInfo.versionName;
|
|
1427
|
+
const shouldInstallByVersion =
|
|
1428
|
+
Boolean(desiredRuntimeVersion) &&
|
|
1429
|
+
(!installedVersion || installedVersion !== desiredRuntimeVersion);
|
|
1430
|
+
|
|
1431
|
+
if (forceRuntimeInstall || !runtimeInfo.installed || shouldInstallByVersion) {
|
|
1432
|
+
let targetVersion = desiredRuntimeVersion;
|
|
1433
|
+
if (!targetVersion && !overrideRuntimeUrl) {
|
|
1434
|
+
const versions = await fetchRuntimeVersions();
|
|
1435
|
+
if (!versions || versions.length === 0) {
|
|
1436
|
+
throw new Error("No runtime versions found in runtime repository.");
|
|
1437
|
+
}
|
|
1438
|
+
targetVersion = versions[0];
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const apkUrl = overrideRuntimeUrl || buildRuntimeApkUrl(targetVersion);
|
|
1442
|
+
const apkName = `quicktvui-runtime-${targetVersion || Date.now()}.apk`;
|
|
1443
|
+
const apkPath = path.join(os.tmpdir(), apkName);
|
|
1444
|
+
console.log(`Downloading runtime APK: ${apkUrl}`);
|
|
1445
|
+
await downloadToFile(apkUrl, apkPath);
|
|
1446
|
+
|
|
1447
|
+
try {
|
|
1448
|
+
console.log("Installing runtime APK...");
|
|
1449
|
+
runCommand(adbPath, ["-s", serial, "install", "-r", apkPath], {
|
|
1450
|
+
stdio: "inherit",
|
|
1451
|
+
});
|
|
1452
|
+
} finally {
|
|
1453
|
+
if (exists(apkPath)) {
|
|
1454
|
+
fs.rmSync(apkPath, { force: true });
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
runtimeInfo = getInstalledRuntimeInfo(adbPath, serial);
|
|
1459
|
+
installedVersion = runtimeInfo.versionName;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
console.log(`Launching runtime app: ${RUNTIME_PACKAGE_NAME}`);
|
|
1463
|
+
runCommandCapture(adbPath, [
|
|
1464
|
+
"-s",
|
|
1465
|
+
serial,
|
|
1466
|
+
"shell",
|
|
1467
|
+
"am",
|
|
1468
|
+
"start",
|
|
1469
|
+
"-n",
|
|
1470
|
+
RUNTIME_LAUNCH_ACTIVITY,
|
|
1471
|
+
]);
|
|
1472
|
+
|
|
1473
|
+
const isRunning = await waitForRuntimeRunning(adbPath, serial, 25000);
|
|
1474
|
+
if (!isRunning) {
|
|
1475
|
+
throw new Error("Runtime app did not enter running state in time.");
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
setRuntimeDebugServerHost(adbPath, serial, hostIp);
|
|
1479
|
+
console.log(`Runtime debug server host configured: ${hostIp}`);
|
|
1480
|
+
|
|
1481
|
+
return {
|
|
1482
|
+
hostIp,
|
|
1483
|
+
runtimeVersion: installedVersion,
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function resolveProjectAppPackage(projectRoot) {
|
|
1488
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
1489
|
+
if (!exists(packageJsonPath)) {
|
|
1490
|
+
return "quicktvui-app";
|
|
1491
|
+
}
|
|
1492
|
+
const packageJson = readJsonFile(packageJsonPath);
|
|
1493
|
+
if (typeof packageJson.appName === "string" && packageJson.appName.trim()) {
|
|
1494
|
+
return packageJson.appName.trim();
|
|
1495
|
+
}
|
|
1496
|
+
if (typeof packageJson.name === "string" && packageJson.name.trim()) {
|
|
1497
|
+
return packageJson.name.replace(/^@[^/]+\//, "").trim();
|
|
1498
|
+
}
|
|
1499
|
+
return "quicktvui-app";
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function parseJsonObjectArg(rawValue, optionName) {
|
|
1503
|
+
if (typeof rawValue === "undefined") return {};
|
|
1504
|
+
if (rawValue && typeof rawValue === "object" && !Array.isArray(rawValue)) {
|
|
1505
|
+
return rawValue;
|
|
1506
|
+
}
|
|
1507
|
+
const text = String(rawValue).trim();
|
|
1508
|
+
if (!text) return {};
|
|
1509
|
+
let parsed;
|
|
1510
|
+
try {
|
|
1511
|
+
parsed = JSON.parse(text);
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
throw new Error(`${optionName} must be a valid JSON object string.`);
|
|
1514
|
+
}
|
|
1515
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
|
1516
|
+
throw new Error(`${optionName} must be a JSON object.`);
|
|
1517
|
+
}
|
|
1518
|
+
return parsed;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function appendEsappParam(target, key, value) {
|
|
1522
|
+
if (typeof value === "undefined" || value === null) return;
|
|
1523
|
+
if (typeof value === "string" && !value.trim()) return;
|
|
1524
|
+
target[key] = value;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function buildEsappActionStartUri(queryParams) {
|
|
1528
|
+
const search = new URLSearchParams();
|
|
1529
|
+
for (const [key, value] of Object.entries(queryParams || {})) {
|
|
1530
|
+
if (typeof value === "undefined" || value === null) continue;
|
|
1531
|
+
if (typeof value === "string" && !value.trim()) continue;
|
|
1532
|
+
if (typeof value === "object") {
|
|
1533
|
+
search.set(String(key), JSON.stringify(value));
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
search.set(String(key), String(value));
|
|
1537
|
+
}
|
|
1538
|
+
const query = search.toString();
|
|
1539
|
+
return query ? `esapp://action/start?${query}` : "esapp://action/start";
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function ensureSupportedEsappUri(uri) {
|
|
1543
|
+
const normalized = String(uri || "")
|
|
1544
|
+
.trim()
|
|
1545
|
+
.toLowerCase();
|
|
1546
|
+
if (
|
|
1547
|
+
normalized.startsWith("esapp://") ||
|
|
1548
|
+
normalized.startsWith("quicktv://") ||
|
|
1549
|
+
normalized.startsWith("appcast://")
|
|
1550
|
+
) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
throw new Error(
|
|
1554
|
+
`Unsupported URI scheme for runtime launch: ${uri}. Use esapp://, quicktv://, or appcast://.`,
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function buildEsappLaunchUri(args, projectRoot, defaults = {}) {
|
|
1559
|
+
const positional =
|
|
1560
|
+
args._ && typeof args._[1] === "string" ? args._[1].trim() : "";
|
|
1561
|
+
const rawUri =
|
|
1562
|
+
(typeof args["esapp-uri"] === "string" && args["esapp-uri"].trim()) ||
|
|
1563
|
+
(positional.includes("://") ? positional : "");
|
|
1564
|
+
if (rawUri) {
|
|
1565
|
+
ensureSupportedEsappUri(rawUri);
|
|
1566
|
+
return rawUri;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const query = {};
|
|
1570
|
+
const defaultPkg =
|
|
1571
|
+
(typeof defaults.pkg === "string" && defaults.pkg.trim()) ||
|
|
1572
|
+
resolveProjectAppPackage(projectRoot);
|
|
1573
|
+
const defaultFrom =
|
|
1574
|
+
(typeof defaults.from === "string" && defaults.from.trim()) || "cmd";
|
|
1575
|
+
|
|
1576
|
+
const pkg =
|
|
1577
|
+
(typeof args.pkg === "string" && args.pkg.trim()) ||
|
|
1578
|
+
(!positional.includes("://") ? positional : "") ||
|
|
1579
|
+
defaultPkg;
|
|
1580
|
+
if (!pkg) {
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
"Missing target package for esapp launch. Use --pkg <package> or --esapp-uri <uri>.",
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
appendEsappParam(
|
|
1587
|
+
query,
|
|
1588
|
+
"from",
|
|
1589
|
+
(typeof args.from === "string" && args.from.trim()) || defaultFrom,
|
|
1590
|
+
);
|
|
1591
|
+
appendEsappParam(query, "pkg", pkg);
|
|
1592
|
+
|
|
1593
|
+
appendEsappParam(query, "ver", args.ver);
|
|
1594
|
+
appendEsappParam(query, "minVer", args["min-ver"]);
|
|
1595
|
+
appendEsappParam(query, "repository", args.repository);
|
|
1596
|
+
appendEsappParam(query, "uri", args.uri || defaults.uri);
|
|
1597
|
+
appendEsappParam(query, "flags", args.flags);
|
|
1598
|
+
appendEsappParam(query, "args", args.args);
|
|
1599
|
+
appendEsappParam(query, "exp", args.exp);
|
|
1600
|
+
appendEsappParam(query, "name", args.name);
|
|
1601
|
+
appendEsappParam(query, "icon", args.icon);
|
|
1602
|
+
appendEsappParam(query, "pageTag", args["page-tag"]);
|
|
1603
|
+
appendEsappParam(query, "pageLimit", args["page-limit"]);
|
|
1604
|
+
appendEsappParam(query, "bgColor", args["bg-color"]);
|
|
1605
|
+
appendEsappParam(query, "splash", args.splash);
|
|
1606
|
+
|
|
1607
|
+
if (typeof args["is-card"] !== "undefined") {
|
|
1608
|
+
query.isCard = toBooleanFlag(args["is-card"], false);
|
|
1609
|
+
}
|
|
1610
|
+
if (typeof args.transparent !== "undefined") {
|
|
1611
|
+
query.transparent = toBooleanFlag(args.transparent, false);
|
|
1612
|
+
}
|
|
1613
|
+
if (typeof args.enc !== "undefined") {
|
|
1614
|
+
query.enc = toBooleanFlag(args.enc, false);
|
|
1615
|
+
}
|
|
1616
|
+
if (typeof args["check-network"] !== "undefined") {
|
|
1617
|
+
query.checkNetwork = toBooleanFlag(args["check-network"], false);
|
|
1618
|
+
}
|
|
1619
|
+
if (typeof args["use-latest"] !== "undefined") {
|
|
1620
|
+
query.useLatest = toBooleanFlag(args["use-latest"], false);
|
|
1621
|
+
}
|
|
1622
|
+
if (typeof args["feature-single-activity"] !== "undefined") {
|
|
1623
|
+
query.feature_single_activity = toBooleanFlag(
|
|
1624
|
+
args["feature-single-activity"],
|
|
1625
|
+
false,
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
const extQuery = parseJsonObjectArg(args["esapp-query"], "--esapp-query");
|
|
1630
|
+
for (const [key, value] of Object.entries(extQuery)) {
|
|
1631
|
+
appendEsappParam(query, key, value);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
return buildEsappActionStartUri(query);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function startEsappOnRuntimeByUri(adbPath, serial, runtimePackage, launchUri) {
|
|
1638
|
+
ensureSupportedEsappUri(launchUri);
|
|
1639
|
+
const result = runCommandCapture(adbPath, [
|
|
1640
|
+
"-s",
|
|
1641
|
+
serial,
|
|
1642
|
+
"shell",
|
|
1643
|
+
"am",
|
|
1644
|
+
"start",
|
|
1645
|
+
"-a",
|
|
1646
|
+
"android.intent.action.VIEW",
|
|
1647
|
+
"-p",
|
|
1648
|
+
runtimePackage,
|
|
1649
|
+
"-d",
|
|
1650
|
+
launchUri,
|
|
1651
|
+
]);
|
|
1652
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
|
|
1653
|
+
if (/(^|\s)error[:\s]/i.test(output) || /exception/i.test(output)) {
|
|
1654
|
+
throw new Error(output || "Unknown runtime start error.");
|
|
1655
|
+
}
|
|
1656
|
+
return output;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
async function clickRuntimeLoadButtonByDpad(adbPath, serial) {
|
|
1660
|
+
const rightCount = 6;
|
|
1661
|
+
for (let i = 0; i < rightCount; i += 1) {
|
|
1662
|
+
runCommandCapture(adbPath, [
|
|
1663
|
+
"-s",
|
|
1664
|
+
serial,
|
|
1665
|
+
"shell",
|
|
1666
|
+
"input",
|
|
1667
|
+
"keyevent",
|
|
1668
|
+
"22",
|
|
1669
|
+
]);
|
|
1670
|
+
await delay(120);
|
|
1671
|
+
}
|
|
1672
|
+
runCommandCapture(adbPath, [
|
|
1673
|
+
"-s",
|
|
1674
|
+
serial,
|
|
1675
|
+
"shell",
|
|
1676
|
+
"input",
|
|
1677
|
+
"keyevent",
|
|
1678
|
+
"23",
|
|
1679
|
+
]);
|
|
1680
|
+
}
|
|
20
1681
|
|
|
21
|
-
function
|
|
22
|
-
|
|
1682
|
+
async function autoLoadLocalBundleOnRuntime(
|
|
1683
|
+
adbPath,
|
|
1684
|
+
serial,
|
|
1685
|
+
projectRoot,
|
|
1686
|
+
hostIp,
|
|
1687
|
+
port,
|
|
1688
|
+
args = {},
|
|
1689
|
+
) {
|
|
1690
|
+
const appPkg = resolveProjectAppPackage(projectRoot);
|
|
1691
|
+
const launchUri = buildEsappLaunchUri(args, projectRoot, {
|
|
1692
|
+
pkg: appPkg,
|
|
1693
|
+
from: "cmd",
|
|
1694
|
+
uri: `${hostIp}:${port}`,
|
|
1695
|
+
});
|
|
1696
|
+
console.log(`Auto loading bundle on runtime: ${launchUri}`);
|
|
1697
|
+
try {
|
|
1698
|
+
startEsappOnRuntimeByUri(adbPath, serial, RUNTIME_PACKAGE_NAME, launchUri);
|
|
1699
|
+
console.log("Triggered local bundle by runtime intent.");
|
|
1700
|
+
return;
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
console.log(
|
|
1703
|
+
`Runtime intent load failed, fallback to key events: ${error.message}`,
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
await clickRuntimeLoadButtonByDpad(adbPath, serial);
|
|
1708
|
+
console.log("Triggered runtime load button with DPAD key events.");
|
|
23
1709
|
}
|
|
24
1710
|
|
|
25
|
-
function
|
|
26
|
-
|
|
1711
|
+
function shouldInstallNodeForCurrentPlatform(requiredMajor, args) {
|
|
1712
|
+
const force = toBooleanFlag(args["force-node-install"], false);
|
|
1713
|
+
if (force) return true;
|
|
1714
|
+
const currentMajor = resolveNodeMajorVersion();
|
|
1715
|
+
if (!currentMajor) return true;
|
|
1716
|
+
return currentMajor < requiredMajor;
|
|
27
1717
|
}
|
|
28
1718
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1719
|
+
function installNodeForMac(requiredMajor) {
|
|
1720
|
+
if (commandCanRun("brew", ["--version"])) {
|
|
1721
|
+
runCommand("brew", ["install", `node@${requiredMajor}`], {
|
|
1722
|
+
stdio: "inherit",
|
|
1723
|
+
});
|
|
1724
|
+
runCommand(
|
|
1725
|
+
"brew",
|
|
1726
|
+
["link", `node@${requiredMajor}`, "--force", "--overwrite"],
|
|
1727
|
+
{
|
|
1728
|
+
stdio: "inherit",
|
|
1729
|
+
},
|
|
1730
|
+
);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
if (
|
|
1735
|
+
commandExistsViaShell(
|
|
1736
|
+
"/bin/zsh -lc 'command -v nvm >/dev/null 2>&1 && nvm --version >/dev/null 2>&1'",
|
|
1737
|
+
)
|
|
1738
|
+
) {
|
|
1739
|
+
runCommand(
|
|
1740
|
+
"/bin/zsh",
|
|
1741
|
+
["-lc", `nvm install ${requiredMajor} && nvm use ${requiredMajor}`],
|
|
1742
|
+
{ stdio: "inherit" },
|
|
1743
|
+
);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
throw new Error(
|
|
1748
|
+
"Unable to auto-install Node.js on macOS. Install Homebrew or nvm first, then rerun setup-vue-env.",
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function installNodeForWindows() {
|
|
1753
|
+
if (commandCanRun("winget", ["--version"])) {
|
|
1754
|
+
runCommand(
|
|
1755
|
+
"winget",
|
|
1756
|
+
[
|
|
1757
|
+
"install",
|
|
1758
|
+
"--id",
|
|
1759
|
+
"OpenJS.NodeJS.LTS",
|
|
1760
|
+
"-e",
|
|
1761
|
+
"--accept-source-agreements",
|
|
1762
|
+
"--accept-package-agreements",
|
|
1763
|
+
],
|
|
1764
|
+
{ stdio: "inherit" },
|
|
1765
|
+
);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
if (commandCanRun("choco", ["-v"])) {
|
|
1770
|
+
runCommand("choco", ["install", "nodejs-lts", "-y"], { stdio: "inherit" });
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
"Unable to auto-install Node.js on Windows. Install winget or chocolatey first, then rerun setup-vue-env.",
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
async function runSetupVueEnv(args) {
|
|
1780
|
+
const projectRoot = args.project ? path.resolve(args.project) : process.cwd();
|
|
1781
|
+
const requiredNodeMajor = Number(
|
|
1782
|
+
args["node-major"] || DEFAULT_NODE_LTS_MAJOR,
|
|
1783
|
+
);
|
|
1784
|
+
const skipNodeInstall = toBooleanFlag(args["skip-node-install"], false);
|
|
1785
|
+
const skipYarnInstall = toBooleanFlag(args["skip-yarn-install"], false);
|
|
1786
|
+
const skipQuicktvuiCliInstall = toBooleanFlag(
|
|
1787
|
+
args["skip-quicktvui-cli-install"],
|
|
1788
|
+
false,
|
|
1789
|
+
);
|
|
1790
|
+
const skipProjectInstall = toBooleanFlag(args["skip-project-install"], false);
|
|
1791
|
+
|
|
1792
|
+
if (
|
|
1793
|
+
!skipNodeInstall &&
|
|
1794
|
+
shouldInstallNodeForCurrentPlatform(requiredNodeMajor, args)
|
|
1795
|
+
) {
|
|
1796
|
+
console.log(`Installing Node.js LTS (major ${requiredNodeMajor})...`);
|
|
1797
|
+
if (process.platform === "darwin") {
|
|
1798
|
+
installNodeForMac(requiredNodeMajor);
|
|
1799
|
+
} else if (process.platform === "win32") {
|
|
1800
|
+
installNodeForWindows();
|
|
37
1801
|
} else {
|
|
38
|
-
|
|
1802
|
+
throw new Error(
|
|
1803
|
+
"Auto Node.js installation currently supports macOS and Windows only.",
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
} else {
|
|
1807
|
+
console.log(
|
|
1808
|
+
`Node.js check passed (current=${process.versions.node}, required>=${requiredNodeMajor}).`,
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
if (!commandCanRun("npm", ["--version"])) {
|
|
1813
|
+
throw new Error("npm is unavailable after Node.js setup.");
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
if (!skipYarnInstall && !commandCanRun("yarn", ["--version"])) {
|
|
1817
|
+
console.log("Installing yarn globally...");
|
|
1818
|
+
runCommand("npm", ["install", "-g", "yarn"], { stdio: "inherit" });
|
|
1819
|
+
}
|
|
1820
|
+
|
|
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
|
+
if (!skipProjectInstall) {
|
|
1829
|
+
if (!exists(path.join(projectRoot, "package.json"))) {
|
|
1830
|
+
throw new Error(`Missing package.json in project root: ${projectRoot}`);
|
|
1831
|
+
}
|
|
1832
|
+
console.log(`Installing project dependencies: ${projectRoot}`);
|
|
1833
|
+
installProjectDependencies(projectRoot, args);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
console.log(
|
|
1837
|
+
`Vue development environment is ready for project: ${projectRoot}`,
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
async function runSetupAllEnv(args) {
|
|
1842
|
+
console.log("Step 1/2: setup Vue development environment...");
|
|
1843
|
+
await runSetupVueEnv(args);
|
|
1844
|
+
console.log("Step 2/2: setup Android runtime environment...");
|
|
1845
|
+
await runSetupAndroidEnv(args);
|
|
1846
|
+
console.log("All development environments are ready.");
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function resolvePackageManagerCommand(projectRoot) {
|
|
1850
|
+
const yarnLock = path.join(projectRoot, "yarn.lock");
|
|
1851
|
+
const pnpmLock = path.join(projectRoot, "pnpm-lock.yaml");
|
|
1852
|
+
if (exists(yarnLock) && commandExists("yarn")) {
|
|
1853
|
+
return { command: "yarn", args: ["dev"] };
|
|
1854
|
+
}
|
|
1855
|
+
if (exists(pnpmLock) && commandExists("pnpm")) {
|
|
1856
|
+
return { command: "pnpm", args: ["dev"] };
|
|
1857
|
+
}
|
|
1858
|
+
if (commandExists("npm")) {
|
|
1859
|
+
return { command: "npm", args: ["run", "dev"] };
|
|
1860
|
+
}
|
|
1861
|
+
throw new Error("Neither yarn, pnpm, nor npm is available on this machine.");
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
async function runSetupAndroidEnv(args) {
|
|
1865
|
+
const projectRoot = args.project ? path.resolve(args.project) : process.cwd();
|
|
1866
|
+
const skipRuntimeSetup = toBooleanFlag(args["skip-runtime-setup"], false);
|
|
1867
|
+
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";
|
|
1872
|
+
const avdName =
|
|
1873
|
+
typeof args["avd-name"] === "string" && args["avd-name"].trim()
|
|
1874
|
+
? args["avd-name"].trim()
|
|
1875
|
+
: DEFAULT_ANDROID_AVD_NAME;
|
|
1876
|
+
|
|
1877
|
+
const { sdkRoot, source } = resolveAndroidSdkRootForSetup();
|
|
1878
|
+
console.log(`Android SDK root: ${sdkRoot || "not found"} (${source})`);
|
|
1879
|
+
|
|
1880
|
+
const adbPath = await resolveAdbPathForSetup(args, sdkRoot);
|
|
1881
|
+
if (!adbPath) {
|
|
1882
|
+
throw new Error(
|
|
1883
|
+
"adb is unavailable. quicktvui-ai does not bundle adb by default. Install Android SDK Platform-Tools (or Android Studio), or pass --adb-path <path>.",
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
let deviceState = listConnectedDevices(adbPath);
|
|
1888
|
+
console.log(
|
|
1889
|
+
`ADB devices: connected=${deviceState.devices.length}, unauthorized=${deviceState.unauthorized.length}, offline=${deviceState.offline.length}`,
|
|
1890
|
+
);
|
|
1891
|
+
let useConnectedDevice = deviceState.devices.length > 0;
|
|
1892
|
+
let shouldSetupEmulator = false;
|
|
1893
|
+
let preferredRealDeviceSerial = null;
|
|
1894
|
+
|
|
1895
|
+
if (deviceState.devices.length > 0) {
|
|
1896
|
+
const connectedList = deviceState.devices.join(", ");
|
|
1897
|
+
useConnectedDevice = await askYesNo(
|
|
1898
|
+
`Detected connected Android device(s): ${connectedList}. Use this device for setup?`,
|
|
1899
|
+
true,
|
|
1900
|
+
args,
|
|
1901
|
+
);
|
|
1902
|
+
if (useConnectedDevice) {
|
|
1903
|
+
preferredRealDeviceSerial = deviceState.devices[0] || null;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
if (!useConnectedDevice) {
|
|
1908
|
+
const connectRealByIp = await askYesNo(
|
|
1909
|
+
"Do you want to connect a real device by IP now (adb connect)?",
|
|
1910
|
+
true,
|
|
1911
|
+
args,
|
|
1912
|
+
);
|
|
1913
|
+
if (connectRealByIp) {
|
|
1914
|
+
const connectedEndpoint = await connectRealDeviceByIp(adbPath, args);
|
|
1915
|
+
if (connectedEndpoint) {
|
|
1916
|
+
await delay(600);
|
|
1917
|
+
deviceState = listConnectedDevices(adbPath);
|
|
1918
|
+
if (deviceState.devices.length > 0) {
|
|
1919
|
+
useConnectedDevice = true;
|
|
1920
|
+
preferredRealDeviceSerial =
|
|
1921
|
+
deviceState.devices.find(
|
|
1922
|
+
(serial) => serial === connectedEndpoint,
|
|
1923
|
+
) || connectedEndpoint;
|
|
1924
|
+
console.log(
|
|
1925
|
+
`Detected connected device(s): ${deviceState.devices.join(", ")}`,
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
if (!useConnectedDevice) {
|
|
1933
|
+
if (autoEmulator) {
|
|
1934
|
+
shouldSetupEmulator = await askYesNo(
|
|
1935
|
+
"No usable real device found. Do you want to download and use official Google Android emulator now?",
|
|
1936
|
+
true,
|
|
1937
|
+
args,
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if (!shouldSetupEmulator) {
|
|
1942
|
+
throw new Error(
|
|
1943
|
+
"No real device connected and emulator setup declined. Please manually install/connect device or emulator.",
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
if (!useConnectedDevice && shouldSetupEmulator) {
|
|
1949
|
+
const emulatorPath = resolveAndroidTool("emulator", sdkRoot);
|
|
1950
|
+
if (!emulatorPath) {
|
|
1951
|
+
throw new Error(
|
|
1952
|
+
"No Android device is connected and emulator tool is unavailable. Install Android Studio (with SDK + Emulator) first.",
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
let avds = listAvds(emulatorPath);
|
|
1957
|
+
if (avds.length === 0) {
|
|
1958
|
+
let sdkmanagerPath = resolveAndroidTool("sdkmanager", sdkRoot);
|
|
1959
|
+
let avdmanagerPath = resolveAndroidTool("avdmanager", sdkRoot);
|
|
1960
|
+
|
|
1961
|
+
if (!sdkRoot || !sdkmanagerPath || !avdmanagerPath) {
|
|
1962
|
+
const toolPaths = await ensureAndroidCommandLineToolsAvailable(sdkRoot);
|
|
1963
|
+
sdkmanagerPath = toolPaths.sdkmanagerPath;
|
|
1964
|
+
avdmanagerPath = toolPaths.avdmanagerPath;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
if (!sdkmanagerPath || !avdmanagerPath) {
|
|
1968
|
+
throw new Error(
|
|
1969
|
+
"No AVD found and sdkmanager/avdmanager is still unavailable after auto-install. Please install Android command-line tools manually.",
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
const googleReachable = await canReachGoogleAndroidRepo();
|
|
1974
|
+
if (!googleReachable) {
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
"Unable to connect to Google Android repository (dl.google.com). Please manually download/install Android emulator and SDK, then rerun setup-android-env.",
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
try {
|
|
1981
|
+
const sizeMap = await fetchAndroidPackageSizeMap();
|
|
1982
|
+
printEmulatorDownloadEstimate(
|
|
1983
|
+
sizeMap,
|
|
1984
|
+
getPreferredSystemImageCandidates(),
|
|
1985
|
+
);
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
console.log(
|
|
1988
|
+
`Unable to fetch package size metadata: ${error.message}. Continue with sdkmanager download.`,
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
console.log("Installing required Android SDK packages...");
|
|
1993
|
+
ensureAndroidSdkPackages(sdkmanagerPath, sdkRoot, [
|
|
1994
|
+
"platform-tools",
|
|
1995
|
+
"emulator",
|
|
1996
|
+
]);
|
|
1997
|
+
|
|
1998
|
+
const imagePackage = installSystemImagePackage(sdkmanagerPath, sdkRoot);
|
|
1999
|
+
console.log(`Creating AVD: ${avdName}`);
|
|
2000
|
+
createAvd(avdmanagerPath, avdName, imagePackage);
|
|
2001
|
+
avds = listAvds(emulatorPath);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
const targetAvd = avds.includes(avdName) ? avdName : avds[0];
|
|
2005
|
+
console.log(`Starting Android emulator: ${targetAvd}`);
|
|
2006
|
+
startAvd(emulatorPath, targetAvd, args);
|
|
2007
|
+
|
|
2008
|
+
const serial = await waitForAdbDevice(adbPath, 180000);
|
|
2009
|
+
await waitForBootComplete(adbPath, serial, 180000);
|
|
2010
|
+
console.log(`Emulator ready: ${serial}`);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
deviceState = listConnectedDevices(adbPath);
|
|
2014
|
+
if (deviceState.devices.length === 0) {
|
|
2015
|
+
throw new Error(
|
|
2016
|
+
"No Android device is connected. Connect a TV/box via adb or enable --auto-emulator.",
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
const deviceSerial = pickPrimaryDeviceSerial(deviceState);
|
|
2021
|
+
const targetSerial = preferredRealDeviceSerial || deviceSerial;
|
|
2022
|
+
if (!targetSerial) {
|
|
2023
|
+
throw new Error("Unable to resolve target adb device serial.");
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
let hostIp = getLocalIPv4Address();
|
|
2027
|
+
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
|
+
}
|
|
2045
|
+
} else {
|
|
2046
|
+
console.log("Skip runtime setup due to --skip-runtime-setup.");
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
console.log(
|
|
2050
|
+
`Android environment is ready for project: ${projectRoot}, device=${targetSerial}`,
|
|
2051
|
+
);
|
|
2052
|
+
return {
|
|
2053
|
+
projectRoot,
|
|
2054
|
+
adbPath,
|
|
2055
|
+
deviceSerial: targetSerial,
|
|
2056
|
+
hostIp,
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
async function runRunDev(args) {
|
|
2061
|
+
const projectRoot = args.project ? path.resolve(args.project) : process.cwd();
|
|
2062
|
+
if (!exists(path.join(projectRoot, "package.json"))) {
|
|
2063
|
+
throw new Error(`Missing package.json in project root: ${projectRoot}`);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
let setupResult = null;
|
|
2067
|
+
if (!toBooleanFlag(args["skip-env-check"], false)) {
|
|
2068
|
+
setupResult = await runSetupAndroidEnv({
|
|
2069
|
+
...args,
|
|
2070
|
+
project: projectRoot,
|
|
2071
|
+
"skip-runtime-setup": args["skip-runtime-setup"],
|
|
2072
|
+
});
|
|
2073
|
+
} else {
|
|
2074
|
+
console.log("Skip Android environment check due to --skip-env-check.");
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const command = resolvePackageManagerCommand(projectRoot);
|
|
2078
|
+
const autoLoadLocalBundle = toBooleanFlag(
|
|
2079
|
+
args["auto-load-local-bundle"],
|
|
2080
|
+
true,
|
|
2081
|
+
);
|
|
2082
|
+
const serverPort = Number(args.port || DEFAULT_DEV_SERVER_PORT);
|
|
2083
|
+
|
|
2084
|
+
console.log(
|
|
2085
|
+
`Starting dev server with '${command.command} ${command.args.join(" ")}'...`,
|
|
2086
|
+
);
|
|
2087
|
+
const child = spawn(command.command, command.args, {
|
|
2088
|
+
cwd: projectRoot,
|
|
2089
|
+
stdio: "inherit",
|
|
2090
|
+
shell: process.platform === "win32",
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
if (autoLoadLocalBundle) {
|
|
2094
|
+
try {
|
|
2095
|
+
const adbPath =
|
|
2096
|
+
(setupResult && setupResult.adbPath) ||
|
|
2097
|
+
resolveAndroidTool("adb", resolveAndroidSdkRoot().sdkRoot);
|
|
2098
|
+
if (adbPath) {
|
|
2099
|
+
const serial =
|
|
2100
|
+
(setupResult && setupResult.deviceSerial) ||
|
|
2101
|
+
pickPrimaryDeviceSerial(listConnectedDevices(adbPath));
|
|
2102
|
+
const hostIp =
|
|
2103
|
+
(setupResult && setupResult.hostIp) ||
|
|
2104
|
+
(typeof args["server-host"] === "string"
|
|
2105
|
+
? args["server-host"]
|
|
2106
|
+
: "") ||
|
|
2107
|
+
getLocalIPv4Address();
|
|
2108
|
+
|
|
2109
|
+
if (serial && hostIp) {
|
|
2110
|
+
console.log(`Waiting for local dev server on port ${serverPort}...`);
|
|
2111
|
+
await waitForPort("127.0.0.1", serverPort, 150000);
|
|
2112
|
+
await autoLoadLocalBundleOnRuntime(
|
|
2113
|
+
adbPath,
|
|
2114
|
+
serial,
|
|
2115
|
+
projectRoot,
|
|
2116
|
+
hostIp,
|
|
2117
|
+
serverPort,
|
|
2118
|
+
args,
|
|
2119
|
+
);
|
|
2120
|
+
} else {
|
|
2121
|
+
console.log(
|
|
2122
|
+
"Skip auto local-bundle trigger: missing adb device or local host IP.",
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
} else {
|
|
2126
|
+
console.log("Skip auto local-bundle trigger: adb is unavailable.");
|
|
2127
|
+
}
|
|
2128
|
+
} catch (error) {
|
|
2129
|
+
console.log(`Auto local-bundle trigger failed: ${error.message}`);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
await new Promise((resolve, reject) => {
|
|
2134
|
+
child.on("error", reject);
|
|
2135
|
+
child.on("exit", (code, signal) => {
|
|
2136
|
+
if (code === 0 || signal === "SIGINT" || signal === "SIGTERM") {
|
|
2137
|
+
resolve(true);
|
|
2138
|
+
} else {
|
|
2139
|
+
reject(
|
|
2140
|
+
new Error(`Dev server exited with code ${code}, signal ${signal}`),
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
async function runRunEsapp(args) {
|
|
2148
|
+
const projectRoot = args.project ? path.resolve(args.project) : process.cwd();
|
|
2149
|
+
let setupResult = null;
|
|
2150
|
+
if (!toBooleanFlag(args["skip-env-check"], false)) {
|
|
2151
|
+
setupResult = await runSetupAndroidEnv({
|
|
2152
|
+
...args,
|
|
2153
|
+
project: projectRoot,
|
|
2154
|
+
"skip-runtime-setup": args["skip-runtime-setup"],
|
|
2155
|
+
});
|
|
2156
|
+
} else {
|
|
2157
|
+
console.log("Skip Android environment check due to --skip-env-check.");
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
const sdkState = resolveAndroidSdkRootForSetup();
|
|
2161
|
+
const adbPath =
|
|
2162
|
+
(setupResult && setupResult.adbPath) ||
|
|
2163
|
+
(await resolveAdbPathForSetup(args, sdkState.sdkRoot));
|
|
2164
|
+
if (!adbPath) {
|
|
2165
|
+
throw new Error("adb is unavailable for run-esapp.");
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
let serial = typeof args.device === "string" ? args.device.trim() : "";
|
|
2169
|
+
if (!serial && setupResult && setupResult.deviceSerial) {
|
|
2170
|
+
serial = setupResult.deviceSerial;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
if (
|
|
2174
|
+
!serial &&
|
|
2175
|
+
typeof args["device-ip"] === "string" &&
|
|
2176
|
+
args["device-ip"].trim()
|
|
2177
|
+
) {
|
|
2178
|
+
const endpoint = normalizeDeviceEndpoint(args["device-ip"]);
|
|
2179
|
+
console.log(`Trying to connect device: ${endpoint}`);
|
|
2180
|
+
const connectResult = tryAdbConnect(adbPath, endpoint);
|
|
2181
|
+
if (!connectResult.connected) {
|
|
2182
|
+
throw new Error(
|
|
2183
|
+
`Failed to connect device ${endpoint}: ${
|
|
2184
|
+
connectResult.output || "unknown adb connect error"
|
|
2185
|
+
}`,
|
|
2186
|
+
);
|
|
39
2187
|
}
|
|
2188
|
+
await delay(600);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
if (!serial) {
|
|
2192
|
+
const state = listConnectedDevices(adbPath);
|
|
2193
|
+
serial = pickPrimaryDeviceSerial(state);
|
|
40
2194
|
}
|
|
2195
|
+
if (!serial) {
|
|
2196
|
+
throw new Error(
|
|
2197
|
+
"No Android device available. Use --device/--device-ip or run setup-android-env first.",
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
const launchUri = buildEsappLaunchUri(args, projectRoot, {
|
|
2202
|
+
pkg: resolveProjectAppPackage(projectRoot),
|
|
2203
|
+
from: "cmd",
|
|
2204
|
+
});
|
|
2205
|
+
const runtimePackage =
|
|
2206
|
+
typeof args["runtime-package"] === "string" &&
|
|
2207
|
+
args["runtime-package"].trim()
|
|
2208
|
+
? args["runtime-package"].trim()
|
|
2209
|
+
: RUNTIME_PACKAGE_NAME;
|
|
2210
|
+
|
|
2211
|
+
console.log("Launching ES app via protocol...");
|
|
2212
|
+
console.log(`- device: ${serial}`);
|
|
2213
|
+
console.log(`- runtime: ${runtimePackage}`);
|
|
2214
|
+
console.log(`- uri: ${launchUri}`);
|
|
2215
|
+
startEsappOnRuntimeByUri(adbPath, serial, runtimePackage, launchUri);
|
|
2216
|
+
console.log("ES app launch command sent.");
|
|
41
2217
|
}
|
|
42
2218
|
|
|
43
2219
|
function resolveSkillsSource() {
|
|
@@ -97,6 +2273,16 @@ function parseArgs(argv) {
|
|
|
97
2273
|
return args;
|
|
98
2274
|
}
|
|
99
2275
|
|
|
2276
|
+
function toBooleanFlag(value, defaultValue) {
|
|
2277
|
+
if (typeof value === "undefined") return defaultValue;
|
|
2278
|
+
if (typeof value === "boolean") return value;
|
|
2279
|
+
|
|
2280
|
+
const normalized = String(value).trim().toLowerCase();
|
|
2281
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
2282
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
2283
|
+
return defaultValue;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
100
2286
|
function printHelp() {
|
|
101
2287
|
console.log(`quicktvui-ai v${PACKAGE_VERSION}
|
|
102
2288
|
|
|
@@ -108,14 +2294,61 @@ Commands:
|
|
|
108
2294
|
doctor Check whether skills and local QuickTVUI rules are available
|
|
109
2295
|
validate Strict check for required skill files (non-zero exit if missing)
|
|
110
2296
|
update Reinstall skill assets to target directory
|
|
2297
|
+
create-project Create a QuickTVUI project (remote clone, local fallback)
|
|
2298
|
+
setup-vue-env Setup Vue development environment (Node/yarn/quicktvui cli)
|
|
2299
|
+
setup-android-env Configure Android device/emulator + runtime environment
|
|
2300
|
+
setup-all-env Setup both Vue and Android development environments
|
|
2301
|
+
run-dev Run project dev command (optionally checks Android env first)
|
|
2302
|
+
run-esapp Launch an ES app on runtime via esapp:// protocol
|
|
111
2303
|
prompt Print an LLM-ready installation prompt block
|
|
112
2304
|
help Show this message
|
|
113
2305
|
|
|
114
2306
|
Options:
|
|
115
2307
|
--dir <path> Custom skill installation directory
|
|
116
2308
|
--project <path> Project root for @quicktvui/ai checks (default: current dir)
|
|
2309
|
+
--dest <path> Destination base directory for create-project
|
|
2310
|
+
--offline Skip git clone and use bundled template directly
|
|
2311
|
+
--skip-install Skip dependency install in create-project
|
|
117
2312
|
--strict Non-zero exit in doctor when checks fail
|
|
118
2313
|
--lang <code> Prompt language for 'prompt' command: zh | en
|
|
2314
|
+
--gemini-dir <path> Gemini config directory (default: ~/.gemini or $GEMINI_CLI_HOME/.gemini)
|
|
2315
|
+
--skip-gemini-config Skip updating Gemini bridge files during init/update
|
|
2316
|
+
--yes Non-interactive mode; accept default prompts
|
|
2317
|
+
--no-interactive Disable interactive prompts
|
|
2318
|
+
--node-major <n> Target Node.js LTS major for setup-vue-env (default: 20)
|
|
2319
|
+
--skip-node-install Skip Node.js auto-install stage in setup-vue-env
|
|
2320
|
+
--force-node-install Force Node.js auto-install even if current version is ok
|
|
2321
|
+
--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
|
+
--skip-project-install Skip project dependency install in setup-vue-env
|
|
2324
|
+
--auto-emulator <true|false> Auto create/start emulator when no adb device
|
|
2325
|
+
--adb-path <path> Custom adb binary path/command (or set QUICKTVUI_ADB_PATH)
|
|
2326
|
+
--device-ip <ip[:port]> Preferred real device endpoint for adb connect
|
|
2327
|
+
--avd-name <name> Custom AVD name for setup-android-env
|
|
2328
|
+
--headless Start emulator with -no-window -no-audio
|
|
2329
|
+
--runtime-setup-mode <direct|qui> Runtime setup mode in setup-android-env (default: direct)
|
|
2330
|
+
--runtime-version <version> Pin runtime version when direct mode is used
|
|
2331
|
+
--runtime-url <url> Use custom runtime apk url in direct mode
|
|
2332
|
+
--server-host <ip> Override local debug server host IP
|
|
2333
|
+
--force-runtime-install Force reinstall runtime apk in direct mode
|
|
2334
|
+
--skip-runtime-setup Skip runtime setup stage in setup-android-env/run-dev
|
|
2335
|
+
--auto-load-local-bundle <true|false> Try auto trigger runtime to load local bundle in run-dev
|
|
2336
|
+
--port <n> Dev server port used by run-dev auto load (default: 38989)
|
|
2337
|
+
--skip-env-check Skip setup-android-env stage in run-dev
|
|
2338
|
+
--runtime-package <pkg> Runtime package name for run-esapp (default: com.extscreen.runtime)
|
|
2339
|
+
--device <serial> Target adb device serial for run-esapp
|
|
2340
|
+
--esapp-uri <uri> Raw esapp:// / quicktv:// / appcast:// URI for run-esapp
|
|
2341
|
+
--esapp-query <json> Extra query params JSON merged into action/start URI
|
|
2342
|
+
--pkg <pkg> ES app package for run-esapp structured mode
|
|
2343
|
+
--ver <version> ES app version for run-esapp structured mode
|
|
2344
|
+
--min-ver <ver> ES app minVer for run-esapp structured mode
|
|
2345
|
+
--repository <url> Repository URL for run-esapp structured mode
|
|
2346
|
+
--uri <uri> ES app load URI (local/remote/assets/file) for run-esapp structured mode
|
|
2347
|
+
--from <from> Caller marker for run-esapp (default: cmd)
|
|
2348
|
+
--args <json> Startup args JSON string for run-esapp
|
|
2349
|
+
--exp <json> Startup exp JSON string for run-esapp
|
|
2350
|
+
--flags <n> Startup flags for run-esapp (supports old 100/200/300/400 mapping)
|
|
2351
|
+
--use-latest <true|false> Pass useLatest for run-esapp
|
|
119
2352
|
`);
|
|
120
2353
|
}
|
|
121
2354
|
|
|
@@ -193,6 +2426,191 @@ function installSkills(installDir) {
|
|
|
193
2426
|
return { installDir, sourceDir };
|
|
194
2427
|
}
|
|
195
2428
|
|
|
2429
|
+
function resolveGeminiConfigDir(args) {
|
|
2430
|
+
if (typeof args["gemini-dir"] === "string" && args["gemini-dir"].trim()) {
|
|
2431
|
+
return path.resolve(args["gemini-dir"].trim());
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
const customGeminiHome = process.env.GEMINI_CLI_HOME
|
|
2435
|
+
? String(process.env.GEMINI_CLI_HOME).trim()
|
|
2436
|
+
: "";
|
|
2437
|
+
if (customGeminiHome) {
|
|
2438
|
+
const resolved = path.resolve(customGeminiHome);
|
|
2439
|
+
if (path.basename(resolved).toLowerCase() === ".gemini") {
|
|
2440
|
+
return resolved;
|
|
2441
|
+
}
|
|
2442
|
+
return path.join(resolved, ".gemini");
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
return path.join(os.homedir(), ".gemini");
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function buildGeminiBridgeImportPaths(installDir) {
|
|
2449
|
+
const preferredFiles = [
|
|
2450
|
+
"SKILL.md",
|
|
2451
|
+
path.join("references", "create-project-checklist.md"),
|
|
2452
|
+
path.join("references", "dev-env-checklist.md"),
|
|
2453
|
+
path.join("references", "esapp-protocol-cheatsheet.md"),
|
|
2454
|
+
];
|
|
2455
|
+
|
|
2456
|
+
const importPaths = [];
|
|
2457
|
+
for (const relPath of preferredFiles) {
|
|
2458
|
+
const absolutePath = path.join(installDir, relPath);
|
|
2459
|
+
if (exists(absolutePath)) {
|
|
2460
|
+
importPaths.push(absolutePath);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
if (importPaths.length === 0) {
|
|
2465
|
+
importPaths.push(path.join(installDir, "SKILL.md"));
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
return importPaths;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
function updateGeminiBridgeConfig(args, installDir) {
|
|
2472
|
+
if (toBooleanFlag(args["skip-gemini-config"], false)) {
|
|
2473
|
+
return {
|
|
2474
|
+
skipped: true,
|
|
2475
|
+
reason: "flag",
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
const geminiDir = resolveGeminiConfigDir(args);
|
|
2480
|
+
ensureDir(geminiDir);
|
|
2481
|
+
|
|
2482
|
+
const geminiMdPath = path.join(geminiDir, "GEMINI.md");
|
|
2483
|
+
const settingsPath = path.join(geminiDir, "settings.json");
|
|
2484
|
+
const importPaths = buildGeminiBridgeImportPaths(installDir);
|
|
2485
|
+
|
|
2486
|
+
const bridgeBlock = [
|
|
2487
|
+
QUICKTVUI_GEMINI_BRIDGE_START,
|
|
2488
|
+
"QuickTVUI global skill bridge (managed by quicktvui-ai assistant).",
|
|
2489
|
+
...importPaths.map((importPath) => `@${importPath}`),
|
|
2490
|
+
QUICKTVUI_GEMINI_BRIDGE_END,
|
|
2491
|
+
].join("\n");
|
|
2492
|
+
|
|
2493
|
+
const beforeGeminiMd = exists(geminiMdPath)
|
|
2494
|
+
? fs.readFileSync(geminiMdPath, "utf8")
|
|
2495
|
+
: "";
|
|
2496
|
+
const bridgePattern = new RegExp(
|
|
2497
|
+
`${escapeRegExp(QUICKTVUI_GEMINI_BRIDGE_START)}[\\s\\S]*?${escapeRegExp(QUICKTVUI_GEMINI_BRIDGE_END)}`,
|
|
2498
|
+
"m",
|
|
2499
|
+
);
|
|
2500
|
+
let nextGeminiMd;
|
|
2501
|
+
if (bridgePattern.test(beforeGeminiMd)) {
|
|
2502
|
+
nextGeminiMd = beforeGeminiMd.replace(bridgePattern, bridgeBlock);
|
|
2503
|
+
} else {
|
|
2504
|
+
const trimmed = beforeGeminiMd.replace(/\s*$/, "");
|
|
2505
|
+
nextGeminiMd = trimmed
|
|
2506
|
+
? `${trimmed}\n\n${bridgeBlock}\n`
|
|
2507
|
+
: `${bridgeBlock}\n`;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
let geminiMdUpdated = false;
|
|
2511
|
+
let geminiMdBackup = null;
|
|
2512
|
+
if (nextGeminiMd !== beforeGeminiMd) {
|
|
2513
|
+
geminiMdBackup = backupFile(geminiMdPath);
|
|
2514
|
+
fs.writeFileSync(geminiMdPath, nextGeminiMd, "utf8");
|
|
2515
|
+
geminiMdUpdated = true;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
const result = {
|
|
2519
|
+
skipped: false,
|
|
2520
|
+
geminiDir,
|
|
2521
|
+
geminiMdPath,
|
|
2522
|
+
settingsPath,
|
|
2523
|
+
importPaths,
|
|
2524
|
+
geminiMdUpdated,
|
|
2525
|
+
geminiMdBackup,
|
|
2526
|
+
settingsUpdated: false,
|
|
2527
|
+
settingsBackup: null,
|
|
2528
|
+
settingsParseError: null,
|
|
2529
|
+
};
|
|
2530
|
+
|
|
2531
|
+
let settings = {};
|
|
2532
|
+
const beforeSettings = exists(settingsPath)
|
|
2533
|
+
? fs.readFileSync(settingsPath, "utf8")
|
|
2534
|
+
: "";
|
|
2535
|
+
if (beforeSettings.trim()) {
|
|
2536
|
+
try {
|
|
2537
|
+
settings = JSON.parse(beforeSettings);
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
result.settingsParseError = error.message;
|
|
2540
|
+
return result;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
2545
|
+
settings = {};
|
|
2546
|
+
}
|
|
2547
|
+
if (!settings.context || typeof settings.context !== "object") {
|
|
2548
|
+
settings.context = {};
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
const current = settings.context.fileName;
|
|
2552
|
+
const normalizedList = Array.isArray(current)
|
|
2553
|
+
? current
|
|
2554
|
+
.map((item) => String(item).trim())
|
|
2555
|
+
.filter((item) => item.length > 0)
|
|
2556
|
+
: typeof current === "string" && current.trim()
|
|
2557
|
+
? [current.trim()]
|
|
2558
|
+
: [];
|
|
2559
|
+
|
|
2560
|
+
const lowerSet = new Set(normalizedList.map((item) => item.toLowerCase()));
|
|
2561
|
+
for (const fileName of DEFAULT_GEMINI_CONTEXT_FILENAMES) {
|
|
2562
|
+
if (!lowerSet.has(fileName.toLowerCase())) {
|
|
2563
|
+
normalizedList.push(fileName);
|
|
2564
|
+
lowerSet.add(fileName.toLowerCase());
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
settings.context.fileName = normalizedList;
|
|
2568
|
+
|
|
2569
|
+
const nextSettings = `${JSON.stringify(settings, null, 2)}\n`;
|
|
2570
|
+
if (nextSettings !== beforeSettings) {
|
|
2571
|
+
result.settingsBackup = backupFile(settingsPath);
|
|
2572
|
+
fs.writeFileSync(settingsPath, nextSettings, "utf8");
|
|
2573
|
+
result.settingsUpdated = true;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
return result;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
function printGeminiBridgeResult(state) {
|
|
2580
|
+
if (!state) return;
|
|
2581
|
+
if (state.error) {
|
|
2582
|
+
console.log(`- Gemini config: failed (${state.error})`);
|
|
2583
|
+
console.log(
|
|
2584
|
+
` Hint: rerun with --skip-gemini-config or fix filesystem permission.`,
|
|
2585
|
+
);
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
if (state.skipped) {
|
|
2589
|
+
console.log(`- Gemini config: skipped (--skip-gemini-config)`);
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
console.log(`- Gemini config dir: ${state.geminiDir}`);
|
|
2594
|
+
console.log(
|
|
2595
|
+
`- Gemini bridge block: ${state.geminiMdUpdated ? "updated" : "already up to date"}`,
|
|
2596
|
+
);
|
|
2597
|
+
console.log(
|
|
2598
|
+
`- Gemini context.fileName: ${
|
|
2599
|
+
state.settingsParseError
|
|
2600
|
+
? "skipped (settings.json parse failed)"
|
|
2601
|
+
: state.settingsUpdated
|
|
2602
|
+
? "updated"
|
|
2603
|
+
: "already up to date"
|
|
2604
|
+
}`,
|
|
2605
|
+
);
|
|
2606
|
+
if (state.settingsParseError) {
|
|
2607
|
+
console.log(` Warning: ${state.settingsParseError}`);
|
|
2608
|
+
console.log(
|
|
2609
|
+
` Hint: fix ${state.settingsPath} JSON and rerun 'quicktvui-ai init'.`,
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
196
2614
|
function printDoctorReport(skillState, projectState, projectRoot) {
|
|
197
2615
|
console.log(`Skill dir: ${skillState.installDir}`);
|
|
198
2616
|
console.log(`- Directory exists: ${skillState.exists ? "yes" : "no"}`);
|
|
@@ -220,9 +2638,16 @@ function printDoctorReport(skillState, projectState, projectRoot) {
|
|
|
220
2638
|
async function runInit(args) {
|
|
221
2639
|
const installDir = getInstallDir(args);
|
|
222
2640
|
const result = installSkills(installDir);
|
|
2641
|
+
let geminiState;
|
|
2642
|
+
try {
|
|
2643
|
+
geminiState = updateGeminiBridgeConfig(args, result.installDir);
|
|
2644
|
+
} catch (error) {
|
|
2645
|
+
geminiState = { error: error.message };
|
|
2646
|
+
}
|
|
223
2647
|
console.log(`Installed QuickTVUI skill assets.`);
|
|
224
2648
|
console.log(`- source: ${result.sourceDir}`);
|
|
225
2649
|
console.log(`- target: ${result.installDir}`);
|
|
2650
|
+
printGeminiBridgeResult(geminiState);
|
|
226
2651
|
console.log(`Next: restart/reload your AI agent so it can rescan skills.`);
|
|
227
2652
|
}
|
|
228
2653
|
|
|
@@ -276,9 +2701,78 @@ async function runValidate(args) {
|
|
|
276
2701
|
async function runUpdate(args) {
|
|
277
2702
|
const installDir = getInstallDir(args);
|
|
278
2703
|
const result = installSkills(installDir);
|
|
2704
|
+
let geminiState;
|
|
2705
|
+
try {
|
|
2706
|
+
geminiState = updateGeminiBridgeConfig(args, result.installDir);
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
geminiState = { error: error.message };
|
|
2709
|
+
}
|
|
279
2710
|
console.log(`Updated QuickTVUI skill assets.`);
|
|
280
2711
|
console.log(`- source: ${result.sourceDir}`);
|
|
281
2712
|
console.log(`- target: ${result.installDir}`);
|
|
2713
|
+
printGeminiBridgeResult(geminiState);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function cloneTemplateProject(targetDir) {
|
|
2717
|
+
runCommand("git", ["clone", "--depth", "1", TEMPLATE_REPO_URL, targetDir]);
|
|
2718
|
+
removeDirectoryIfExists(path.join(targetDir, ".git"));
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
function copyBundledTemplateProject(targetDir) {
|
|
2722
|
+
const templateSource = resolveBundledTemplateSource();
|
|
2723
|
+
copyDirectoryRecursive(templateSource, targetDir);
|
|
2724
|
+
removeDirectoryIfExists(path.join(targetDir, ".git"));
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
async function runCreateProject(args) {
|
|
2728
|
+
const projectName = args._[1];
|
|
2729
|
+
if (!projectName) {
|
|
2730
|
+
throw new Error(
|
|
2731
|
+
"Missing project name. Usage: quicktvui-ai create-project <project-name>",
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
const destinationBase = args.dest ? path.resolve(args.dest) : process.cwd();
|
|
2736
|
+
ensureDir(destinationBase);
|
|
2737
|
+
|
|
2738
|
+
const targetDir = path.join(destinationBase, projectName);
|
|
2739
|
+
if (exists(targetDir) && !isDirectoryEmpty(targetDir)) {
|
|
2740
|
+
throw new Error(`Target directory is not empty: ${targetDir}`);
|
|
2741
|
+
}
|
|
2742
|
+
ensureDir(targetDir);
|
|
2743
|
+
|
|
2744
|
+
let usedSource = "bundled-template";
|
|
2745
|
+
let cloneError;
|
|
2746
|
+
|
|
2747
|
+
if (!args.offline) {
|
|
2748
|
+
try {
|
|
2749
|
+
cloneTemplateProject(targetDir);
|
|
2750
|
+
usedSource = "git-clone";
|
|
2751
|
+
} catch (error) {
|
|
2752
|
+
cloneError = error;
|
|
2753
|
+
removeDirectoryIfExists(targetDir);
|
|
2754
|
+
ensureDir(targetDir);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
if (usedSource !== "git-clone") {
|
|
2759
|
+
copyBundledTemplateProject(targetDir);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
ensureProjectPackageRules(targetDir, projectName);
|
|
2763
|
+
installProjectDependencies(targetDir, args);
|
|
2764
|
+
|
|
2765
|
+
console.log(`Created QuickTVUI project: ${targetDir}`);
|
|
2766
|
+
console.log(`- source: ${usedSource}`);
|
|
2767
|
+
if (cloneError) {
|
|
2768
|
+
console.log(`- clone fallback reason: ${cloneError.message}`);
|
|
2769
|
+
}
|
|
2770
|
+
console.log(
|
|
2771
|
+
"- ensured: package.json name/version updated and @quicktvui/ai added to devDependencies",
|
|
2772
|
+
);
|
|
2773
|
+
console.log(
|
|
2774
|
+
"- next: if not configured, run 'npm install -g @quicktvui/cli@latest' and 'qui setup'",
|
|
2775
|
+
);
|
|
282
2776
|
}
|
|
283
2777
|
|
|
284
2778
|
function buildPrompt(lang) {
|
|
@@ -300,7 +2794,24 @@ quicktvui-ai doctor
|
|
|
300
2794
|
npm install @quicktvui/ai --save-dev
|
|
301
2795
|
\`\`\`
|
|
302
2796
|
|
|
303
|
-
安装完成后请提醒用户重启或 reload AI Agent,以便重新扫描 ~/.agents/skills/quicktvui
|
|
2797
|
+
安装完成后请提醒用户重启或 reload AI Agent,以便重新扫描 ~/.agents/skills/quicktvui。
|
|
2798
|
+
并提示:quicktvui-ai init 会自动更新 ~/.gemini/GEMINI.md 与 ~/.gemini/settings.json 的 QuickTVUI bridge 配置。
|
|
2799
|
+
|
|
2800
|
+
创建项目时优先使用:
|
|
2801
|
+
\`\`\`bash
|
|
2802
|
+
quicktvui-ai create-project <project-name>
|
|
2803
|
+
\`\`\`
|
|
2804
|
+
该命令在 git clone 失败时会自动回退到本地内置模板。
|
|
2805
|
+
|
|
2806
|
+
项目创建后可执行:
|
|
2807
|
+
\`\`\`bash
|
|
2808
|
+
quicktvui-ai setup-vue-env --project <project-path>
|
|
2809
|
+
quicktvui-ai setup-android-env --project <project-path>
|
|
2810
|
+
quicktvui-ai run-dev --project <project-path>
|
|
2811
|
+
quicktvui-ai run-esapp --project <project-path> --pkg <es-package>
|
|
2812
|
+
# 或一键
|
|
2813
|
+
quicktvui-ai setup-all-env --project <project-path>
|
|
2814
|
+
\`\`\``;
|
|
304
2815
|
}
|
|
305
2816
|
|
|
306
2817
|
return `# Install QuickTVUI AI Skill Runtime
|
|
@@ -319,7 +2830,24 @@ If the user is inside a QuickTVUI project, also run:
|
|
|
319
2830
|
npm install @quicktvui/ai --save-dev
|
|
320
2831
|
\`\`\`
|
|
321
2832
|
|
|
322
|
-
After installation, ask the user to restart or reload the AI Agent so it rescans ~/.agents/skills/quicktvui
|
|
2833
|
+
After installation, ask the user to restart or reload the AI Agent so it rescans ~/.agents/skills/quicktvui.
|
|
2834
|
+
Also note that quicktvui-ai init auto-updates QuickTVUI bridge config in ~/.gemini/GEMINI.md and ~/.gemini/settings.json.
|
|
2835
|
+
|
|
2836
|
+
When creating a project, prefer:
|
|
2837
|
+
\`\`\`bash
|
|
2838
|
+
quicktvui-ai create-project <project-name>
|
|
2839
|
+
\`\`\`
|
|
2840
|
+
This command auto-fallbacks to bundled local template when git clone fails.
|
|
2841
|
+
|
|
2842
|
+
After project creation, you can run:
|
|
2843
|
+
\`\`\`bash
|
|
2844
|
+
quicktvui-ai setup-vue-env --project <project-path>
|
|
2845
|
+
quicktvui-ai setup-android-env --project <project-path>
|
|
2846
|
+
quicktvui-ai run-dev --project <project-path>
|
|
2847
|
+
quicktvui-ai run-esapp --project <project-path> --pkg <es-package>
|
|
2848
|
+
# or one command
|
|
2849
|
+
quicktvui-ai setup-all-env --project <project-path>
|
|
2850
|
+
\`\`\``;
|
|
323
2851
|
}
|
|
324
2852
|
|
|
325
2853
|
async function runPrompt(args) {
|
|
@@ -340,6 +2868,18 @@ async function runCli(argv) {
|
|
|
340
2868
|
return runValidate(args);
|
|
341
2869
|
case "update":
|
|
342
2870
|
return runUpdate(args);
|
|
2871
|
+
case "create-project":
|
|
2872
|
+
return runCreateProject(args);
|
|
2873
|
+
case "setup-vue-env":
|
|
2874
|
+
return runSetupVueEnv(args);
|
|
2875
|
+
case "setup-android-env":
|
|
2876
|
+
return runSetupAndroidEnv(args);
|
|
2877
|
+
case "setup-all-env":
|
|
2878
|
+
return runSetupAllEnv(args);
|
|
2879
|
+
case "run-dev":
|
|
2880
|
+
return runRunDev(args);
|
|
2881
|
+
case "run-esapp":
|
|
2882
|
+
return runRunEsapp(args);
|
|
343
2883
|
case "prompt":
|
|
344
2884
|
return runPrompt(args);
|
|
345
2885
|
case "help":
|