weapp-ide-cli 6.1.8 → 6.2.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 +18 -0
- package/dist/automator-BkZZsGtn.js +1 -0
- package/dist/automator-CRpB6_pA.js +576 -0
- package/dist/automator-session-gWEsSs_A.js +558 -0
- package/dist/{cli-2sY1sAVq.js → cli-ChMQqi5w.js} +1219 -52
- package/dist/cli.js +3 -3
- package/dist/commands-C1PF1B4P.js +528 -0
- package/dist/commands-CZVPfkE9.js +1 -0
- package/dist/devtoolsProjectOwnership-yDD6h0dO.js +1726 -0
- package/dist/index.d.ts +314 -54
- package/dist/index.js +8 -5
- package/dist/managed-Sq8y8FzP.js +212 -0
- package/dist/run-mcp-CPv8cesh.js +1 -0
- package/dist/{run-mcp-LqaWlQJ_.js → run-mcp-Dl_1TH1x.js} +18 -9
- package/dist/sessionStore-zdzy2UFM.js +605 -0
- package/package.json +12 -11
- package/dist/automator-session-fbanL2eW.js +0 -1362
- package/dist/commands-DpMG1w04.js +0 -2
- package/dist/commands-__RJ552L.js +0 -1359
- package/dist/run-mcp-C2SQ_hRJ.js +0 -2
package/README.md
CHANGED
|
@@ -253,6 +253,24 @@ if (isWeappIdeTopLevelCommand('preview')) {
|
|
|
253
253
|
console.log(WEAPP_IDE_TOP_LEVEL_COMMAND_NAMES)
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
+
截图发生协议、导航或截图请求超时时,默认最多重试一次,失败连接由原会话生命周期释放,不在重试层按项目键再次关闭共享连接;不调用无项目定位的 `close` 或退出共享宿主。重试仍失败时保留原始错误,手动打开的窗口和其他项目保持不变。
|
|
257
|
+
|
|
258
|
+
登录诊断可使用 `queryWechatIdeLogin(cliPath, { timeout: 3000 })`,显式指定本轮选择的 CLI。只有原生 JSON 中的 `login` 布尔值才返回 `{ status: 'success', login }`;超时、命令失败、空输出或矛盾响应均返回 `{ status: 'unknown', reason }`。`unknown` 不等于未登录。机器租约冲突返回 `reason: 'runtime-busy'`,应等待持有者结束后再查询。该方法不提示、不重试、不回退安装路径;原生 `islogin` 可能启动 IDE,不属于静态探针。旧的 `isWechatIdeLoggedIn()` 保留 CLI 透传行为,其 Promise 完成不能作为登录事实。
|
|
259
|
+
|
|
260
|
+
### 安装选择与共享登录
|
|
261
|
+
|
|
262
|
+
日常开发和本仓库 E2E 推荐使用固定位置的一份官方 Stable。多个项目复用同一个已登录宿主,无需为测试另外准备微信账号;登录正常过期时仍需通过 IDE 完成登录。不要复制 ticket、Cookie 或账号配置来同步两个安装。
|
|
263
|
+
|
|
264
|
+
未提供固定 `target` 时,程序化调用的显式 `cliPath` 优先;未指定时依次使用进程环境变量 `WEAPP_IDE_CLI_PATH`、全局 `cliPath` 配置和平台默认路径。提供 `target` 后,额外的 `cliPath` 必须指向同一个真实 CLI 文件,符号链接别名可用;不同安装会抛出 `WECHAT_DEVTOOLS_INSTALLATION_SELECTION_CONFLICT`,不会覆盖既定选择。环境变量只作用于当前进程及其子进程,不写回配置文件。`weapp config doctor` 显示持久配置与实际生效路径、产品版本和安装身份。
|
|
265
|
+
|
|
266
|
+
HTTP 服务端口和 automator 会话都绑定选定安装。另一安装的配置较新、端口可连或版本号相同,均不代表可以复用。未知旧会话不会直接连接;宿主或监听端口归属不一致时停止操作,保留原有 IDE 和账号状态。`port` 仍表示对应接口原有的端口类型,不将 automator WebSocket 端口当作 HTTP 服务端口。
|
|
267
|
+
|
|
268
|
+
显式 HTTP 或 automator 端口必须是 `1` 到 `65535` 的整数;CLI、MCP 和 REST 同样拒绝无效端口,不会将 `0`、小数、混合文本或超范围值当作默认端口。打开项目的 HTTP 请求与官方 CLI 回退共用一次安装选择和机器租约,安装冲突不会触发回退。
|
|
269
|
+
|
|
270
|
+
`openWechatIdeProjectByHttp()` 使用 `/v2/open` 的 `project` 参数并保留原始响应正文;空正文或空 JSON 对象会抛出 `WECHAT_DEVTOOLS_HTTP_OPEN_UNCONFIRMED`,不能把 HTTP 200 当作项目已打开的证据。目标项目及 runtime 是否就绪仍需通过会话或 IDE 界面确认。
|
|
271
|
+
|
|
272
|
+
本仓库真实 IDE E2E 显式设置 `WEAPP_VITE_E2E_DEVTOOLS_CLI_PATH`,预检把它传入本轮公共 CLI,核对执行时官方 Stable 与实际宿主;不会自动安装或切换版本。不同 worktree 的 E2E 使用同机同用户的排他租约,冲突报告 `Runtime busy`,应等待持有者结束。普通 CLI 的宿主变更同样参与协调,已建立的多项目连接不长期占用此租约。自动恢复不清共享缓存、不退出账号;只释放本任务登记的资源。
|
|
273
|
+
|
|
256
274
|
### 6. 程序化 opened-session helper
|
|
257
275
|
|
|
258
276
|
除了官方 CLI 透传和 automator 子命令,`weapp-ide-cli` 也提供了一组面向「已打开 DevTools 会话」的 helper。它们会优先复用当前项目对应的会话,在官方 HTTP / Tool 域能力可用时直接下发指令,减少再次拉起 IDE 或误打开项目选择页的概率。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as launchAutomator } from "./automator-CRpB6_pA.js";
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { _ as readCustomConfig, a as assertWechatDevtoolsPort, i as assertWechatDevtoolsHost, n as readPersistedAutomatorSession, r as resolveWechatDevtoolsTarget, t as persistAutomatorSession } from "./sessionStore-zdzy2UFM.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { withMachineE2ELease } from "@weapp-vite/devtools-runtime";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { OperationLifecycle } from "@weapp-vite/miniprogram-automator/operation";
|
|
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/cli/automator/errors.ts
|
|
44
|
+
const ERROR_STACK_PREFIX_RE = /^at /;
|
|
45
|
+
const ERROR_PREFIX_RE = /^\[error\]\s*/i;
|
|
46
|
+
const ERROR_LABEL_PREFIX_RE = /^error\s*:\s*/i;
|
|
47
|
+
const ERROR_LINE_SPLIT_RE = /\r?\n/;
|
|
48
|
+
const LOGIN_REQUIRED_CN_RE = /需要重新登录/;
|
|
49
|
+
const LOGIN_REQUIRED_EN_RE = /need\s+re-?login|re-?login/i;
|
|
50
|
+
const LOGIN_REQUIRED_CODE_RE = /code\s*[:=]\s*(\d+)/i;
|
|
51
|
+
const DEVTOOLS_HTTP_PORT_ERROR = "Failed to launch wechat web devTools, please make sure http port is open";
|
|
52
|
+
const DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED_RE = /Extension context invalidated/i;
|
|
53
|
+
const AUTOMATOR_LAUNCH_TIMEOUT_RE = /Wait timed out after \d+ ms/i;
|
|
54
|
+
const AUTOMATOR_WS_CONNECT_RE = /Failed connecting to ws:\/\/127\.0\.0\.1:\d+/i;
|
|
55
|
+
const AUTOMATOR_PORT_IN_USE_RE = /Port \d+ is in use, please specify another port/i;
|
|
56
|
+
const DEVTOOLS_PROTOCOL_TIMEOUT_RE = /DevTools did not respond to protocol method (\S+) within \d+ms/i;
|
|
57
|
+
const DEVTOOLS_INFRA_ERROR_PATTERNS = [
|
|
58
|
+
/#initialize-error:\s*wait IDE port timeout/i,
|
|
59
|
+
/wait IDE port timeout/i,
|
|
60
|
+
/listen EPERM/i,
|
|
61
|
+
/operation not permitted 0\.0\.0\.0/i,
|
|
62
|
+
/EACCES/i,
|
|
63
|
+
/ECONNREFUSED/i,
|
|
64
|
+
/connect ECONNREFUSED/i
|
|
65
|
+
];
|
|
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
|
+
/**
|
|
73
|
+
* @description 从错误对象中提取可读文本。
|
|
74
|
+
*/
|
|
75
|
+
function extractErrorText(error) {
|
|
76
|
+
if (!error || typeof error !== "object") return "";
|
|
77
|
+
const candidate = error;
|
|
78
|
+
return [
|
|
79
|
+
candidate.message,
|
|
80
|
+
candidate.shortMessage,
|
|
81
|
+
candidate.stderr,
|
|
82
|
+
candidate.stdout
|
|
83
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0).join("\n");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* @description 提取登录失效时最适合展示给用户的一行信息。
|
|
87
|
+
*/
|
|
88
|
+
function extractLoginRequiredMessage(text) {
|
|
89
|
+
if (!text) return "";
|
|
90
|
+
if (LOGIN_REQUIRED_CN_RE.test(text)) return "需要重新登录";
|
|
91
|
+
const englishMatch = text.match(LOGIN_REQUIRED_EN_RE);
|
|
92
|
+
if (englishMatch?.[0]) return englishMatch[0].toLowerCase();
|
|
93
|
+
const firstLine = text.split(ERROR_LINE_SPLIT_RE).map((line) => line.trim()).find((line) => Boolean(line) && !ERROR_STACK_PREFIX_RE.test(line));
|
|
94
|
+
if (!firstLine) return "";
|
|
95
|
+
return firstLine.replace(ERROR_PREFIX_RE, "").replace(ERROR_LABEL_PREFIX_RE, "").slice(0, 120);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* @description 判断错误是否属于开发者工具服务端口不可用。
|
|
99
|
+
*/
|
|
100
|
+
function isDevtoolsHttpPortError(error) {
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
return message.includes(DEVTOOLS_HTTP_PORT_ERROR) || DEVTOOLS_INFRA_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* @description 判断错误是否属于开发者工具 automator 扩展上下文尚未就绪。
|
|
106
|
+
*/
|
|
107
|
+
function isDevtoolsExtensionContextInvalidatedError(error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
return DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED_RE.test(message);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* @description 判断错误是否属于可重试的 automator 启动抖动。
|
|
113
|
+
*/
|
|
114
|
+
function isRetryableAutomatorLaunchError(error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
return DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED_RE.test(message) || AUTOMATOR_LAUNCH_TIMEOUT_RE.test(message) || AUTOMATOR_WS_CONNECT_RE.test(message);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* @description 判断错误是否属于开发者工具 websocket 连接失败。
|
|
120
|
+
*/
|
|
121
|
+
function isAutomatorWsConnectError(error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
return AUTOMATOR_WS_CONNECT_RE.test(message);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* @description 判断错误是否属于目标项目 automator 端口仍被旧会话占用。
|
|
127
|
+
*/
|
|
128
|
+
function isAutomatorPortInUseError(error) {
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
+
return AUTOMATOR_PORT_IN_USE_RE.test(message);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* @description 判断错误是否属于开发者工具协议调用超时。
|
|
134
|
+
*/
|
|
135
|
+
function isAutomatorProtocolTimeoutError(error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
return DEVTOOLS_PROTOCOL_TIMEOUT_RE.test(message);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* @description 提取协议超时的方法名。
|
|
141
|
+
*/
|
|
142
|
+
function getAutomatorProtocolTimeoutMethod(error) {
|
|
143
|
+
return (error instanceof Error ? error.message : String(error)).match(DEVTOOLS_PROTOCOL_TIMEOUT_RE)?.[1];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* @description 判断错误是否属于开发者工具登录失效。
|
|
147
|
+
*/
|
|
148
|
+
function isAutomatorLoginError(error) {
|
|
149
|
+
const text = extractErrorText(error);
|
|
150
|
+
if (!text) return false;
|
|
151
|
+
return DEVTOOLS_LOGIN_REQUIRED_PATTERNS.some((pattern) => pattern.test(text));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* @description 格式化开发者工具登录失效错误,便于终端展示。
|
|
155
|
+
*/
|
|
156
|
+
function formatAutomatorLoginError(error) {
|
|
157
|
+
const text = extractErrorText(error);
|
|
158
|
+
const code = text.match(LOGIN_REQUIRED_CODE_RE)?.[1];
|
|
159
|
+
const message = extractLoginRequiredMessage(text);
|
|
160
|
+
const lines = ["微信开发者工具返回登录错误:"];
|
|
161
|
+
if (code) lines.push(`- code: ${code}`);
|
|
162
|
+
if (message) lines.push(`- message: ${message}`);
|
|
163
|
+
if (!code && !message) lines.push("- message: 需要重新登录");
|
|
164
|
+
return lines.join("\n");
|
|
165
|
+
}
|
|
166
|
+
//#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
|
+
//#region src/cli/wechatDevtoolsSettings.ts
|
|
286
|
+
function createStorageHash(key) {
|
|
287
|
+
return createHash("md5").update(key).digest("hex");
|
|
288
|
+
}
|
|
289
|
+
const SETTINGS_STORAGE_HASH = createStorageHash("reduxPersist:settings");
|
|
290
|
+
const SETTINGS_STORAGE_FILE_NAMES = [`localstorage_${SETTINGS_STORAGE_HASH}.json`, `ls_${SETTINGS_STORAGE_HASH}.json`];
|
|
291
|
+
function isRecord(value) {
|
|
292
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
293
|
+
}
|
|
294
|
+
function normalizePort(value) {
|
|
295
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return;
|
|
296
|
+
if (value <= 0 || value > 65535) return;
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
function normalizeWechatDevtoolsSecuritySettings(value) {
|
|
300
|
+
if (!isRecord(value)) return;
|
|
301
|
+
const normalized = {};
|
|
302
|
+
if (typeof value.enableServicePort === "boolean") normalized.enableServicePort = value.enableServicePort;
|
|
303
|
+
const port = normalizePort(value.port);
|
|
304
|
+
if (port !== void 0) normalized.port = port;
|
|
305
|
+
if (typeof value.allowGetTicket === "boolean") normalized.allowGetTicket = value.allowGetTicket;
|
|
306
|
+
if (typeof value.trustWhenAuto === "boolean") normalized.trustWhenAuto = value.trustWhenAuto;
|
|
307
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
308
|
+
}
|
|
309
|
+
async function resolveSelectedProfile(options) {
|
|
310
|
+
const platform = options.platform ?? process.platform;
|
|
311
|
+
if (!options.target && platform !== "darwin" && platform !== "win32") return;
|
|
312
|
+
return (await resolveWechatDevtoolsTarget(options)).profileDir;
|
|
313
|
+
}
|
|
314
|
+
async function readJsonObject(filePath) {
|
|
315
|
+
try {
|
|
316
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
317
|
+
const parsed = JSON.parse(raw);
|
|
318
|
+
if (!isRecord(parsed)) return {};
|
|
319
|
+
return parsed;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (error.code === "ENOENT") return {};
|
|
322
|
+
if (error instanceof SyntaxError) return {};
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function writeJsonObject(filePath, value) {
|
|
327
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
328
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
329
|
+
}
|
|
330
|
+
async function detectWechatDevtoolsSecuritySettings(localDataDir) {
|
|
331
|
+
for (const fileName of SETTINGS_STORAGE_FILE_NAMES) {
|
|
332
|
+
const security = normalizeWechatDevtoolsSecuritySettings((await readJsonObject(path.join(localDataDir, fileName))).security);
|
|
333
|
+
if (security) return security;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function trustWechatDevtoolsProject(localDataDir, projectPath) {
|
|
337
|
+
const normalizedProjectPath = path.resolve(projectPath);
|
|
338
|
+
const projectHash = createStorageHash(`project2_${normalizedProjectPath}`);
|
|
339
|
+
const fileNames = [`localstorage_${projectHash}.json`, `ls_${projectHash}.json`];
|
|
340
|
+
let trusted = false;
|
|
341
|
+
for (const fileName of fileNames) {
|
|
342
|
+
const projectFilePath = path.join(localDataDir, fileName);
|
|
343
|
+
const current = await readJsonObject(projectFilePath);
|
|
344
|
+
if (current.projectid !== normalizedProjectPath || current.projectpath !== normalizedProjectPath || typeof current.appid !== "string" || !current.appid || !isRecord(current.attr) || !isRecord(current.attr.setting)) continue;
|
|
345
|
+
await writeJsonObject(projectFilePath, {
|
|
346
|
+
...current,
|
|
347
|
+
isTrusted: true
|
|
348
|
+
});
|
|
349
|
+
trusted = true;
|
|
350
|
+
}
|
|
351
|
+
return trusted;
|
|
352
|
+
}
|
|
353
|
+
async function scanWechatDevtoolsServicePort(profileDir) {
|
|
354
|
+
const localDataDir = profileDir && path.join(profileDir, "WeappLocalData");
|
|
355
|
+
const exists = localDataDir && await fs.stat(localDataDir).then((stat) => stat.isDirectory(), (error) => {
|
|
356
|
+
if (error.code !== "ENOENT") throw error;
|
|
357
|
+
return false;
|
|
358
|
+
});
|
|
359
|
+
const security = exists && localDataDir ? await detectWechatDevtoolsSecuritySettings(localDataDir) : void 0;
|
|
360
|
+
return {
|
|
361
|
+
touchedInstanceCount: exists ? 1 : 0,
|
|
362
|
+
detectedSecurityCount: security ? 1 : 0,
|
|
363
|
+
servicePort: security?.port,
|
|
364
|
+
servicePortEnabled: security?.enableServicePort
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* @description 检测微信开发者工具当前服务端口配置,严格沿用用户已有设置。
|
|
369
|
+
*/
|
|
370
|
+
async function detectWechatDevtoolsServicePort(options = {}) {
|
|
371
|
+
return await scanWechatDevtoolsServicePort(await resolveSelectedProfile(options));
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* @description 在启动微信开发者工具前,检测服务端口配置,并按需写入项目信任信息。
|
|
375
|
+
*/
|
|
376
|
+
async function bootstrapWechatDevtoolsSettings(options = {}) {
|
|
377
|
+
const profileDir = await resolveSelectedProfile(options);
|
|
378
|
+
const result = await scanWechatDevtoolsServicePort(profileDir);
|
|
379
|
+
const trusted = profileDir && result.touchedInstanceCount && options.projectPath && options.trustProject === true ? await trustWechatDevtoolsProject(path.join(profileDir, "WeappLocalData"), options.projectPath) : false;
|
|
380
|
+
return {
|
|
381
|
+
...result,
|
|
382
|
+
updatedSecurityCount: 0,
|
|
383
|
+
trustedProjectCount: trusted ? 1 : 0
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/cli/automator.ts
|
|
388
|
+
const PROJECT_AUTOMATOR_PORT_BASE = 9620;
|
|
389
|
+
const PROJECT_AUTOMATOR_PORT_RANGE = 2e3;
|
|
390
|
+
/**
|
|
391
|
+
* @description 为项目路径派生稳定的 DevTools automator 端口,避免多个 dev:open 项目抢占默认端口。
|
|
392
|
+
*/
|
|
393
|
+
function resolveProjectAutomatorPort(projectPath) {
|
|
394
|
+
const offset = createHash("sha1").update(path.resolve(projectPath)).digest().readUInt32BE(0) % PROJECT_AUTOMATOR_PORT_RANGE;
|
|
395
|
+
return PROJECT_AUTOMATOR_PORT_BASE + offset;
|
|
396
|
+
}
|
|
397
|
+
async function launchSelectedAutomator(options) {
|
|
398
|
+
const { port, projectPath, sessionId, timeout = 3e4 } = options;
|
|
399
|
+
const lifecycle = new OperationLifecycle(timeout, "IDE launch", options.signal);
|
|
400
|
+
let managedLaunch;
|
|
401
|
+
try {
|
|
402
|
+
return await lifecycle.run(async (scope) => {
|
|
403
|
+
const { Launcher } = await scope.step(() => import("@weapp-vite/miniprogram-automator"), { stage: "load-automator" });
|
|
404
|
+
const { target, cliPath: resolvedCliPath, installationId, runtimeProvider } = await scope.step(() => resolveAutomatorSessionOptions(options), { stage: "resolve-cli" });
|
|
405
|
+
if (runtimeProvider === "headless") return await scope.step(() => new Launcher().launch({
|
|
406
|
+
projectPath,
|
|
407
|
+
runtimeProvider,
|
|
408
|
+
timeout: scope.remainingMs(),
|
|
409
|
+
signal: scope.signal
|
|
410
|
+
}), {
|
|
411
|
+
stage: "launch",
|
|
412
|
+
disposeLate: (program) => program.disconnect()
|
|
413
|
+
});
|
|
414
|
+
await scope.step(() => assertWechatDevtoolsHost(target, {
|
|
415
|
+
signal: scope.signal,
|
|
416
|
+
timeout: scope.remainingMs()
|
|
417
|
+
}), { stage: "host-identity" });
|
|
418
|
+
const config = await scope.step(() => readCustomConfig(), { stage: "configuration" });
|
|
419
|
+
const resolvedTrustProject = options.trustProject ?? config.autoTrustProject ?? false;
|
|
420
|
+
const launcher = new Launcher();
|
|
421
|
+
let lastError = null;
|
|
422
|
+
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");
|
|
429
|
+
managedLaunch = launchManagedAutomator({
|
|
430
|
+
launcher,
|
|
431
|
+
scope,
|
|
432
|
+
target,
|
|
433
|
+
projectPath: resolvedProject.projectPath,
|
|
434
|
+
sourceProjectPath: projectPath,
|
|
435
|
+
port: port ?? resolveProjectAutomatorPort(resolvedProject.projectPath),
|
|
436
|
+
sessionId,
|
|
437
|
+
trustProject: resolvedTrustProject,
|
|
438
|
+
persistAsDefaultSession: options.persistAsDefaultSession
|
|
439
|
+
});
|
|
440
|
+
return await managedLaunch;
|
|
441
|
+
}
|
|
442
|
+
if (config.autoBootstrapDevtools !== false) bootstrapResult = await scope.step(() => bootstrapWechatDevtoolsSettings({
|
|
443
|
+
target,
|
|
444
|
+
projectPath: resolvedProject.projectPath,
|
|
445
|
+
trustProject: resolvedTrustProject
|
|
446
|
+
}), { stage: "settings" });
|
|
447
|
+
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.");
|
|
448
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
449
|
+
scope.attempt();
|
|
450
|
+
try {
|
|
451
|
+
const miniProgram = await scope.step(() => launcher.launch({
|
|
452
|
+
cliPath: resolvedCliPath,
|
|
453
|
+
...port ? { port } : {},
|
|
454
|
+
projectPath: resolvedProject.projectPath,
|
|
455
|
+
timeout: scope.remainingMs(),
|
|
456
|
+
signal: scope.signal,
|
|
457
|
+
trustProject: resolvedTrustProject
|
|
458
|
+
}), {
|
|
459
|
+
stage: "launch",
|
|
460
|
+
disposeLate: (program) => program.disconnect()
|
|
461
|
+
});
|
|
462
|
+
scope.own(() => miniProgram.disconnect(), "automator-session");
|
|
463
|
+
const sessionMetadata = Reflect.get(miniProgram, "__WEAPP_VITE_SESSION_METADATA");
|
|
464
|
+
if (typeof sessionMetadata?.wsEndpoint === "string" && sessionMetadata.wsEndpoint) {
|
|
465
|
+
const wsEndpoint = sessionMetadata.wsEndpoint;
|
|
466
|
+
await scope.step(() => assertWechatDevtoolsPort(target, Number(new URL(wsEndpoint).port), {
|
|
467
|
+
signal: scope.signal,
|
|
468
|
+
timeout: scope.remainingMs()
|
|
469
|
+
}), { stage: "port-identity" });
|
|
470
|
+
await scope.step(() => persistAutomatorSession({
|
|
471
|
+
installationId,
|
|
472
|
+
signal: scope.signal,
|
|
473
|
+
...port ? { port: sessionMetadata.port ?? port } : {},
|
|
474
|
+
projectPath,
|
|
475
|
+
sessionId,
|
|
476
|
+
wsEndpoint
|
|
477
|
+
}), { stage: "session-persist" });
|
|
478
|
+
if (options.persistAsDefaultSession && (port || sessionId)) await scope.step(() => persistAutomatorSession({
|
|
479
|
+
installationId,
|
|
480
|
+
signal: scope.signal,
|
|
481
|
+
projectPath,
|
|
482
|
+
wsEndpoint
|
|
483
|
+
}), { stage: "session-persist" });
|
|
484
|
+
}
|
|
485
|
+
return miniProgram;
|
|
486
|
+
} catch (error) {
|
|
487
|
+
lastError = error;
|
|
488
|
+
scope.recordFailure(error);
|
|
489
|
+
scope.throwIfAborted();
|
|
490
|
+
if (!isRetryableAutomatorLaunchError(error) || attempt === 1) throw error;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
494
|
+
});
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (managedLaunch) await (await managedLaunch).close();
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* @description 基于当前配置解析 CLI 路径,并通过现代化 automator 入口启动会话。
|
|
502
|
+
*/
|
|
503
|
+
async function launchAutomator(options) {
|
|
504
|
+
options.signal?.throwIfAborted();
|
|
505
|
+
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));
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* @description 登记由外部 automator bridge 创建的会话,供 CLI 子进程安全复用。
|
|
509
|
+
*/
|
|
510
|
+
async function persistOpenedAutomatorSession(options) {
|
|
511
|
+
const resolved = await resolveAutomatorSessionOptions(options);
|
|
512
|
+
if (resolved.runtimeProvider === "headless" || !resolved.target) throw new Error("DEVTOOLS_SESSION_PROVIDER_INVALID: external automator sessions require the WeChat DevTools provider.");
|
|
513
|
+
const endpoint = new URL(options.wsEndpoint);
|
|
514
|
+
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.");
|
|
515
|
+
const endpointPort = Number(endpoint.port);
|
|
516
|
+
const port = options.port ?? endpointPort;
|
|
517
|
+
if (port !== endpointPort) throw new Error(`DEVTOOLS_SESSION_PORT_MISMATCH: endpoint=${endpointPort} option=${port}`);
|
|
518
|
+
await assertWechatDevtoolsPort(resolved.target, endpointPort, {
|
|
519
|
+
signal: options.signal,
|
|
520
|
+
timeout: options.timeout ?? 3e4
|
|
521
|
+
});
|
|
522
|
+
await persistAutomatorSession({
|
|
523
|
+
installationId: resolved.installationId,
|
|
524
|
+
projectPath: options.projectPath,
|
|
525
|
+
port,
|
|
526
|
+
...options.sessionId ? { sessionId: options.sessionId } : {},
|
|
527
|
+
signal: options.signal,
|
|
528
|
+
wsEndpoint: options.wsEndpoint
|
|
529
|
+
});
|
|
530
|
+
if (!options.port && !options.sessionId) await persistAutomatorSession({
|
|
531
|
+
installationId: resolved.installationId,
|
|
532
|
+
projectPath: options.projectPath,
|
|
533
|
+
signal: options.signal,
|
|
534
|
+
wsEndpoint: options.wsEndpoint
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* @description 只读连接当前项目已打开的自动化会话;失败不证明缓存过期,也不删除其他操作持有的会话记录。
|
|
539
|
+
*/
|
|
540
|
+
async function connectOpenedAutomator(options) {
|
|
541
|
+
const { port, projectPath, sessionId } = options;
|
|
542
|
+
return await new OperationLifecycle(options.timeout ?? 3e4, "IDE connect", options.signal).run(async (scope) => {
|
|
543
|
+
const { Launcher } = await scope.step(() => import("@weapp-vite/miniprogram-automator"), { stage: "load-automator" });
|
|
544
|
+
scope.attempt();
|
|
545
|
+
const launcher = new Launcher();
|
|
546
|
+
const resolved = await scope.step(() => resolveAutomatorSessionOptions(options), { stage: "resolve-cli" });
|
|
547
|
+
const persistedSession = await scope.step(() => readPersistedAutomatorSession({
|
|
548
|
+
projectPath,
|
|
549
|
+
sessionId,
|
|
550
|
+
port,
|
|
551
|
+
installationId: resolved.installationId
|
|
552
|
+
}), { stage: "session-lookup" });
|
|
553
|
+
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" });
|
|
554
|
+
const wsEndpoint = persistedSession.wsEndpoint;
|
|
555
|
+
await scope.step(() => assertWechatDevtoolsPort(resolved.target, Number(new URL(wsEndpoint).port), {
|
|
556
|
+
signal: scope.signal,
|
|
557
|
+
timeout: scope.remainingMs()
|
|
558
|
+
}), { stage: "port-identity" });
|
|
559
|
+
const miniProgram = await scope.step(async () => await launcher.connect({
|
|
560
|
+
timeout: scope.remainingMs(),
|
|
561
|
+
signal: scope.signal,
|
|
562
|
+
wsEndpoint
|
|
563
|
+
}), {
|
|
564
|
+
stage: "connect",
|
|
565
|
+
disposeLate: (program) => {
|
|
566
|
+
if (program && typeof program === "object" && "disconnect" in program && typeof program.disconnect === "function") program.disconnect();
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
if (process.env.WEAPP_IDE_MANAGED_PROJECT_JOURNAL) miniProgram.close = async () => {
|
|
570
|
+
miniProgram.disconnect();
|
|
571
|
+
};
|
|
572
|
+
return miniProgram;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
//#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 };
|