weapp-ide-cli 5.4.0 → 5.4.2

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.
@@ -9,6 +9,7 @@ import path$1 from "pathe";
9
9
  import logger, { colors } from "@weapp-core/logger";
10
10
  import { createHash } from "node:crypto";
11
11
  import { acquireSharedMiniProgram, closeSharedMiniProgram, getSharedMiniProgramSessionCount, releaseSharedMiniProgram, withMiniProgram } from "@weapp-vite/devtools-runtime";
12
+ import { emitKeypressEvents } from "node:readline";
12
13
  //#region src/utils/path.ts
13
14
  /**
14
15
  * @description 解析为绝对路径(基于当前工作目录)
@@ -577,7 +578,7 @@ async function removePersistedAutomatorSession(projectPath, sessionId, port) {
577
578
  /**
578
579
  * @description 提取登录失效时最适合展示给用户的一行信息。
579
580
  */
580
- function extractLoginRequiredMessage(text) {
581
+ function extractLoginRequiredMessage$1(text) {
581
582
  if (!text) return "";
582
583
  if (LOGIN_REQUIRED_CN_RE.test(text)) return "需要重新登录";
583
584
  const englishMatch = text.match(LOGIN_REQUIRED_EN_RE);
@@ -641,7 +642,7 @@ function isAutomatorLoginError(error) {
641
642
  function formatAutomatorLoginError(error) {
642
643
  const text = extractErrorText(error);
643
644
  const code = text.match(LOGIN_REQUIRED_CODE_RE)?.[1];
644
- const message = extractLoginRequiredMessage(text);
645
+ const message = extractLoginRequiredMessage$1(text);
645
646
  const lines = ["微信开发者工具返回登录错误:"];
646
647
  if (code) lines.push(`- code: ${code}`);
647
648
  if (message) lines.push(`- message: ${message}`);
@@ -767,7 +768,316 @@ function readLangOption(argv) {
767
768
  }
768
769
  }
769
770
  //#endregion
771
+ //#region src/cli/inputCoordinator.ts
772
+ const sharedSessions = /* @__PURE__ */ new Set();
773
+ const exclusiveKeypressStack = [];
774
+ const EXCLUSIVE_KEYPRESS_CARRYOVER_GUARD_MS = 500;
775
+ let initialized = false;
776
+ let rawModeEnabled = false;
777
+ let dataListenerAttached = false;
778
+ let keypressListenerAttached = false;
779
+ let lastExclusiveResolvedKeypress;
780
+ function hasInteractiveStdin() {
781
+ return Boolean(process.stdin?.isTTY);
782
+ }
783
+ function canSetRawMode() {
784
+ return typeof process.stdin?.setRawMode === "function";
785
+ }
786
+ function updateTerminalState() {
787
+ const shouldKeepInputActive = Array.from(sharedSessions).some((session) => !session.closed && !session.suspended) || exclusiveKeypressStack.length > 0;
788
+ if (canSetRawMode() && rawModeEnabled !== shouldKeepInputActive) {
789
+ process.stdin.setRawMode(shouldKeepInputActive);
790
+ rawModeEnabled = shouldKeepInputActive;
791
+ }
792
+ if (shouldKeepInputActive) {
793
+ process.stdin.resume();
794
+ return;
795
+ }
796
+ process.stdin.pause();
797
+ }
798
+ function handleData(chunk) {
799
+ if (exclusiveKeypressStack.length > 0) return;
800
+ for (const session of sharedSessions) {
801
+ if (session.closed || session.suspended || !session.options.onData) continue;
802
+ session.options.onData(chunk);
803
+ }
804
+ }
805
+ function handleKeypress(str, key) {
806
+ const activeExclusive = exclusiveKeypressStack[exclusiveKeypressStack.length - 1];
807
+ if (activeExclusive) {
808
+ const now = Date.now();
809
+ const signature = `${key?.ctrl ? "ctrl+" : ""}${key?.name ?? str}`;
810
+ if (lastExclusiveResolvedKeypress && lastExclusiveResolvedKeypress.signature === signature && now - lastExclusiveResolvedKeypress.timestamp <= EXCLUSIVE_KEYPRESS_CARRYOVER_GUARD_MS) {
811
+ lastExclusiveResolvedKeypress.timestamp = now;
812
+ return;
813
+ }
814
+ if (now < activeExclusive.ignoreUntil) return;
815
+ const nextValue = activeExclusive.onKeypress(str, key);
816
+ if (nextValue !== void 0) {
817
+ clearTimeout(activeExclusive.timeout);
818
+ exclusiveKeypressStack.pop();
819
+ lastExclusiveResolvedKeypress = {
820
+ signature,
821
+ timestamp: now
822
+ };
823
+ updateTerminalState();
824
+ activeExclusive.resolve(nextValue);
825
+ }
826
+ return;
827
+ }
828
+ for (const session of sharedSessions) {
829
+ if (session.closed || session.suspended || !session.options.onKeypress) continue;
830
+ session.options.onKeypress(str, key);
831
+ }
832
+ }
833
+ function ensureCoordinatorInitialized() {
834
+ if (initialized || !hasInteractiveStdin()) return;
835
+ emitKeypressEvents(process.stdin);
836
+ if (!dataListenerAttached) {
837
+ process.stdin.on("data", handleData);
838
+ dataListenerAttached = true;
839
+ }
840
+ if (!keypressListenerAttached) {
841
+ process.stdin.on("keypress", handleKeypress);
842
+ keypressListenerAttached = true;
843
+ }
844
+ initialized = true;
845
+ }
846
+ function createSharedInputSession(options) {
847
+ if (!hasInteractiveStdin()) return;
848
+ ensureCoordinatorInitialized();
849
+ const session = {
850
+ closed: false,
851
+ options,
852
+ suspended: false
853
+ };
854
+ sharedSessions.add(session);
855
+ updateTerminalState();
856
+ return {
857
+ close() {
858
+ if (session.closed) return;
859
+ session.closed = true;
860
+ sharedSessions.delete(session);
861
+ updateTerminalState();
862
+ },
863
+ resume() {
864
+ if (session.closed) return;
865
+ session.suspended = false;
866
+ updateTerminalState();
867
+ },
868
+ suspend() {
869
+ if (session.closed) return;
870
+ session.suspended = true;
871
+ updateTerminalState();
872
+ }
873
+ };
874
+ }
875
+ async function waitForExclusiveKeypress(options) {
876
+ if (!hasInteractiveStdin()) return "timeout";
877
+ ensureCoordinatorInitialized();
878
+ return await new Promise((resolve) => {
879
+ const normalizedTimeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 3e4;
880
+ const normalizedIgnoreInitialMs = Number.isFinite(options.ignoreInitialMs) && options.ignoreInitialMs && options.ignoreInitialMs > 0 ? options.ignoreInitialMs : 0;
881
+ const record = {
882
+ ignoreUntil: Date.now() + normalizedIgnoreInitialMs,
883
+ onKeypress: options.onKeypress,
884
+ resolve,
885
+ timeout: setTimeout(() => {
886
+ const index = exclusiveKeypressStack.lastIndexOf(record);
887
+ if (index >= 0) exclusiveKeypressStack.splice(index, 1);
888
+ updateTerminalState();
889
+ resolve("timeout");
890
+ }, normalizedTimeoutMs)
891
+ };
892
+ exclusiveKeypressStack.push(record);
893
+ updateTerminalState();
894
+ });
895
+ }
896
+ async function runWithSuspendedSharedInput(runner) {
897
+ const previousStates = /* @__PURE__ */ new Map();
898
+ for (const session of sharedSessions) {
899
+ if (session.closed) continue;
900
+ previousStates.set(session, session.suspended);
901
+ session.suspended = true;
902
+ }
903
+ if (canSetRawMode() && rawModeEnabled) {
904
+ process.stdin.setRawMode(false);
905
+ rawModeEnabled = false;
906
+ }
907
+ process.stdin.resume();
908
+ try {
909
+ return await runner();
910
+ } finally {
911
+ for (const [session, suspended] of previousStates) if (!session.closed) session.suspended = suspended;
912
+ updateTerminalState();
913
+ }
914
+ }
915
+ //#endregion
916
+ //#region src/cli/retry.ts
917
+ const RETRY_PROMPT_INITIAL_IGNORE_MS = 300;
918
+ const RETRY_CONFIRM_KEYS = ["y"];
919
+ const RETRY_CANCEL_KEYS = [
920
+ "q",
921
+ "Esc",
922
+ "Ctrl+C"
923
+ ];
924
+ const LOGIN_REQUIRED_PATTERNS = [
925
+ /code\s*[:=]\s*10/i,
926
+ /需要重新登录/,
927
+ /need\s+re-?login/i,
928
+ /re-?login/i
929
+ ];
930
+ /**
931
+ * @description 提取执行错误文本,便于统一匹配与提示。
932
+ */
933
+ function extractExecutionErrorText(error) {
934
+ if (!error || typeof error !== "object") return "";
935
+ const parts = [];
936
+ const candidate = error;
937
+ for (const field of [
938
+ candidate.message,
939
+ candidate.shortMessage,
940
+ candidate.stderr,
941
+ candidate.stdout
942
+ ]) if (typeof field === "string" && field.trim()) parts.push(field);
943
+ return parts.join("\n");
944
+ }
945
+ function extractLoginRequiredMessage(text) {
946
+ if (!text) return "";
947
+ if (/需要重新登录/.test(text)) return "需要重新登录";
948
+ const englishMatch = text.match(/need\s+re-?login|re-?login/i);
949
+ if (englishMatch?.[0]) return englishMatch[0].toLowerCase();
950
+ const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find((line) => Boolean(line) && !line.startsWith("at "));
951
+ if (!firstLine) return "";
952
+ return firstLine.replace(/^\[error\]\s*/i, "").replace(/^error\s*:\s*/i, "").slice(0, 120);
953
+ }
954
+ /**
955
+ * @description 判断是否为微信开发者工具登录失效错误。
956
+ */
957
+ function isWechatIdeLoginRequiredError(error) {
958
+ const text = extractExecutionErrorText(error);
959
+ if (!text) return false;
960
+ return LOGIN_REQUIRED_PATTERNS.some((pattern) => pattern.test(text));
961
+ }
962
+ /**
963
+ * @description 将登录失效错误格式化为更易读的摘要。
964
+ */
965
+ function formatWechatIdeLoginRequiredError(error) {
966
+ const text = extractExecutionErrorText(error);
967
+ const code = text.match(/code\s*[:=]\s*(\d+)/i)?.[1];
968
+ const message = extractLoginRequiredMessage(text);
969
+ const lines = ["微信开发者工具返回登录错误:"];
970
+ if (code) lines.push(`- code: ${code}`);
971
+ if (message) lines.push(`- message: ${message}`);
972
+ if (!code && !message) lines.push("- message: 需要重新登录");
973
+ return lines.join("\n");
974
+ }
975
+ /**
976
+ * @description 创建登录失效专用错误,并携带退出码语义。
977
+ */
978
+ function createWechatIdeLoginRequiredExitError(error, reason) {
979
+ const summary = formatWechatIdeLoginRequiredError(error);
980
+ const message = reason ? `${reason}\n${summary}` : summary;
981
+ const loginError = new Error(message);
982
+ loginError.name = "WechatIdeLoginRequiredError";
983
+ loginError.code = 10;
984
+ loginError.exitCode = 10;
985
+ return loginError;
986
+ }
987
+ /**
988
+ * @description 判断错误是否为登录失效专用退出错误。
989
+ */
990
+ function isWechatIdeLoginRequiredExitError(error) {
991
+ if (!error || typeof error !== "object") return false;
992
+ const candidate = error;
993
+ return candidate.name === "WechatIdeLoginRequiredError" || candidate.code === 10 || candidate.exitCode === 10;
994
+ }
995
+ /**
996
+ * @description 交互等待用户按 y 重试,按 q 或 Ctrl+C 取消。
997
+ */
998
+ async function waitForRetryKeypress(options = {}) {
999
+ const { timeoutMs = 3e4 } = options;
1000
+ if (!process.stdin.isTTY) return "cancel";
1001
+ return await waitForExclusiveKeypress({
1002
+ ignoreInitialMs: 300,
1003
+ onKeypress: (_str, key) => {
1004
+ if (!key) return;
1005
+ if (key.ctrl && key.name === "c") return "cancel";
1006
+ if (key.name === "y") return "retry";
1007
+ if (key.name === "q" || key.name === "escape") return "cancel";
1008
+ },
1009
+ timeoutMs
1010
+ });
1011
+ }
1012
+ function highlightHotkey(key) {
1013
+ return colors.bold(colors.green(key));
1014
+ }
1015
+ /**
1016
+ * @description 生成重试按键提示,并高亮关键热键。
1017
+ */
1018
+ function formatRetryHotkeyPrompt(timeoutMs = 3e4) {
1019
+ const highlight = (key) => highlightHotkey(key);
1020
+ const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1e3));
1021
+ const confirmKeys = RETRY_CONFIRM_KEYS.map(highlight).join(" / ");
1022
+ const cancelKeys = RETRY_CANCEL_KEYS.map(highlight).join(" / ");
1023
+ return i18nText(`按 ${confirmKeys} 重试,按 ${cancelKeys} 退出(${timeoutSeconds}s 内无输入将自动失败)。`, `Press ${confirmKeys} to retry, ${cancelKeys} to cancel (auto fail in ${timeoutSeconds}s).`);
1024
+ }
1025
+ /**
1026
+ * @description 输出重试热键提示,并独占等待当前提示对应的按键输入。
1027
+ */
1028
+ async function promptRetryKeypress(options) {
1029
+ const { logger, timeoutMs = 3e4 } = options;
1030
+ logger.info(formatRetryHotkeyPrompt(timeoutMs));
1031
+ return await waitForRetryKeypress({ timeoutMs });
1032
+ }
1033
+ /**
1034
+ * @description 统一处理微信开发者工具登录失效场景下的提示与重试交互。
1035
+ */
1036
+ async function promptWechatIdeLoginRetry(options) {
1037
+ const { allowRetry = true, cancelLevel = "info", error, logger, promptOpenIdeLogin = false, retryTimeoutMs = 3e4 } = options;
1038
+ logger.error(i18nText("检测到微信开发者工具登录状态失效,请先登录后重试。", "Wechat DevTools login has expired. Please login and retry."));
1039
+ if (promptOpenIdeLogin) logger.warn(i18nText("请先打开微信开发者工具完成登录。", "Please open Wechat DevTools and complete login first."));
1040
+ logger.warn(formatWechatIdeLoginRequiredError(error));
1041
+ if (!allowRetry) return "cancel";
1042
+ const action = await promptRetryKeypress({
1043
+ logger,
1044
+ timeoutMs: retryTimeoutMs
1045
+ });
1046
+ if (action === "timeout") {
1047
+ logger.error(i18nText(`等待登录重试输入超时(${retryTimeoutMs}ms),已自动取消。`, `Retry prompt timed out (${retryTimeoutMs}ms), canceled automatically.`));
1048
+ return "cancel";
1049
+ }
1050
+ if (action !== "retry") {
1051
+ logger[cancelLevel](i18nText("已取消重试。完成登录后请重新执行当前命令。", "Retry canceled. Please run the command again after login."));
1052
+ return "cancel";
1053
+ }
1054
+ return "retry";
1055
+ }
1056
+ //#endregion
1057
+ //#region src/cli/run-login-executor.ts
1058
+ /**
1059
+ * @description 执行可重试命令循环,并将“是否可重试”与“如何提示重试”交给调用方定义。
1060
+ */
1061
+ async function runRetryableCommand(options) {
1062
+ const { createCancelError, execute, isRetryableResult, onCancel, onRetry, promptRetry, shouldRetry } = options;
1063
+ let retryCount = 0;
1064
+ while (true) {
1065
+ const result = await execute();
1066
+ if (!isRetryableResult(result)) return result;
1067
+ if (shouldRetry(await promptRetry(result, retryCount))) {
1068
+ retryCount += 1;
1069
+ onRetry?.();
1070
+ continue;
1071
+ }
1072
+ onCancel?.(result);
1073
+ throw createCancelError(result);
1074
+ }
1075
+ }
1076
+ //#endregion
770
1077
  //#region src/cli/automator-session.ts
1078
+ function unwrapAutomatorConnectionError(result) {
1079
+ return result.kind === "retryable" ? result.error : result.value;
1080
+ }
771
1081
  function normalizeMiniProgramConnectionError(error) {
772
1082
  if (isAutomatorLoginError(error)) {
773
1083
  logger_default.error(i18nText("检测到微信开发者工具登录状态失效,请先登录后重试。", "Wechat DevTools login has expired. Please login and retry."));
@@ -801,22 +1111,56 @@ function normalizeMiniProgramConnectionError(error) {
801
1111
  * @description 建立 automator 会话,并统一处理常见连接错误提示。
802
1112
  */
803
1113
  async function connectMiniProgram(options) {
804
- if (options.preferOpenedSession === false) try {
805
- return await launchAutomator(options);
806
- } catch (error) {
807
- throw normalizeMiniProgramConnectionError(error);
808
- }
809
- try {
810
- return await connectOpenedAutomator(options);
811
- } catch (error) {
812
- const normalizedOpenSessionError = normalizeMiniProgramConnectionError(error);
813
- if (normalizedOpenSessionError instanceof Error && normalizedOpenSessionError.message === "DEVTOOLS_PROTOCOL_TIMEOUT") throw normalizedOpenSessionError;
814
- try {
815
- return await launchAutomator(options);
816
- } catch (launchError) {
817
- throw normalizeMiniProgramConnectionError(launchError);
818
- }
819
- }
1114
+ const result = await runRetryableCommand({
1115
+ createCancelError: (result) => createWechatIdeLoginRequiredExitError(unwrapAutomatorConnectionError(result), "cancelled"),
1116
+ execute: async () => {
1117
+ if (options.preferOpenedSession === false) try {
1118
+ return {
1119
+ kind: "result",
1120
+ value: await launchAutomator(options)
1121
+ };
1122
+ } catch (error) {
1123
+ if (!isAutomatorLoginError(error)) throw normalizeMiniProgramConnectionError(error);
1124
+ return {
1125
+ error,
1126
+ kind: "retryable"
1127
+ };
1128
+ }
1129
+ try {
1130
+ return {
1131
+ kind: "result",
1132
+ value: await connectOpenedAutomator(options)
1133
+ };
1134
+ } catch (error) {
1135
+ const normalizedOpenSessionError = normalizeMiniProgramConnectionError(error);
1136
+ if (normalizedOpenSessionError instanceof Error && normalizedOpenSessionError.message === "DEVTOOLS_PROTOCOL_TIMEOUT") throw normalizedOpenSessionError;
1137
+ try {
1138
+ return {
1139
+ kind: "result",
1140
+ value: await launchAutomator(options)
1141
+ };
1142
+ } catch (launchError) {
1143
+ if (!isAutomatorLoginError(launchError)) throw normalizeMiniProgramConnectionError(launchError);
1144
+ return {
1145
+ error: launchError,
1146
+ kind: "retryable"
1147
+ };
1148
+ }
1149
+ }
1150
+ },
1151
+ isRetryableResult: (result) => result.kind === "retryable",
1152
+ onRetry: () => {
1153
+ logger_default.info(i18nText("正在重试连接微信开发者工具...", "Retrying to connect Wechat DevTools..."));
1154
+ },
1155
+ promptRetry: async (result) => await promptWechatIdeLoginRetry({
1156
+ error: unwrapAutomatorConnectionError(result),
1157
+ logger: logger_default,
1158
+ promptOpenIdeLogin: true
1159
+ }),
1160
+ shouldRetry: (action) => action === "retry"
1161
+ });
1162
+ if (result.kind === "retryable") throw createWechatIdeLoginRequiredExitError(result.error);
1163
+ return result.value;
820
1164
  }
821
1165
  const runtimeHooks = {
822
1166
  connectMiniProgram,
@@ -835,4 +1179,4 @@ async function withMiniProgram$1(options, runner) {
835
1179
  return await withMiniProgram(runtimeHooks, options, runner);
836
1180
  }
837
1181
  //#endregion
838
- export { colors as A, defaultCustomConfigFilePath as B, getConfig as C, getDefaultCliPath as D, SupportedPlatformsMap as E, createLocaleConfig as F, overwriteCustomConfig as I, readCustomConfig as L, createAutoBootstrapDevtoolsConfig as M, createAutoTrustProjectConfig as N, isOperatingSystemSupported as O, createCustomConfig as P, removeCustomConfigKey as R, resolveCliPath as S, resolveDevtoolsAutomationDefaults as T, resolvePath as V, isDevtoolsHttpPortError as _, releaseSharedMiniProgram as a, bootstrapWechatDevtoolsSettings as b, i18nText as c, formatAutomatorLoginError as d, getAutomatorProtocolTimeoutMethod as f, isDevtoolsExtensionContextInvalidatedError as g, isAutomatorWsConnectError as h, getSharedMiniProgramSessionCount as i, logger_default as j, operatingSystemName as k, validateLocaleOption as l, isAutomatorProtocolTimeoutError as m, closeSharedMiniProgram as n, withMiniProgram$1 as o, isAutomatorLoginError as p, connectMiniProgram as r, configureLocaleFromArgv as s, acquireSharedMiniProgram$1 as t, connectOpenedAutomator as u, isRetryableAutomatorLaunchError as v, getConfiguredLocale as w, detectWechatDevtoolsServicePort as x, launchAutomator as y, defaultCustomConfigDirPath as z };
1182
+ export { readCustomConfig as $, isAutomatorProtocolTimeoutError as A, getConfiguredLocale as B, configureLocaleFromArgv as C, formatAutomatorLoginError as D, connectOpenedAutomator as E, launchAutomator as F, operatingSystemName as G, SupportedPlatformsMap as H, bootstrapWechatDevtoolsSettings as I, createAutoBootstrapDevtoolsConfig as J, colors as K, detectWechatDevtoolsServicePort as L, isDevtoolsExtensionContextInvalidatedError as M, isDevtoolsHttpPortError as N, getAutomatorProtocolTimeoutMethod as O, isRetryableAutomatorLaunchError as P, overwriteCustomConfig as Q, resolveCliPath as R, waitForExclusiveKeypress as S, validateLocaleOption as T, getDefaultCliPath as U, resolveDevtoolsAutomationDefaults as V, isOperatingSystemSupported as W, createCustomConfig as X, createAutoTrustProjectConfig as Y, createLocaleConfig as Z, promptRetryKeypress as _, releaseSharedMiniProgram as a, createSharedInputSession as b, RETRY_CANCEL_KEYS as c, createWechatIdeLoginRequiredExitError as d, removeCustomConfigKey as et, extractExecutionErrorText as f, isWechatIdeLoginRequiredExitError as g, isWechatIdeLoginRequiredError as h, getSharedMiniProgramSessionCount as i, isAutomatorWsConnectError as j, isAutomatorLoginError as k, RETRY_CONFIRM_KEYS as l, formatWechatIdeLoginRequiredError as m, closeSharedMiniProgram as n, defaultCustomConfigFilePath as nt, withMiniProgram$1 as o, formatRetryHotkeyPrompt as p, logger_default as q, connectMiniProgram as r, resolvePath as rt, runRetryableCommand as s, acquireSharedMiniProgram$1 as t, defaultCustomConfigDirPath as tt, RETRY_PROMPT_INITIAL_IGNORE_MS as u, promptWechatIdeLoginRetry as v, i18nText as w, runWithSuspendedSharedInput as x, waitForRetryKeypress as y, getConfig as z };