weapp-ide-cli 6.1.9 → 6.3.0

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.
@@ -0,0 +1,822 @@
1
+ import { a as readPersistedAutomatorSession, i as persistAutomatorSession, j as colors, m as assertWechatDevtoolsPort, p as assertWechatDevtoolsHost, r as resolveAutomatorProjectPath, u as resolveWechatDevtoolsTarget, z as readCustomConfig } from "./automatorProject-DjYLqMj6.js";
2
+ import path from "node:path";
3
+ import { withMachineE2ELease } from "@weapp-vite/devtools-runtime";
4
+ import fs from "node:fs/promises";
5
+ import process from "node:process";
6
+ import { createHash } from "node:crypto";
7
+ import { OperationLifecycle } from "@weapp-vite/miniprogram-automator/operation";
8
+ import { emitKeypressEvents } from "node:readline";
9
+ //#region src/cli/automator/port.ts
10
+ /** 显式端口不能被截断或作为默认值忽略,避免连接其他项目。 */
11
+ function assertAutomatorPort(port) {
12
+ if (port !== void 0 && (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)) throw Object.assign(/* @__PURE__ */ new RangeError("Automator port must be an integer between 1 and 65535."), { code: "DEVTOOLS_INVALID_PORT" });
13
+ }
14
+ /** CLI 只接受完整十进制数字,缺失值、尾随字符和指数写法均报错。 */
15
+ function parseAutomatorPort(raw) {
16
+ const port = raw !== void 0 && /^\d+$/.test(raw) ? Number(raw) : NaN;
17
+ assertAutomatorPort(port);
18
+ return port;
19
+ }
20
+ //#endregion
21
+ //#region src/cli/automator/context.ts
22
+ /** 在连接与共享缓存入口固定安装选择;headless 不读取或修改 IDE 状态。 */
23
+ async function resolveAutomatorSessionOptions(options) {
24
+ assertAutomatorPort(options.port);
25
+ options.signal?.throwIfAborted();
26
+ const runtimeProvider = options.runtimeProvider ?? process.env.WEAPP_VITE_AUTOMATOR_RUNTIME_PROVIDER ?? process.env.WEAPP_VITE_E2E_RUNTIME_PROVIDER;
27
+ if (runtimeProvider === "headless") return {
28
+ ...options,
29
+ runtimeProvider,
30
+ installationId: "headless"
31
+ };
32
+ const target = await resolveWechatDevtoolsTarget(options);
33
+ options.signal?.throwIfAborted();
34
+ if (options.installationId && options.installationId !== target.installationId) throw Object.assign(/* @__PURE__ */ new Error("DEVTOOLS_INSTALLATION_MISMATCH: resolved installation does not match the selected session."), { code: "DEVTOOLS_INSTALLATION_MISMATCH" });
35
+ return {
36
+ ...options,
37
+ target,
38
+ cliPath: target.cliPath,
39
+ installationId: target.installationId
40
+ };
41
+ }
42
+ //#endregion
43
+ //#region src/i18n.ts
44
+ let currentLocale = "zh";
45
+ /**
46
+ * @description 设置当前语言。
47
+ */
48
+ function setLocale(locale) {
49
+ currentLocale = normalizeLocale(locale);
50
+ }
51
+ /**
52
+ * @description 从 argv 和环境变量中解析语言并设置。
53
+ */
54
+ function configureLocaleFromArgv(argv, fallbackLocale) {
55
+ const localeFromArgv = readLangOption(argv);
56
+ if (localeFromArgv) {
57
+ setLocale(localeFromArgv);
58
+ return currentLocale;
59
+ }
60
+ if (fallbackLocale) {
61
+ setLocale(fallbackLocale);
62
+ return currentLocale;
63
+ }
64
+ const localeFromEnv = process.env.WEAPP_IDE_CLI_LANG || process.env.LANG;
65
+ if (localeFromEnv) {
66
+ setLocale(localeFromEnv);
67
+ return currentLocale;
68
+ }
69
+ setLocale("zh");
70
+ return currentLocale;
71
+ }
72
+ /**
73
+ * @description 根据当前语言选择文案。
74
+ */
75
+ function i18nText(zh, en) {
76
+ return currentLocale === "en" ? en : zh;
77
+ }
78
+ /**
79
+ * @description 校验 --lang 参数是否合法。
80
+ */
81
+ function validateLocaleOption(argv) {
82
+ const localeFromArgv = readLangOption(argv);
83
+ if (!localeFromArgv) return;
84
+ if (!isLocaleTokenSupported(localeFromArgv)) throw new Error(`不支持的语言: ${localeFromArgv},仅支持 zh 或 en`);
85
+ }
86
+ function normalizeLocale(raw) {
87
+ if (!raw) return "zh";
88
+ const normalized = raw.trim().toLowerCase();
89
+ if (normalized === "en" || normalized.startsWith("en_") || normalized.startsWith("en-")) return "en";
90
+ return "zh";
91
+ }
92
+ function isLocaleTokenSupported(raw) {
93
+ const normalized = raw.trim().toLowerCase();
94
+ return normalized === "zh" || normalized === "en" || normalized.startsWith("zh_") || normalized.startsWith("zh-") || normalized.startsWith("en_") || normalized.startsWith("en-");
95
+ }
96
+ function readLangOption(argv) {
97
+ for (let index = 0; index < argv.length; index += 1) {
98
+ const token = argv[index];
99
+ if (!token) continue;
100
+ if (token === "--lang") {
101
+ const value = argv[index + 1];
102
+ return typeof value === "string" ? value : void 0;
103
+ }
104
+ if (token.startsWith("--lang=")) return token.slice(7);
105
+ }
106
+ }
107
+ //#endregion
108
+ //#region src/cli/inputCoordinator.ts
109
+ const sharedSessions = /* @__PURE__ */ new Set();
110
+ const exclusiveKeypressStack = [];
111
+ const EXCLUSIVE_KEYPRESS_CARRYOVER_GUARD_MS = 500;
112
+ let initialized = false;
113
+ let rawModeEnabled = false;
114
+ let dataListenerAttached = false;
115
+ let keypressListenerAttached = false;
116
+ let lastExclusiveResolvedKeypress;
117
+ function hasInteractiveStdin() {
118
+ return Boolean(process.stdin?.isTTY);
119
+ }
120
+ function canSetRawMode() {
121
+ return typeof process.stdin?.setRawMode === "function";
122
+ }
123
+ function updateTerminalState() {
124
+ const shouldKeepInputActive = Array.from(sharedSessions).some((session) => !session.closed && !session.suspended) || exclusiveKeypressStack.length > 0;
125
+ if (canSetRawMode() && rawModeEnabled !== shouldKeepInputActive) {
126
+ process.stdin.setRawMode(shouldKeepInputActive);
127
+ rawModeEnabled = shouldKeepInputActive;
128
+ }
129
+ if (shouldKeepInputActive) {
130
+ process.stdin.resume();
131
+ return;
132
+ }
133
+ process.stdin.pause();
134
+ }
135
+ function handleData(chunk) {
136
+ if (exclusiveKeypressStack.length > 0) return;
137
+ for (const session of sharedSessions) {
138
+ if (session.closed || session.suspended || !session.options.onData) continue;
139
+ session.options.onData(chunk);
140
+ }
141
+ }
142
+ function handleKeypress(str, key) {
143
+ const activeExclusive = exclusiveKeypressStack[exclusiveKeypressStack.length - 1];
144
+ if (activeExclusive) {
145
+ const now = Date.now();
146
+ const signature = `${key?.ctrl ? "ctrl+" : ""}${key?.name ?? str}`;
147
+ if (lastExclusiveResolvedKeypress && lastExclusiveResolvedKeypress.signature === signature && now - lastExclusiveResolvedKeypress.timestamp <= EXCLUSIVE_KEYPRESS_CARRYOVER_GUARD_MS) {
148
+ lastExclusiveResolvedKeypress.timestamp = now;
149
+ return;
150
+ }
151
+ if (now < activeExclusive.ignoreUntil) return;
152
+ const nextValue = activeExclusive.onKeypress(str, key);
153
+ if (nextValue !== void 0) {
154
+ clearTimeout(activeExclusive.timeout);
155
+ exclusiveKeypressStack.pop();
156
+ lastExclusiveResolvedKeypress = {
157
+ signature,
158
+ timestamp: now
159
+ };
160
+ updateTerminalState();
161
+ activeExclusive.resolve(nextValue);
162
+ }
163
+ return;
164
+ }
165
+ for (const session of sharedSessions) {
166
+ if (session.closed || session.suspended || !session.options.onKeypress) continue;
167
+ session.options.onKeypress(str, key);
168
+ }
169
+ }
170
+ function ensureCoordinatorInitialized() {
171
+ if (initialized || !hasInteractiveStdin()) return;
172
+ emitKeypressEvents(process.stdin);
173
+ if (!dataListenerAttached) {
174
+ process.stdin.on("data", handleData);
175
+ dataListenerAttached = true;
176
+ }
177
+ if (!keypressListenerAttached) {
178
+ process.stdin.on("keypress", handleKeypress);
179
+ keypressListenerAttached = true;
180
+ }
181
+ initialized = true;
182
+ }
183
+ function createSharedInputSession(options) {
184
+ if (!hasInteractiveStdin()) return;
185
+ ensureCoordinatorInitialized();
186
+ const session = {
187
+ closed: false,
188
+ options,
189
+ suspended: false
190
+ };
191
+ sharedSessions.add(session);
192
+ updateTerminalState();
193
+ return {
194
+ close() {
195
+ if (session.closed) return;
196
+ session.closed = true;
197
+ sharedSessions.delete(session);
198
+ updateTerminalState();
199
+ },
200
+ resume() {
201
+ if (session.closed) return;
202
+ session.suspended = false;
203
+ updateTerminalState();
204
+ },
205
+ suspend() {
206
+ if (session.closed) return;
207
+ session.suspended = true;
208
+ updateTerminalState();
209
+ }
210
+ };
211
+ }
212
+ async function waitForExclusiveKeypress(options) {
213
+ options.signal?.throwIfAborted();
214
+ if (!hasInteractiveStdin()) return "timeout";
215
+ ensureCoordinatorInitialized();
216
+ return await new Promise((resolve, reject) => {
217
+ const normalizedTimeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 3e4;
218
+ const normalizedIgnoreInitialMs = Number.isFinite(options.ignoreInitialMs) && options.ignoreInitialMs && options.ignoreInitialMs > 0 ? options.ignoreInitialMs : 0;
219
+ let onAbort;
220
+ const record = {
221
+ ignoreUntil: Date.now() + normalizedIgnoreInitialMs,
222
+ onKeypress: options.onKeypress,
223
+ resolve: (value) => {
224
+ options.signal?.removeEventListener("abort", onAbort);
225
+ resolve(value);
226
+ },
227
+ timeout: setTimeout(() => {
228
+ const index = exclusiveKeypressStack.lastIndexOf(record);
229
+ if (index >= 0) exclusiveKeypressStack.splice(index, 1);
230
+ updateTerminalState();
231
+ record.resolve("timeout");
232
+ }, normalizedTimeoutMs)
233
+ };
234
+ onAbort = () => {
235
+ clearTimeout(record.timeout);
236
+ options.signal?.removeEventListener("abort", onAbort);
237
+ const index = exclusiveKeypressStack.indexOf(record);
238
+ if (index >= 0) exclusiveKeypressStack.splice(index, 1);
239
+ updateTerminalState();
240
+ reject(options.signal?.reason);
241
+ };
242
+ options.signal?.addEventListener("abort", onAbort, { once: true });
243
+ exclusiveKeypressStack.push(record);
244
+ updateTerminalState();
245
+ });
246
+ }
247
+ async function runWithSuspendedSharedInput(runner) {
248
+ const previousStates = /* @__PURE__ */ new Map();
249
+ for (const session of sharedSessions) {
250
+ if (session.closed) continue;
251
+ previousStates.set(session, session.suspended);
252
+ session.suspended = true;
253
+ }
254
+ if (canSetRawMode() && rawModeEnabled) {
255
+ process.stdin.setRawMode(false);
256
+ rawModeEnabled = false;
257
+ }
258
+ process.stdin.resume();
259
+ try {
260
+ return await runner();
261
+ } finally {
262
+ for (const [session, suspended] of previousStates) if (!session.closed) session.suspended = suspended;
263
+ updateTerminalState();
264
+ }
265
+ }
266
+ //#endregion
267
+ //#region src/cli/retry.ts
268
+ const RETRY_PROMPT_INITIAL_IGNORE_MS = 300;
269
+ const RETRY_CONFIRM_KEYS = ["y"];
270
+ const RETRY_CANCEL_KEYS = [
271
+ "q",
272
+ "Esc",
273
+ "Ctrl+C"
274
+ ];
275
+ const LOGIN_REQUIRED_PATTERNS = [
276
+ /code(?:\s*[:=]\s*|\s+)13\b/i,
277
+ /需要重新登录/,
278
+ /need\s+re-?login/i,
279
+ /re-?login/i,
280
+ /\blogin(?:\s+again)?\s+(?:is\s+)?required\b/i
281
+ ];
282
+ /**
283
+ * @description 提取执行错误文本,便于统一匹配与提示。
284
+ */
285
+ function extractExecutionErrorText(error) {
286
+ if (!error || typeof error !== "object") return "";
287
+ const parts = [];
288
+ const candidate = error;
289
+ for (const field of [
290
+ candidate.message,
291
+ candidate.shortMessage,
292
+ candidate.stderr,
293
+ candidate.stdout
294
+ ]) if (typeof field === "string" && field.trim()) parts.push(field);
295
+ return parts.join("\n");
296
+ }
297
+ function extractLoginRequiredMessage$1(text) {
298
+ if (!text) return "";
299
+ if (/需要重新登录/.test(text)) return "需要重新登录";
300
+ const englishMatch = text.match(/need\s+re-?login|re-?login/i);
301
+ if (englishMatch?.[0]) return englishMatch[0].toLowerCase();
302
+ const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find((line) => Boolean(line) && !line.startsWith("at "));
303
+ if (!firstLine) return "";
304
+ return firstLine.replace(/^\[error\]\s*/i, "").replace(/^error\s*:\s*/i, "").slice(0, 120);
305
+ }
306
+ /**
307
+ * @description 判断是否为微信开发者工具登录失效错误。
308
+ */
309
+ function isWechatIdeLoginRequiredError(error) {
310
+ const text = extractExecutionErrorText(error);
311
+ if (!text) return false;
312
+ return LOGIN_REQUIRED_PATTERNS.some((pattern) => pattern.test(text));
313
+ }
314
+ /**
315
+ * @description 将登录失效错误格式化为更易读的摘要。
316
+ */
317
+ function formatWechatIdeLoginRequiredError(error) {
318
+ const text = extractExecutionErrorText(error);
319
+ const code = text.match(/code\s*[:=]\s*(\d+)/i)?.[1];
320
+ const message = extractLoginRequiredMessage$1(text);
321
+ const lines = ["微信开发者工具返回登录错误:"];
322
+ if (code) lines.push(`- code: ${code}`);
323
+ if (message) lines.push(`- message: ${message}`);
324
+ if (!code && !message) lines.push("- message: 需要重新登录");
325
+ return lines.join("\n");
326
+ }
327
+ /**
328
+ * @description 创建登录失效专用错误,并携带退出码语义。
329
+ */
330
+ function createWechatIdeLoginRequiredExitError(error, reason) {
331
+ const summary = formatWechatIdeLoginRequiredError(error);
332
+ const message = reason ? `${reason}\n${summary}` : summary;
333
+ const loginError = new Error(message, { cause: error });
334
+ loginError.name = "WechatIdeLoginRequiredError";
335
+ loginError.code = 10;
336
+ loginError.exitCode = 10;
337
+ return loginError;
338
+ }
339
+ /**
340
+ * @description 判断错误是否为登录失效专用退出错误。
341
+ */
342
+ function isWechatIdeLoginRequiredExitError(error) {
343
+ if (!error || typeof error !== "object") return false;
344
+ const candidate = error;
345
+ return candidate.name === "WechatIdeLoginRequiredError" || (candidate.code === 10 || candidate.exitCode === 10) && isWechatIdeLoginRequiredError(error);
346
+ }
347
+ /**
348
+ * @description 交互等待用户按 y 重试,按 q 或 Ctrl+C 取消。
349
+ */
350
+ async function waitForRetryKeypress(options = {}) {
351
+ const { timeoutMs = 3e4 } = options;
352
+ if (!process.stdin.isTTY) return "cancel";
353
+ return await waitForExclusiveKeypress({
354
+ signal: options.signal,
355
+ ignoreInitialMs: 300,
356
+ onKeypress: (_str, key) => {
357
+ if (!key) return;
358
+ if (key.ctrl && key.name === "c") return "cancel";
359
+ if (key.name === "y") return "retry";
360
+ if (key.name === "q" || key.name === "escape") return "cancel";
361
+ },
362
+ timeoutMs
363
+ });
364
+ }
365
+ function highlightHotkey(key) {
366
+ return colors.bold(colors.green(key));
367
+ }
368
+ /**
369
+ * @description 生成重试按键提示,并高亮关键热键。
370
+ */
371
+ function formatRetryHotkeyPrompt(timeoutMs = 3e4) {
372
+ const highlight = (key) => highlightHotkey(key);
373
+ const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1e3));
374
+ const confirmKeys = RETRY_CONFIRM_KEYS.map(highlight).join(" / ");
375
+ const cancelKeys = RETRY_CANCEL_KEYS.map(highlight).join(" / ");
376
+ return i18nText(`按 ${confirmKeys} 重试,按 ${cancelKeys} 退出(${timeoutSeconds}s 内无输入将自动失败)。`, `Press ${confirmKeys} to retry, ${cancelKeys} to cancel (auto fail in ${timeoutSeconds}s).`);
377
+ }
378
+ /**
379
+ * @description 输出重试热键提示,并独占等待当前提示对应的按键输入。
380
+ */
381
+ async function promptRetryKeypress(options) {
382
+ const { logger, timeoutMs = 3e4 } = options;
383
+ logger.info(formatRetryHotkeyPrompt(timeoutMs));
384
+ return await waitForRetryKeypress({
385
+ timeoutMs,
386
+ signal: options.signal
387
+ });
388
+ }
389
+ /**
390
+ * @description 统一处理微信开发者工具登录失效场景下的提示与重试交互。
391
+ */
392
+ async function promptWechatIdeLoginRetry(options) {
393
+ const { allowRetry = true, cancelLevel = "info", error, logger, promptOpenIdeLogin = false, retryTimeoutMs = 3e4 } = options;
394
+ logger.error(i18nText("检测到微信开发者工具登录状态失效,请先登录后重试。", "Wechat DevTools login has expired. Please login and retry."));
395
+ if (promptOpenIdeLogin) logger.warn(i18nText("请先打开微信开发者工具完成登录。", "Please open Wechat DevTools and complete login first."));
396
+ logger.warn(formatWechatIdeLoginRequiredError(error));
397
+ if (!allowRetry) return "cancel";
398
+ const action = await promptRetryKeypress({
399
+ logger,
400
+ timeoutMs: retryTimeoutMs,
401
+ signal: options.signal
402
+ });
403
+ if (action === "timeout") {
404
+ logger.error(i18nText(`等待登录重试输入超时(${retryTimeoutMs}ms),已自动取消。`, `Retry prompt timed out (${retryTimeoutMs}ms), canceled automatically.`));
405
+ return "cancel";
406
+ }
407
+ if (action !== "retry") {
408
+ logger[cancelLevel](i18nText("已取消重试。完成登录后请重新执行当前命令。", "Retry canceled. Please run the command again after login."));
409
+ return "cancel";
410
+ }
411
+ return "retry";
412
+ }
413
+ //#endregion
414
+ //#region src/cli/automator/errors.ts
415
+ const ERROR_STACK_PREFIX_RE = /^at /;
416
+ const ERROR_PREFIX_RE = /^\[error\]\s*/i;
417
+ const ERROR_LABEL_PREFIX_RE = /^error\s*:\s*/i;
418
+ const ERROR_LINE_SPLIT_RE = /\r?\n/;
419
+ const LOGIN_REQUIRED_CN_RE = /需要重新登录/;
420
+ const LOGIN_REQUIRED_EN_RE = /need\s+re-?login|re-?login/i;
421
+ const LOGIN_REQUIRED_CODE_RE = /code\s*[:=]\s*(\d+)/i;
422
+ const DEVTOOLS_HTTP_PORT_ERROR = "Failed to launch wechat web devTools, please make sure http port is open";
423
+ const DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED_RE = /Extension context invalidated/i;
424
+ const AUTOMATOR_LAUNCH_TIMEOUT_RE = /Wait timed out after \d+ ms/i;
425
+ const AUTOMATOR_WS_CONNECT_RE = /Failed connecting to ws:\/\/127\.0\.0\.1:\d+/i;
426
+ const AUTOMATOR_PORT_IN_USE_RE = /Port \d+ is in use, please specify another port/i;
427
+ const DEVTOOLS_PROTOCOL_TIMEOUT_RE = /DevTools did not respond to protocol method (\S+) within \d+ms/i;
428
+ const DEVTOOLS_INFRA_ERROR_PATTERNS = [
429
+ /#initialize-error:\s*wait IDE port timeout/i,
430
+ /wait IDE port timeout/i,
431
+ /listen EPERM/i,
432
+ /operation not permitted 0\.0\.0\.0/i,
433
+ /EACCES/i,
434
+ /ECONNREFUSED/i,
435
+ /connect ECONNREFUSED/i
436
+ ];
437
+ /**
438
+ * @description 从错误对象中提取可读文本。
439
+ */
440
+ function extractErrorText(error) {
441
+ if (!error || typeof error !== "object") return "";
442
+ const candidate = error;
443
+ return [
444
+ candidate.message,
445
+ candidate.shortMessage,
446
+ candidate.stderr,
447
+ candidate.stdout
448
+ ].filter((value) => typeof value === "string" && value.trim().length > 0).join("\n");
449
+ }
450
+ /**
451
+ * @description 提取登录失效时最适合展示给用户的一行信息。
452
+ */
453
+ function extractLoginRequiredMessage(text) {
454
+ if (!text) return "";
455
+ if (LOGIN_REQUIRED_CN_RE.test(text)) return "需要重新登录";
456
+ const englishMatch = text.match(LOGIN_REQUIRED_EN_RE);
457
+ if (englishMatch?.[0]) return englishMatch[0].toLowerCase();
458
+ const firstLine = text.split(ERROR_LINE_SPLIT_RE).map((line) => line.trim()).find((line) => Boolean(line) && !ERROR_STACK_PREFIX_RE.test(line));
459
+ if (!firstLine) return "";
460
+ return firstLine.replace(ERROR_PREFIX_RE, "").replace(ERROR_LABEL_PREFIX_RE, "").slice(0, 120);
461
+ }
462
+ /**
463
+ * @description 判断错误是否属于开发者工具服务端口不可用。
464
+ */
465
+ function isDevtoolsHttpPortError(error) {
466
+ const message = error instanceof Error ? error.message : String(error);
467
+ return message.includes(DEVTOOLS_HTTP_PORT_ERROR) || DEVTOOLS_INFRA_ERROR_PATTERNS.some((pattern) => pattern.test(message));
468
+ }
469
+ /**
470
+ * @description 判断错误是否属于开发者工具 automator 扩展上下文尚未就绪。
471
+ */
472
+ function isDevtoolsExtensionContextInvalidatedError(error) {
473
+ const message = error instanceof Error ? error.message : String(error);
474
+ return DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED_RE.test(message);
475
+ }
476
+ /**
477
+ * @description 判断错误是否属于可重试的 automator 启动抖动。
478
+ */
479
+ function isRetryableAutomatorLaunchError(error) {
480
+ const message = error instanceof Error ? error.message : String(error);
481
+ return DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED_RE.test(message) || AUTOMATOR_LAUNCH_TIMEOUT_RE.test(message) || AUTOMATOR_WS_CONNECT_RE.test(message);
482
+ }
483
+ /**
484
+ * @description 判断错误是否属于开发者工具 websocket 连接失败。
485
+ */
486
+ function isAutomatorWsConnectError(error) {
487
+ const message = error instanceof Error ? error.message : String(error);
488
+ return AUTOMATOR_WS_CONNECT_RE.test(message);
489
+ }
490
+ /**
491
+ * @description 判断错误是否属于目标项目 automator 端口仍被旧会话占用。
492
+ */
493
+ function isAutomatorPortInUseError(error) {
494
+ const message = error instanceof Error ? error.message : String(error);
495
+ return AUTOMATOR_PORT_IN_USE_RE.test(message);
496
+ }
497
+ /**
498
+ * @description 判断错误是否属于开发者工具协议调用超时。
499
+ */
500
+ function isAutomatorProtocolTimeoutError(error) {
501
+ const message = error instanceof Error ? error.message : String(error);
502
+ return DEVTOOLS_PROTOCOL_TIMEOUT_RE.test(message);
503
+ }
504
+ /**
505
+ * @description 提取协议超时的方法名。
506
+ */
507
+ function getAutomatorProtocolTimeoutMethod(error) {
508
+ return (error instanceof Error ? error.message : String(error)).match(DEVTOOLS_PROTOCOL_TIMEOUT_RE)?.[1];
509
+ }
510
+ /**
511
+ * @description 判断错误是否属于开发者工具登录失效。
512
+ */
513
+ function isAutomatorLoginError(error) {
514
+ return isWechatIdeLoginRequiredError(error);
515
+ }
516
+ /**
517
+ * @description 格式化开发者工具登录失效错误,便于终端展示。
518
+ */
519
+ function formatAutomatorLoginError(error) {
520
+ const text = extractErrorText(error);
521
+ const code = text.match(LOGIN_REQUIRED_CODE_RE)?.[1];
522
+ const message = extractLoginRequiredMessage(text);
523
+ const lines = ["微信开发者工具返回登录错误:"];
524
+ if (code) lines.push(`- code: ${code}`);
525
+ if (message) lines.push(`- message: ${message}`);
526
+ if (!code && !message) lines.push("- message: 需要重新登录");
527
+ return lines.join("\n");
528
+ }
529
+ //#endregion
530
+ //#region src/cli/wechatDevtoolsSettings.ts
531
+ function createStorageHash(key) {
532
+ return createHash("md5").update(key).digest("hex");
533
+ }
534
+ const SETTINGS_STORAGE_HASH = createStorageHash("reduxPersist:settings");
535
+ const SETTINGS_STORAGE_FILE_NAMES = [`localstorage_${SETTINGS_STORAGE_HASH}.json`, `ls_${SETTINGS_STORAGE_HASH}.json`];
536
+ function isRecord(value) {
537
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
538
+ }
539
+ function normalizePort(value) {
540
+ if (typeof value !== "number" || !Number.isInteger(value)) return;
541
+ if (value <= 0 || value > 65535) return;
542
+ return value;
543
+ }
544
+ function normalizeWechatDevtoolsSecuritySettings(value) {
545
+ if (!isRecord(value)) return;
546
+ const normalized = {};
547
+ if (typeof value.enableServicePort === "boolean") normalized.enableServicePort = value.enableServicePort;
548
+ const port = normalizePort(value.port);
549
+ if (port !== void 0) normalized.port = port;
550
+ if (typeof value.allowGetTicket === "boolean") normalized.allowGetTicket = value.allowGetTicket;
551
+ if (typeof value.trustWhenAuto === "boolean") normalized.trustWhenAuto = value.trustWhenAuto;
552
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
553
+ }
554
+ async function resolveSelectedProfile(options) {
555
+ const platform = options.platform ?? process.platform;
556
+ if (!options.target && platform !== "darwin" && platform !== "win32") return;
557
+ return (await resolveWechatDevtoolsTarget(options)).profileDir;
558
+ }
559
+ async function readJsonObject(filePath) {
560
+ try {
561
+ const raw = await fs.readFile(filePath, "utf8");
562
+ const parsed = JSON.parse(raw);
563
+ if (!isRecord(parsed)) return {};
564
+ return parsed;
565
+ } catch (error) {
566
+ if (error.code === "ENOENT") return {};
567
+ if (error instanceof SyntaxError) return {};
568
+ throw error;
569
+ }
570
+ }
571
+ async function writeJsonObject(filePath, value) {
572
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
573
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
574
+ }
575
+ async function detectWechatDevtoolsSecuritySettings(localDataDir) {
576
+ for (const fileName of SETTINGS_STORAGE_FILE_NAMES) {
577
+ const security = normalizeWechatDevtoolsSecuritySettings((await readJsonObject(path.join(localDataDir, fileName))).security);
578
+ if (security) return security;
579
+ }
580
+ }
581
+ async function trustWechatDevtoolsProject(localDataDir, projectPath) {
582
+ const normalizedProjectPath = path.resolve(projectPath);
583
+ const projectHash = createStorageHash(`project2_${normalizedProjectPath}`);
584
+ const fileNames = [`localstorage_${projectHash}.json`, `ls_${projectHash}.json`];
585
+ let trusted = false;
586
+ for (const fileName of fileNames) {
587
+ const projectFilePath = path.join(localDataDir, fileName);
588
+ const current = await readJsonObject(projectFilePath);
589
+ if (current.projectid !== normalizedProjectPath || current.projectpath !== normalizedProjectPath || typeof current.appid !== "string" || !current.appid || !isRecord(current.attr) || !isRecord(current.attr.setting)) continue;
590
+ await writeJsonObject(projectFilePath, {
591
+ ...current,
592
+ isTrusted: true
593
+ });
594
+ trusted = true;
595
+ }
596
+ return trusted;
597
+ }
598
+ async function scanWechatDevtoolsServicePort(profileDir) {
599
+ const localDataDir = profileDir && path.join(profileDir, "WeappLocalData");
600
+ const exists = localDataDir && await fs.stat(localDataDir).then((stat) => stat.isDirectory(), (error) => {
601
+ if (error.code !== "ENOENT") throw error;
602
+ return false;
603
+ });
604
+ const security = exists && localDataDir ? await detectWechatDevtoolsSecuritySettings(localDataDir) : void 0;
605
+ return {
606
+ touchedInstanceCount: exists ? 1 : 0,
607
+ detectedSecurityCount: security ? 1 : 0,
608
+ servicePort: security?.port,
609
+ servicePortEnabled: security?.enableServicePort
610
+ };
611
+ }
612
+ /**
613
+ * @description 检测微信开发者工具当前服务端口配置,严格沿用用户已有设置。
614
+ */
615
+ async function detectWechatDevtoolsServicePort(options = {}) {
616
+ return await scanWechatDevtoolsServicePort(await resolveSelectedProfile(options));
617
+ }
618
+ /**
619
+ * @description 在启动微信开发者工具前,检测服务端口配置,并按需写入项目信任信息。
620
+ */
621
+ async function bootstrapWechatDevtoolsSettings(options = {}) {
622
+ const profileDir = await resolveSelectedProfile(options);
623
+ const result = await scanWechatDevtoolsServicePort(profileDir);
624
+ const trusted = profileDir && result.touchedInstanceCount && options.projectPath && options.trustProject === true ? await trustWechatDevtoolsProject(path.join(profileDir, "WeappLocalData"), options.projectPath) : false;
625
+ return {
626
+ ...result,
627
+ updatedSecurityCount: 0,
628
+ trustedProjectCount: trusted ? 1 : 0
629
+ };
630
+ }
631
+ //#endregion
632
+ //#region src/cli/automator.ts
633
+ const PROJECT_AUTOMATOR_PORT_BASE = 9620;
634
+ const PROJECT_AUTOMATOR_PORT_RANGE = 2e3;
635
+ /**
636
+ * @description 为项目路径派生稳定的 DevTools automator 端口,避免多个 dev:open 项目抢占默认端口。
637
+ */
638
+ function resolveProjectAutomatorPort(projectPath) {
639
+ const offset = createHash("sha1").update(path.resolve(projectPath)).digest().readUInt32BE(0) % PROJECT_AUTOMATOR_PORT_RANGE;
640
+ return PROJECT_AUTOMATOR_PORT_BASE + offset;
641
+ }
642
+ async function launchSelectedAutomator(options) {
643
+ const { port, projectPath, sessionId, timeout = 3e4 } = options;
644
+ const lifecycle = new OperationLifecycle(timeout, "IDE launch", options.signal);
645
+ let managedLaunch;
646
+ try {
647
+ return await lifecycle.run(async (scope) => {
648
+ const { Launcher } = await scope.step(() => import("@weapp-vite/miniprogram-automator"), { stage: "load-automator" });
649
+ const { target, cliPath: resolvedCliPath, installationId, runtimeProvider } = await scope.step(() => resolveAutomatorSessionOptions(options), { stage: "resolve-cli" });
650
+ if (runtimeProvider === "headless") return await scope.step(() => new Launcher().launch({
651
+ projectPath,
652
+ runtimeProvider,
653
+ timeout: scope.remainingMs(),
654
+ signal: scope.signal
655
+ }), {
656
+ stage: "launch",
657
+ disposeLate: (program) => program.disconnect()
658
+ });
659
+ await scope.step(() => assertWechatDevtoolsHost(target, {
660
+ signal: scope.signal,
661
+ timeout: scope.remainingMs()
662
+ }), { stage: "host-identity" });
663
+ const config = await scope.step(() => readCustomConfig(), { stage: "configuration" });
664
+ const resolvedTrustProject = options.trustProject ?? config.autoTrustProject ?? false;
665
+ const launcher = new Launcher();
666
+ let lastError = null;
667
+ let bootstrapResult;
668
+ if (process.env["WEAPP_IDE_MANAGED_PROJECT_JOURNAL"]?.trim()) {
669
+ const { launchManagedAutomator } = await import("./managed-B04Xo9U_.js");
670
+ managedLaunch = launchManagedAutomator({
671
+ launcher,
672
+ scope,
673
+ target,
674
+ projectPath: path.resolve(projectPath),
675
+ sourceProjectPath: projectPath,
676
+ port: port ?? resolveProjectAutomatorPort(projectPath),
677
+ sessionId,
678
+ trustProject: resolvedTrustProject,
679
+ persistAsDefaultSession: options.persistAsDefaultSession,
680
+ preserveProjectRoot: options.preserveProjectRoot
681
+ });
682
+ return await managedLaunch;
683
+ }
684
+ const resolvedProject = options.preserveProjectRoot ? {
685
+ projectPath: path.resolve(projectPath),
686
+ sourceProjectPath: path.resolve(projectPath)
687
+ } : await scope.step(() => resolveAutomatorProjectPath(projectPath), { stage: "project" });
688
+ if (config.autoBootstrapDevtools !== false) bootstrapResult = await scope.step(() => bootstrapWechatDevtoolsSettings({
689
+ target,
690
+ projectPath: resolvedProject.projectPath,
691
+ trustProject: resolvedTrustProject
692
+ }), { stage: "settings" });
693
+ if (bootstrapResult?.servicePortEnabled === false) throw new Error("Detected WeChat DevTools service port is disabled in current settings. Please enable it manually; existing user settings were not modified.");
694
+ for (let attempt = 0; attempt < 2; attempt += 1) {
695
+ scope.attempt();
696
+ try {
697
+ const miniProgram = await scope.step(() => launcher.launch({
698
+ cliPath: resolvedCliPath,
699
+ ...port ? { port } : {},
700
+ projectPath: resolvedProject.projectPath,
701
+ timeout: scope.remainingMs(),
702
+ signal: scope.signal,
703
+ trustProject: resolvedTrustProject
704
+ }), {
705
+ stage: "launch",
706
+ disposeLate: (program) => program.disconnect()
707
+ });
708
+ scope.own(() => miniProgram.disconnect(), "automator-session");
709
+ const sessionMetadata = Reflect.get(miniProgram, "__WEAPP_VITE_SESSION_METADATA");
710
+ if (typeof sessionMetadata?.wsEndpoint === "string" && sessionMetadata.wsEndpoint) {
711
+ const wsEndpoint = sessionMetadata.wsEndpoint;
712
+ await scope.step(() => assertWechatDevtoolsPort(target, Number(new URL(wsEndpoint).port), {
713
+ signal: scope.signal,
714
+ timeout: scope.remainingMs()
715
+ }), { stage: "port-identity" });
716
+ await scope.step(() => persistAutomatorSession({
717
+ installationId,
718
+ signal: scope.signal,
719
+ ...port ? { port: sessionMetadata.port ?? port } : {},
720
+ projectPath,
721
+ sessionId,
722
+ wsEndpoint
723
+ }), { stage: "session-persist" });
724
+ if (options.persistAsDefaultSession && (port || sessionId)) await scope.step(() => persistAutomatorSession({
725
+ installationId,
726
+ signal: scope.signal,
727
+ projectPath,
728
+ wsEndpoint
729
+ }), { stage: "session-persist" });
730
+ }
731
+ return miniProgram;
732
+ } catch (error) {
733
+ lastError = error;
734
+ scope.recordFailure(error);
735
+ scope.throwIfAborted();
736
+ if (!isRetryableAutomatorLaunchError(error) || attempt === 1) throw error;
737
+ }
738
+ }
739
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
740
+ });
741
+ } catch (error) {
742
+ if (managedLaunch) await (await managedLaunch).close();
743
+ throw error;
744
+ }
745
+ }
746
+ /**
747
+ * @description 基于当前配置解析 CLI 路径,并通过现代化 automator 入口启动会话。
748
+ */
749
+ async function launchAutomator(options) {
750
+ options.signal?.throwIfAborted();
751
+ return (options.runtimeProvider ?? process.env.WEAPP_VITE_AUTOMATOR_RUNTIME_PROVIDER ?? process.env.WEAPP_VITE_E2E_RUNTIME_PROVIDER) === "headless" ? await launchSelectedAutomator(options) : await withMachineE2ELease(() => launchSelectedAutomator(options));
752
+ }
753
+ /**
754
+ * @description 登记由外部 automator bridge 创建的会话,供 CLI 子进程安全复用。
755
+ */
756
+ async function persistOpenedAutomatorSession(options) {
757
+ const resolved = await resolveAutomatorSessionOptions(options);
758
+ if (resolved.runtimeProvider === "headless" || !resolved.target) throw new Error("DEVTOOLS_SESSION_PROVIDER_INVALID: external automator sessions require the WeChat DevTools provider.");
759
+ const endpoint = new URL(options.wsEndpoint);
760
+ if (endpoint.protocol !== "ws:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port) throw new Error("DEVTOOLS_SESSION_ENDPOINT_INVALID: automator session endpoint must be a loopback websocket.");
761
+ const endpointPort = Number(endpoint.port);
762
+ const port = options.port ?? endpointPort;
763
+ if (port !== endpointPort) throw new Error(`DEVTOOLS_SESSION_PORT_MISMATCH: endpoint=${endpointPort} option=${port}`);
764
+ await assertWechatDevtoolsPort(resolved.target, endpointPort, {
765
+ signal: options.signal,
766
+ timeout: options.timeout ?? 3e4
767
+ });
768
+ await persistAutomatorSession({
769
+ installationId: resolved.installationId,
770
+ projectPath: options.projectPath,
771
+ port,
772
+ ...options.sessionId ? { sessionId: options.sessionId } : {},
773
+ signal: options.signal,
774
+ wsEndpoint: options.wsEndpoint
775
+ });
776
+ if (!options.port && !options.sessionId) await persistAutomatorSession({
777
+ installationId: resolved.installationId,
778
+ projectPath: options.projectPath,
779
+ signal: options.signal,
780
+ wsEndpoint: options.wsEndpoint
781
+ });
782
+ }
783
+ /**
784
+ * @description 只读连接当前项目已打开的自动化会话;失败不证明缓存过期,也不删除其他操作持有的会话记录。
785
+ */
786
+ async function connectOpenedAutomator(options) {
787
+ const { port, projectPath, sessionId } = options;
788
+ return await new OperationLifecycle(options.timeout ?? 3e4, "IDE connect", options.signal).run(async (scope) => {
789
+ const { Launcher } = await scope.step(() => import("@weapp-vite/miniprogram-automator"), { stage: "load-automator" });
790
+ scope.attempt();
791
+ const launcher = new Launcher();
792
+ const resolved = await scope.step(() => resolveAutomatorSessionOptions(options), { stage: "resolve-cli" });
793
+ const persistedSession = await scope.step(() => readPersistedAutomatorSession({
794
+ projectPath,
795
+ sessionId,
796
+ port,
797
+ installationId: resolved.installationId
798
+ }), { stage: "session-lookup" });
799
+ if (!persistedSession) throw Object.assign(/* @__PURE__ */ new Error("DEVTOOLS_SESSION_IDENTITY_UNVERIFIED: no matching installation-bound session; launch the selected installation before connecting."), { code: "DEVTOOLS_SESSION_IDENTITY_UNVERIFIED" });
800
+ const wsEndpoint = persistedSession.wsEndpoint;
801
+ await scope.step(() => assertWechatDevtoolsPort(resolved.target, Number(new URL(wsEndpoint).port), {
802
+ signal: scope.signal,
803
+ timeout: scope.remainingMs()
804
+ }), { stage: "port-identity" });
805
+ const miniProgram = await scope.step(async () => await launcher.connect({
806
+ timeout: scope.remainingMs(),
807
+ signal: scope.signal,
808
+ wsEndpoint
809
+ }), {
810
+ stage: "connect",
811
+ disposeLate: (program) => {
812
+ if (program && typeof program === "object" && "disconnect" in program && typeof program.disconnect === "function") return program.disconnect();
813
+ }
814
+ });
815
+ if (process.env.WEAPP_IDE_MANAGED_PROJECT_JOURNAL) miniProgram.close = async () => {
816
+ await miniProgram.disconnect();
817
+ };
818
+ return miniProgram;
819
+ });
820
+ }
821
+ //#endregion
822
+ export { waitForExclusiveKeypress as A, isWechatIdeLoginRequiredError as C, waitForRetryKeypress as D, promptWechatIdeLoginRetry as E, parseAutomatorPort as F, i18nText as M, validateLocaleOption as N, createSharedInputSession as O, resolveAutomatorSessionOptions as P, formatWechatIdeLoginRequiredError as S, promptRetryKeypress as T, RETRY_CONFIRM_KEYS as _, bootstrapWechatDevtoolsSettings as a, extractExecutionErrorText as b, getAutomatorProtocolTimeoutMethod as c, isAutomatorProtocolTimeoutError as d, isAutomatorWsConnectError as f, RETRY_CANCEL_KEYS as g, isRetryableAutomatorLaunchError as h, resolveProjectAutomatorPort as i, configureLocaleFromArgv as j, runWithSuspendedSharedInput as k, isAutomatorLoginError as l, isDevtoolsHttpPortError as m, launchAutomator as n, detectWechatDevtoolsServicePort as o, isDevtoolsExtensionContextInvalidatedError as p, persistOpenedAutomatorSession as r, formatAutomatorLoginError as s, connectOpenedAutomator as t, isAutomatorPortInUseError as u, RETRY_PROMPT_INITIAL_IGNORE_MS as v, isWechatIdeLoginRequiredExitError as w, formatRetryHotkeyPrompt as x, createWechatIdeLoginRequiredExitError as y };