weapp-ide-cli 6.2.0 → 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.
package/README.md CHANGED
@@ -259,9 +259,42 @@ console.log(WEAPP_IDE_TOP_LEVEL_COMMAND_NAMES)
259
259
 
260
260
  ### 安装选择与共享登录
261
261
 
262
- 日常开发和本仓库 E2E 推荐使用固定位置的一份官方 Stable。多个项目复用同一个已登录宿主,无需为测试另外准备微信账号;登录正常过期时仍需通过 IDE 完成登录。不要复制 ticket、Cookie 或账号配置来同步两个安装。
262
+ 默认选择本机已安装的最新 Stable;官方灰度发布或网络查询失败不会阻塞默认选版。macOS 扫描系统及用户 Applications,Windows 读取安装登记和常见目录,Linux 与自定义位置通过显式 CLI 路径或本地登记接入。渠道和产品版本读取实际应用元数据,按数字段比较版本;符号链接按真实安装去重,同版本存在多份安装时要求 `--cli-path`。
263
263
 
264
- 未提供固定 `target` 时,程序化调用的显式 `cliPath` 优先;未指定时依次使用进程环境变量 `WEAPP_IDE_CLI_PATH`、全局 `cliPath` 配置和平台默认路径。提供 `target` 后,额外的 `cliPath` 必须指向同一个真实 CLI 文件,符号链接别名可用;不同安装会抛出 `WECHAT_DEVTOOLS_INSTALLATION_SELECTION_CONFLICT`,不会覆盖既定选择。环境变量只作用于当前进程及其子进程,不写回配置文件。`weapp config doctor` 显示持久配置与实际生效路径、产品版本和安装身份。
264
+ ```sh
265
+ weapp devtools list --json
266
+ weapp devtools add "<path-to-cli>"
267
+ weapp devtools remove "<path-to-cli>"
268
+ wv ide devtools list
269
+ wv open --devtools-channel=nightly
270
+ wv dev --open --devtools-channel=rc
271
+ wv ide preview --devtools-channel=rc --devtools-version=2.02.2608080
272
+ ```
273
+
274
+ 安装由用户完成,登记只保存 CLI 路径,每次重新读取元数据。列表显示渠道、版本、安装身份、实际配置目录及失效原因;登录使用各安装自己的配置目录,不复制 ticket、Cookie 或账号数据。
275
+
276
+ 同一微信账号切换安装后,另一安装的登录可能失效。无需准备多个账号,可在需要测试的版本中重新扫码;工具不共享或迁移登录数据。矩阵使用非交互模式,登录失效记录为 `blocked`,附带 `runtimeBlocker.code: "needs-login"`,保留原始错误并继续其他版本;不会等待扫码或自动重试,最终返回非零退出码。重新登录后可单独运行该渠道补测。
277
+
278
+ 选择优先级为:已固定的 `target` / 当前操作安装 → 本次 `cliPath`、`devtoolsChannel`、`devtoolsVersion` → 环境变量 → 旧全局 `cliPath` → 本机最新 Stable。显式渠道/版本覆盖继承的路径固定;同层路径、渠道、版本必须一致。`--devtools-version` 是开发者工具产品版本,上传的 `--version` 仍为小程序版本。包装参数不会传给官方 CLI。RC/nightly 需要明确选择,操作的启动、构建、连接、恢复和清理沿用同一安装。
279
+
280
+ 公共环境变量为 `WEAPP_IDE_CLI_PATH`、`WEAPP_IDE_DEVTOOLS_CHANNEL`、`WEAPP_IDE_DEVTOOLS_VERSION`;E2E 使用对应的 `WEAPP_VITE_E2E_DEVTOOLS_*`,解析后向全部子进程传入具体 CLI。兼容 `WEAPP_VITE_E2E_ACCEPTED_DEVTOOLS_VERSION` / `WEAPP_VITE_E2E_ACCEPTED_DEVTOOLS_CHANNEL` 的固定选择。显式 `--require-official-latest`(或公共 `WEAPP_IDE_REQUIRE_OFFICIAL_LATEST=1`、E2E `WEAPP_VITE_E2E_REQUIRE_OFFICIAL_LATEST=1`)才要求匹配所选渠道官方最新版本,查询失败也会阻塞此严格模式。
281
+
282
+ ```ts
283
+ import { listWechatDevtoolsInstallations, previewWechatIde, resolveWechatDevtoolsTarget } from 'weapp-ide-cli'
284
+
285
+ const installations = await listWechatDevtoolsInstallations()
286
+ const target = await resolveWechatDevtoolsTarget({ devtoolsChannel: 'rc' })
287
+ await previewWechatIde({ projectPath: './dist', target })
288
+ ```
289
+
290
+ 两套 MCP 的连接工具和相关 REST 输入都支持 `cliPath`、`devtoolsChannel`、`devtoolsVersion`、`requireOfficialLatest`;会话按真实安装身份隔离,同一项目不会串用 Stable 与 nightly 连接。
291
+
292
+ ```sh
293
+ pnpm e2e:ide:matrix --targets=stable,rc,nightly --suite=ide-smoke
294
+ pnpm e2e:ide:matrix --targets=stable@2.02.2608080,nightly --suite=ide-gate --filter=<task>
295
+ ```
296
+
297
+ 矩阵默认只测 Stable,复用既有 suite 及其筛选和严格策略,全程持有同机同用户串行租约。普通测试失败或缺失安装记录后继续;所有权异常、清理失败、OOM 或取消停止后续目标。报告保存在 `docs/reports/devtools-matrix`,分别记录请求、实际安装、runtime/基础库版本、官方查询状态和结果。缺失、阻塞、partial 或未执行不能计为完整通过,返回非零退出码;全量正式验收仍遵守主线工作树要求。
265
298
 
266
299
  HTTP 服务端口和 automator 会话都绑定选定安装。另一安装的配置较新、端口可连或版本号相同,均不代表可以复用。未知旧会话不会直接连接;宿主或监听端口归属不一致时停止操作,保留原有 IDE 和账号状态。`port` 仍表示对应接口原有的端口类型,不将 automator WebSocket 端口当作 HTTP 服务端口。
267
300
 
@@ -269,7 +302,9 @@ HTTP 服务端口和 automator 会话都绑定选定安装。另一安装的配
269
302
 
270
303
  `openWechatIdeProjectByHttp()` 使用 `/v2/open` 的 `project` 参数并保留原始响应正文;空正文或空 JSON 对象会抛出 `WECHAT_DEVTOOLS_HTTP_OPEN_UNCONFIRMED`,不能把 HTTP 200 当作项目已打开的证据。目标项目及 runtime 是否就绪仍需通过会话或 IDE 界面确认。
271
304
 
272
- 本仓库真实 IDE E2E 显式设置 `WEAPP_VITE_E2E_DEVTOOLS_CLI_PATH`,预检把它传入本轮公共 CLI,核对执行时官方 Stable 与实际宿主;不会自动安装或切换版本。不同 worktree 的 E2E 使用同机同用户的排他租约,冲突报告 `Runtime busy`,应等待持有者结束。普通 CLI 的宿主变更同样参与协调,已建立的多项目连接不长期占用此租约。自动恢复不清共享缓存、不退出账号;只释放本任务登记的资源。
305
+ 本仓库真实 IDE E2E 自动解析并固定本轮安装,核对实际宿主及 runtime 版本;不会自动安装工具。不同 worktree 的 E2E 使用同机同用户的排他租约,冲突报告 `Runtime busy`,应等待持有者结束。普通 CLI 的宿主变更同样参与协调,已建立的多项目连接不长期占用此租约。自动恢复不清共享缓存、不退出账号;只释放本任务登记的资源。
306
+
307
+ macOS 安装退出核验直接使用系统报告的映像路径,避免无关进程的受保护或已删除文件阻塞清理。当前磁盘路径被替换也不会覆盖原映像的归属判断;PID、启动时间、端口和任务登记仍须通过校验。
273
308
 
274
309
  ### 6. 程序化 opened-session helper
275
310
 
@@ -318,6 +353,27 @@ await clearWechatIdeCacheByAutomator({
318
353
  console.log(info, ticket)
319
354
  ```
320
355
 
356
+ ### 7. 程序化 automator 与异步断开
357
+
358
+ `connectOpenedAutomator()` 返回的会话提供公开 `send(method, params?, { timeout }?)` 与 `CDP().enable/sendCommand/listProtocol/onEvent`,用于直接调用已连接 DevTools 支持的自动化和调试协议。`onEvent` 返回取消订阅函数;可用域和命令以目标宿主 `listProtocol()` 的结果为准。
359
+
360
+ ```ts
361
+ import { connectOpenedAutomator } from 'weapp-ide-cli'
362
+
363
+ const miniProgram = await connectOpenedAutomator({ projectPath: '.' })
364
+ try {
365
+ console.log(await miniProgram.send('Tool.getInfo', {}, { timeout: 3_000 }))
366
+ console.log(await miniProgram.CDP().listProtocol({ timeout: 3_000 }))
367
+ }
368
+ finally {
369
+ await miniProgram.disconnect()
370
+ }
371
+ ```
372
+
373
+ `disconnect()` 现在返回 `Promise<void>`,调用时立即停止订阅并拒绝未完成请求,等待 WebSocket 关闭后完成。迁移同步清理回调时应返回或 `await` 此 Promise;超时和清理失败继续抛出。它只释放本连接,保留已打开项目窗口;需要关闭本任务受管窗口时使用相应会话的 `close()`,并等待窗口 owner 完成确认。MCP、日志转发及命令结束清理均等待各自持有的连接,不重新启动 IDE。
374
+
375
+ headless 会话的 `send` 仅适配 `Tool.getInfo`、`App.getCurrentPage/getPageStack/callFunction/callWxMethod`;`CDP()` 和其他原始协议明确报告 `HEADLESS_PROTOCOL_UNSUPPORTED`。触摸接口使用标准 `changedTouches`,旧 `changeTouches` 仍兼容,同时传入以标准字段为准。
376
+
321
377
  ## 路径与参数兼容
322
378
 
323
379
  - `-p` 会被自动替换为 `--project`,并且相对路径会解析为绝对路径。
@@ -1,11 +1,11 @@
1
- import { _ as readCustomConfig, a as assertWechatDevtoolsPort, i as assertWechatDevtoolsHost, n as readPersistedAutomatorSession, r as resolveWechatDevtoolsTarget, t as persistAutomatorSession } from "./sessionStore-zdzy2UFM.js";
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
2
  import path from "node:path";
3
3
  import { withMachineE2ELease } from "@weapp-vite/devtools-runtime";
4
- import { createHash } from "node:crypto";
5
4
  import fs from "node:fs/promises";
6
5
  import process from "node:process";
7
- import os from "node:os";
6
+ import { createHash } from "node:crypto";
8
7
  import { OperationLifecycle } from "@weapp-vite/miniprogram-automator/operation";
8
+ import { emitKeypressEvents } from "node:readline";
9
9
  //#region src/cli/automator/port.ts
10
10
  /** 显式端口不能被截断或作为默认值忽略,避免连接其他项目。 */
11
11
  function assertAutomatorPort(port) {
@@ -40,6 +40,377 @@ async function resolveAutomatorSessionOptions(options) {
40
40
  };
41
41
  }
42
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
43
414
  //#region src/cli/automator/errors.ts
44
415
  const ERROR_STACK_PREFIX_RE = /^at /;
45
416
  const ERROR_PREFIX_RE = /^\[error\]\s*/i;
@@ -63,12 +434,6 @@ const DEVTOOLS_INFRA_ERROR_PATTERNS = [
63
434
  /ECONNREFUSED/i,
64
435
  /connect ECONNREFUSED/i
65
436
  ];
66
- const DEVTOOLS_LOGIN_REQUIRED_PATTERNS = [
67
- /code\s*[:=]\s*10/i,
68
- /需要重新登录/,
69
- /need\s+re-?login/i,
70
- /re-?login/i
71
- ];
72
437
  /**
73
438
  * @description 从错误对象中提取可读文本。
74
439
  */
@@ -146,9 +511,7 @@ function getAutomatorProtocolTimeoutMethod(error) {
146
511
  * @description 判断错误是否属于开发者工具登录失效。
147
512
  */
148
513
  function isAutomatorLoginError(error) {
149
- const text = extractErrorText(error);
150
- if (!text) return false;
151
- return DEVTOOLS_LOGIN_REQUIRED_PATTERNS.some((pattern) => pattern.test(text));
514
+ return isWechatIdeLoginRequiredError(error);
152
515
  }
153
516
  /**
154
517
  * @description 格式化开发者工具登录失效错误,便于终端展示。
@@ -164,124 +527,6 @@ function formatAutomatorLoginError(error) {
164
527
  return lines.join("\n");
165
528
  }
166
529
  //#endregion
167
- //#region src/cli/automatorProject.ts
168
- const WRAPPER_ROOT = path.join(os.tmpdir(), "weapp-ide-cli-automator-projects");
169
- function normalizeProjectRelativeRoot(rawRoot) {
170
- if (typeof rawRoot !== "string") return;
171
- const normalized = rawRoot.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
172
- if (!normalized || normalized === ".") return;
173
- if (normalized.split("/").includes("..")) return;
174
- return normalized;
175
- }
176
- async function pathExists(filePath) {
177
- try {
178
- await fs.access(filePath);
179
- return true;
180
- } catch {
181
- return false;
182
- }
183
- }
184
- async function readJsonObject$1(filePath) {
185
- try {
186
- const raw = await fs.readFile(filePath, "utf8");
187
- const value = JSON.parse(raw);
188
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
189
- } catch {
190
- return;
191
- }
192
- }
193
- async function writeJsonObject$1(filePath, value) {
194
- await fs.mkdir(path.dirname(filePath), { recursive: true });
195
- await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
196
- }
197
- async function copyProjectRoot(sourceProjectPath, wrapperProjectPath, relativeRoot) {
198
- const sourcePath = path.join(sourceProjectPath, relativeRoot);
199
- if (!await pathExists(sourcePath)) return;
200
- await fs.cp(sourcePath, path.join(wrapperProjectPath, relativeRoot), {
201
- dereference: true,
202
- force: true,
203
- recursive: true
204
- });
205
- }
206
- function createWrapperProjectConfig(source, patch) {
207
- const { miniprogramRoot: _miniprogramRoot, qcloudRoot: _qcloudRoot, srcMiniprogramRoot: _srcMiniprogramRoot, ...rest } = source;
208
- const sourceSetting = source.setting && typeof source.setting === "object" ? source.setting : {};
209
- const patchSetting = patch.setting && typeof patch.setting === "object" ? patch.setting : {};
210
- return {
211
- ...rest,
212
- ...patch,
213
- setting: {
214
- ...sourceSetting,
215
- ...patchSetting,
216
- packNpmManually: false,
217
- packNpmRelationList: []
218
- }
219
- };
220
- }
221
- async function ensureWrapperAppConfig(wrapperProjectPath) {
222
- const appConfigPath = path.join(wrapperProjectPath, "app.json");
223
- const appConfig = await readJsonObject$1(appConfigPath);
224
- if (!appConfig) return;
225
- await writeJsonObject$1(appConfigPath, {
226
- ...appConfig,
227
- subPackages: Array.isArray(appConfig.subPackages) ? appConfig.subPackages : []
228
- });
229
- }
230
- /**
231
- * @description 为 DevTools automator 准备稳定的小程序根目录,避免打开瞬间跨 miniprogramRoot 读取 app.json 抖动。
232
- */
233
- async function resolveAutomatorProjectPath(projectPath) {
234
- const sourceProjectPath = path.resolve(projectPath);
235
- const projectConfig = await readJsonObject$1(path.join(sourceProjectPath, "project.config.json"));
236
- const miniprogramRoot = normalizeProjectRelativeRoot(projectConfig?.miniprogramRoot);
237
- if (!miniprogramRoot) return {
238
- projectPath: sourceProjectPath,
239
- sourceProjectPath
240
- };
241
- const sourceProjectConfig = projectConfig ?? {};
242
- const distRoot = path.join(sourceProjectPath, miniprogramRoot);
243
- if (!await pathExists(path.join(distRoot, "app.json"))) return {
244
- projectPath: sourceProjectPath,
245
- sourceProjectPath
246
- };
247
- const wrapperHash = createHash("sha1").update(sourceProjectPath).update("\0").update(path.resolve(distRoot)).digest("hex").slice(0, 16);
248
- const wrapperProjectPath = path.join(WRAPPER_ROOT, wrapperHash);
249
- await fs.rm(wrapperProjectPath, {
250
- recursive: true,
251
- force: true
252
- });
253
- await fs.mkdir(wrapperProjectPath, { recursive: true });
254
- await fs.cp(distRoot, wrapperProjectPath, {
255
- dereference: true,
256
- force: true,
257
- recursive: true
258
- });
259
- const pluginRoot = normalizeProjectRelativeRoot(sourceProjectConfig.pluginRoot);
260
- if (pluginRoot) await copyProjectRoot(sourceProjectPath, wrapperProjectPath, pluginRoot);
261
- const rootPatch = {
262
- compileType: sourceProjectConfig.compileType === "plugin" ? "plugin" : "miniprogram",
263
- miniprogramRoot: "./",
264
- srcMiniprogramRoot: "./"
265
- };
266
- const projectPrivateConfig = await readJsonObject$1(path.join(sourceProjectPath, "project.private.config.json")) ?? {};
267
- await writeJsonObject$1(path.join(wrapperProjectPath, "project.config.json"), createWrapperProjectConfig(sourceProjectConfig, {
268
- ...projectPrivateConfig,
269
- ...rootPatch
270
- }));
271
- await fs.rm(path.join(wrapperProjectPath, "project.private.config.json"), { force: true });
272
- await ensureWrapperAppConfig(wrapperProjectPath);
273
- return {
274
- cleanup: async () => {
275
- await fs.rm(wrapperProjectPath, {
276
- recursive: true,
277
- force: true
278
- });
279
- },
280
- projectPath: wrapperProjectPath,
281
- sourceProjectPath
282
- };
283
- }
284
- //#endregion
285
530
  //#region src/cli/wechatDevtoolsSettings.ts
286
531
  function createStorageHash(key) {
287
532
  return createHash("md5").update(key).digest("hex");
@@ -420,25 +665,26 @@ async function launchSelectedAutomator(options) {
420
665
  const launcher = new Launcher();
421
666
  let lastError = null;
422
667
  let bootstrapResult;
423
- const resolvedProject = options.preserveProjectRoot ? {
424
- projectPath: path.resolve(projectPath),
425
- sourceProjectPath: path.resolve(projectPath)
426
- } : await scope.step(() => resolveAutomatorProjectPath(projectPath), { stage: "project" });
427
- if (process.env.WEAPP_IDE_MANAGED_PROJECT_JOURNAL) {
428
- const { launchManagedAutomator } = await import("./managed-Sq8y8FzP.js");
668
+ if (process.env["WEAPP_IDE_MANAGED_PROJECT_JOURNAL"]?.trim()) {
669
+ const { launchManagedAutomator } = await import("./managed-B04Xo9U_.js");
429
670
  managedLaunch = launchManagedAutomator({
430
671
  launcher,
431
672
  scope,
432
673
  target,
433
- projectPath: resolvedProject.projectPath,
674
+ projectPath: path.resolve(projectPath),
434
675
  sourceProjectPath: projectPath,
435
- port: port ?? resolveProjectAutomatorPort(resolvedProject.projectPath),
676
+ port: port ?? resolveProjectAutomatorPort(projectPath),
436
677
  sessionId,
437
678
  trustProject: resolvedTrustProject,
438
- persistAsDefaultSession: options.persistAsDefaultSession
679
+ persistAsDefaultSession: options.persistAsDefaultSession,
680
+ preserveProjectRoot: options.preserveProjectRoot
439
681
  });
440
682
  return await managedLaunch;
441
683
  }
684
+ const resolvedProject = options.preserveProjectRoot ? {
685
+ projectPath: path.resolve(projectPath),
686
+ sourceProjectPath: path.resolve(projectPath)
687
+ } : await scope.step(() => resolveAutomatorProjectPath(projectPath), { stage: "project" });
442
688
  if (config.autoBootstrapDevtools !== false) bootstrapResult = await scope.step(() => bootstrapWechatDevtoolsSettings({
443
689
  target,
444
690
  projectPath: resolvedProject.projectPath,
@@ -563,14 +809,14 @@ async function connectOpenedAutomator(options) {
563
809
  }), {
564
810
  stage: "connect",
565
811
  disposeLate: (program) => {
566
- if (program && typeof program === "object" && "disconnect" in program && typeof program.disconnect === "function") program.disconnect();
812
+ if (program && typeof program === "object" && "disconnect" in program && typeof program.disconnect === "function") return program.disconnect();
567
813
  }
568
814
  });
569
815
  if (process.env.WEAPP_IDE_MANAGED_PROJECT_JOURNAL) miniProgram.close = async () => {
570
- miniProgram.disconnect();
816
+ await miniProgram.disconnect();
571
817
  };
572
818
  return miniProgram;
573
819
  });
574
820
  }
575
821
  //#endregion
576
- export { parseAutomatorPort as _, bootstrapWechatDevtoolsSettings as a, getAutomatorProtocolTimeoutMethod as c, isAutomatorProtocolTimeoutError as d, isAutomatorWsConnectError as f, resolveAutomatorSessionOptions as g, isRetryableAutomatorLaunchError as h, resolveProjectAutomatorPort as i, 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 };
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 };
@@ -0,0 +1 @@
1
+ export { n as launchAutomator } from "./automator-BOe32upt.js";