+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/unlanzi_d200x/command_executor/com.ulanzi.commandexecutor.ulanziPlugin/property-inspector/inspector.js b/plugins/unlanzi_d200x/command_executor/com.ulanzi.commandexecutor.ulanziPlugin/property-inspector/inspector.js
new file mode 100644
index 0000000..cb9e291
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/com.ulanzi.commandexecutor.ulanziPlugin/property-inspector/inspector.js
@@ -0,0 +1,86 @@
+(function () {
+ "use strict";
+
+ const ACTION_UUID =
+ "com.ulanzi.ulanzistudio.commandexecutor.runcommand";
+ const {
+ normalizeSettings,
+ validateEnvironmentText,
+ buildExecutionPreview
+ } = globalThis.CommandExecutorSettings;
+ const api = $UD;
+ let currentSettings = normalizeSettings();
+ let form = null;
+ let validationMessage = null;
+ let executionPreview = null;
+ let dirty = false;
+
+ function updateDiagnostics() {
+ validationMessage.textContent = validateEnvironmentText(
+ currentSettings.environment,
+ api.language
+ );
+ executionPreview.textContent = buildExecutionPreview(
+ currentSettings,
+ api.language
+ );
+ }
+
+ function renderSettings() {
+ if (!form) {
+ return;
+ }
+ Utils.setFormValue(currentSettings, form);
+ updateDiagnostics();
+ }
+
+ function applyIncomingSettings(value) {
+ if (dirty) {
+ return;
+ }
+ currentSettings = normalizeSettings(value);
+ renderSettings();
+ }
+
+ function captureSettings() {
+ currentSettings = normalizeSettings(Utils.getFormValue(form));
+ dirty = true;
+ updateDiagnostics();
+ }
+
+ function flushSettings() {
+ if (!dirty) {
+ return;
+ }
+ api.sendParamFromPlugin(currentSettings);
+ dirty = false;
+ }
+
+ const debouncedFlush = Utils.debounce(flushSettings, 200);
+
+ function handleInput() {
+ captureSettings();
+ debouncedFlush();
+ }
+
+ function handleChange() {
+ captureSettings();
+ flushSettings();
+ }
+
+ api.connect(ACTION_UUID);
+ api.onConnected(() => {
+ form = document.querySelector("#property-inspector");
+ validationMessage = document.querySelector("#validation-message");
+ executionPreview = document.querySelector("#execution-preview");
+ document
+ .querySelector(".udpi-wrapper")
+ .classList.remove("hidden");
+ form.addEventListener("input", handleInput);
+ form.addEventListener("change", handleChange);
+ renderSettings();
+ });
+ api.onAdd((message) => applyIncomingSettings(message?.param));
+ api.onParamFromApp((message) => applyIncomingSettings(message?.param));
+ window.addEventListener("pagehide", flushSettings);
+}());
diff --git a/plugins/unlanzi_d200x/command_executor/com.ulanzi.commandexecutor.ulanziPlugin/property-inspector/settings.js b/plugins/unlanzi_d200x/command_executor/com.ulanzi.commandexecutor.ulanziPlugin/property-inspector/settings.js
new file mode 100644
index 0000000..e7bdf5e
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/com.ulanzi.commandexecutor.ulanziPlugin/property-inspector/settings.js
@@ -0,0 +1,134 @@
+(function () {
+ "use strict";
+
+ const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
+ const DEFAULT_SETTINGS = Object.freeze({
+ title: "执行命令",
+ command: "",
+ workingDirectory: "",
+ environment: ""
+ });
+ const TEXT = Object.freeze({
+ en: Object.freeze({
+ environmentLine: "Environment variable line",
+ nul: "contains an unsupported NUL character",
+ missingEquals: "is missing an equals sign",
+ invalidName: "has an invalid name",
+ workingDirectory: "Working directory",
+ environment: "Environment variables",
+ command: "Command",
+ none: "None",
+ empty: "(empty)"
+ }),
+ zh_CN: Object.freeze({
+ environmentLine: "环境变量第",
+ nul: "行包含不支持的 NUL 字符",
+ missingEquals: "行缺少等号",
+ invalidName: "行格式错误",
+ workingDirectory: "工作目录",
+ environment: "环境变量",
+ command: "命令",
+ none: "无",
+ empty: "(空)"
+ })
+ });
+
+ function getText(locale) {
+ return typeof locale === "string" &&
+ locale.toLowerCase().startsWith("en")
+ ? TEXT.en
+ : TEXT.zh_CN;
+ }
+
+ function normalizeSettings(value = {}) {
+ const settings = value && typeof value === "object" ? value : {};
+ const title =
+ typeof settings.title === "string" ? settings.title.trim() : "";
+
+ return {
+ title: title || DEFAULT_SETTINGS.title,
+ command:
+ typeof settings.command === "string" ? settings.command : "",
+ workingDirectory:
+ typeof settings.workingDirectory === "string"
+ ? settings.workingDirectory
+ : "",
+ environment:
+ typeof settings.environment === "string" ? settings.environment : ""
+ };
+ }
+
+ function validateEnvironmentText(text = "", locale = "zh_CN") {
+ const lines = String(text).split(/\r?\n/);
+ const messages = getText(locale);
+ const messageForLine = (line, message) =>
+ `${messages.environmentLine} ${line} ${message}`;
+
+ for (let index = 0; index < lines.length; index += 1) {
+ const line = lines[index];
+ if (line.trim() === "") {
+ continue;
+ }
+ if (line.includes("\0")) {
+ return messageForLine(index + 1, messages.nul);
+ }
+
+ const separator = line.indexOf("=");
+ if (separator < 0) {
+ return messageForLine(index + 1, messages.missingEquals);
+ }
+
+ const name = line.slice(0, separator).trim();
+ if (!ENVIRONMENT_NAME.test(name)) {
+ return messageForLine(index + 1, messages.invalidName);
+ }
+ }
+
+ return "";
+ }
+
+ function listEnvironmentNames(text) {
+ const names = [];
+ const knownNames = new Set();
+
+ String(text).split(/\r?\n/).forEach((line) => {
+ const separator = line.indexOf("=");
+ const name = separator >= 0 ? line.slice(0, separator).trim() : "";
+ if (
+ !line.includes("\0") &&
+ ENVIRONMENT_NAME.test(name) &&
+ !knownNames.has(name)
+ ) {
+ knownNames.add(name);
+ names.push(name);
+ }
+ });
+
+ return names;
+ }
+
+ function buildExecutionPreview(value = {}, locale = "zh_CN") {
+ const settings = normalizeSettings(value);
+ const environmentNames = listEnvironmentNames(settings.environment);
+ const messages = getText(locale);
+
+ return [
+ "Shell: $SHELL -lc",
+ `${messages.workingDirectory}: ${settings.workingDirectory || "$HOME"}`,
+ `${messages.environment}: ${
+ environmentNames.length > 0
+ ? environmentNames.join(", ")
+ : messages.none
+ }`,
+ `${messages.command}:`,
+ settings.command || messages.empty
+ ].join("\n");
+ }
+
+ globalThis.CommandExecutorSettings = Object.freeze({
+ DEFAULT_SETTINGS,
+ normalizeSettings,
+ validateEnvironmentText,
+ buildExecutionPreview
+ });
+}());
diff --git a/plugins/unlanzi_d200x/command_executor/com.ulanzi.commandexecutor.ulanziPlugin/zh_CN.json b/plugins/unlanzi_d200x/command_executor/com.ulanzi.commandexecutor.ulanziPlugin/zh_CN.json
new file mode 100644
index 0000000..8c8ff07
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/com.ulanzi.commandexecutor.ulanziPlugin/zh_CN.json
@@ -0,0 +1,17 @@
+{
+ "Localization": {
+ "page.title": "命令执行器",
+ "field.title.label": "按键标题",
+ "field.title.placeholder": "执行命令",
+ "field.command.label": "命令(含参数)",
+ "field.command.placeholder": "输入完整 Shell 命令",
+ "field.command.hint": "支持完整 Shell 语法、多行命令及包含 100~200 个参数的长命令。",
+ "field.cwd.label": "工作目录",
+ "field.cwd.placeholder": "留空使用 $HOME,或填写绝对路径、~/...",
+ "field.environment.label": "环境变量",
+ "field.environment.placeholder": "每行一个 NAME=VALUE",
+ "field.environment.hint": "按第一个等号拆分;同名变量以后出现的值为准。",
+ "preview.title": "执行预览",
+ "security.notice": "安全提示:命令将以当前 macOS 用户权限执行,保存前请确认命令来源可信。"
+ }
+}
diff --git a/plugins/unlanzi_d200x/command_executor/package-lock.json b/plugins/unlanzi_d200x/command_executor/package-lock.json
new file mode 100644
index 0000000..e2158a2
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/package-lock.json
@@ -0,0 +1,1621 @@
+{
+ "name": "life-tools-ulanzi-command-executor",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "life-tools-ulanzi-command-executor",
+ "version": "0.1.0",
+ "dependencies": {
+ "ws": "8.18.0"
+ },
+ "devDependencies": {
+ "webpack": "5.94.0",
+ "webpack-cli": "5.1.4"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@discoveryjs/json-ext": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
+ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "26.1.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
+ "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
+ },
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webpack-cli/configtest": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
+ "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/info": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
+ "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/serve": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
+ "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-attributes": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz",
+ "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.7",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
+ "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.44",
+ "caniuse-lite": "^1.0.30001806",
+ "electron-to-chromium": "^1.5.393",
+ "node-releases": "^2.0.51",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001806",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+ "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.396",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz",
+ "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.24.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz",
+ "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/envinfo": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz",
+ "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "envinfo": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
+ "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/interpret": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/loader-runner": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz",
+ "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.11.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.51",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
+ "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+ "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "^1.20.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/schema-utils": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+ "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.8",
+ "ajv": "^6.12.5",
+ "ajv-keywords": "^3.5.2"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.49.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz",
+ "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz",
+ "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^4.3.0",
+ "terser": "^5.31.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@minify-html/node": {
+ "optional": true
+ },
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/css": {
+ "optional": true
+ },
+ "@swc/html": {
+ "optional": true
+ },
+ "clean-css": {
+ "optional": true
+ },
+ "cssnano": {
+ "optional": true
+ },
+ "csso": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "html-minifier-terser": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/terser-webpack-plugin/node_modules/schema-utils": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
+ "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack": {
+ "version": "5.94.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
+ "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.5",
+ "@webassemblyjs/ast": "^1.12.1",
+ "@webassemblyjs/wasm-edit": "^1.12.1",
+ "@webassemblyjs/wasm-parser": "^1.12.1",
+ "acorn": "^8.7.1",
+ "acorn-import-attributes": "^1.9.5",
+ "browserslist": "^4.21.10",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.17.1",
+ "es-module-lexer": "^1.2.1",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.2.0",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^3.2.0",
+ "tapable": "^2.1.1",
+ "terser-webpack-plugin": "^5.3.10",
+ "watchpack": "^2.4.1",
+ "webpack-sources": "^3.2.3"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
+ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@discoveryjs/json-ext": "^0.5.0",
+ "@webpack-cli/configtest": "^2.1.1",
+ "@webpack-cli/info": "^2.0.2",
+ "@webpack-cli/serve": "^2.0.5",
+ "colorette": "^2.0.14",
+ "commander": "^10.0.1",
+ "cross-spawn": "^7.0.3",
+ "envinfo": "^7.7.3",
+ "fastest-levenshtein": "^1.0.12",
+ "import-local": "^3.0.2",
+ "interpret": "^3.1.1",
+ "rechoir": "^0.8.0",
+ "webpack-merge": "^5.7.3"
+ },
+ "bin": {
+ "webpack-cli": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "@webpack-cli/generators": {
+ "optional": true
+ },
+ "webpack-bundle-analyzer": {
+ "optional": true
+ },
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli/node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/webpack-merge": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
+ "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-deep": "^4.0.1",
+ "flat": "^5.0.2",
+ "wildcard": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz",
+ "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wildcard": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
+ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/unlanzi_d200x/command_executor/package.json b/plugins/unlanzi_d200x/command_executor/package.json
new file mode 100644
index 0000000..9fd9a6b
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "life-tools-ulanzi-command-executor",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "engines": {
+ "node": ">=20"
+ },
+ "scripts": {
+ "test": "node --test tests/*.test.mjs",
+ "bundle": "webpack --config webpack.config.js",
+ "build": "./build.sh"
+ },
+ "dependencies": {
+ "ws": "8.18.0"
+ },
+ "devDependencies": {
+ "webpack": "5.94.0",
+ "webpack-cli": "5.1.4"
+ }
+}
diff --git a/plugins/unlanzi_d200x/command_executor/scripts/validate-package.mjs b/plugins/unlanzi_d200x/command_executor/scripts/validate-package.mjs
new file mode 100644
index 0000000..e24b6ca
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/scripts/validate-package.mjs
@@ -0,0 +1,316 @@
+import {
+ lstat,
+ readFile,
+ readdir,
+ realpath
+} from "node:fs/promises";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const PLUGIN_NAME = "com.ulanzi.commandexecutor.ulanziPlugin";
+const COMMAND_EXECUTOR_ROOT = path.resolve(
+ path.dirname(fileURLToPath(import.meta.url)),
+ ".."
+);
+const SOURCE_PLUGIN_ROOT = path.join(COMMAND_EXECUTOR_ROOT, PLUGIN_NAME);
+const WORKTREE_ROOT = path.resolve(COMMAND_EXECUTOR_ROOT, "../../..");
+const REQUIRED_VENDOR_FILES = [
+ "plugin/vendor/ulanzi-api/constants.js",
+ "plugin/vendor/ulanzi-api/ulanziApi.js",
+ "plugin/vendor/ulanzi-api/utils.js",
+ "libs/assets/u_active.svg",
+ "libs/assets/u_active_none.svg",
+ "libs/assets/u_check_checkbox.svg",
+ "libs/assets/u_check_none.svg",
+ "libs/assets/u_check_radio.svg",
+ "libs/assets/u_down.svg",
+ "libs/assets/u_file.svg",
+ "libs/assets/u_folder.svg",
+ "libs/assets/u_refresh.svg",
+ "libs/assets/u_tip_error.svg",
+ "libs/assets/u_tip_info.svg",
+ "libs/assets/u_tip_success.svg",
+ "libs/assets/u_tip_warn.svg",
+ "libs/css/uspi.css",
+ "libs/js/constants.js",
+ "libs/js/eventEmitter.js",
+ "libs/js/timers.js",
+ "libs/js/ulanziApi.js",
+ "libs/js/utils.js"
+];
+const REQUIRED_RUNTIME_FILES = [
+ "manifest.json",
+ "package.json",
+ "THIRD_PARTY_NOTICES.md",
+ "LICENSES/UlanziDeckPlugin-SDK-APACHE-2.0.txt",
+ "plugin/app.js",
+ "plugin/command-plugin.js",
+ "plugin/command-runner.js",
+ "property-inspector/inspector.html",
+ "property-inspector/inspector.css",
+ "property-inspector/settings.js",
+ "property-inspector/inspector.js",
+ "en.json",
+ "zh_CN.json",
+ ...REQUIRED_VENDOR_FILES
+];
+const SDK_COMMITS = [
+ "112bd13a7ff9d45bd68656f7e069fd61851d1812",
+ "79de0b0b087546e684afd23f97223f7a7bc392da",
+ "550ab80c69285ecf259bd494a7fff767c14f0c0f"
+];
+
+function isInside(root, candidate) {
+ const relative = path.relative(root, candidate);
+ return (
+ relative === "" ||
+ (relative !== ".." &&
+ !relative.startsWith(`..${path.sep}`) &&
+ !path.isAbsolute(relative))
+ );
+}
+
+async function readJson(filePath, label) {
+ let source;
+ try {
+ source = await readFile(filePath, "utf8");
+ } catch (error) {
+ throw new Error(`${label} is missing or unreadable: ${error.message}`);
+ }
+
+ try {
+ return JSON.parse(source);
+ } catch (error) {
+ throw new Error(`${label} is not valid JSON: ${error.message}`);
+ }
+}
+
+async function requireNonEmptyFile(filePath, label) {
+ let fileStat;
+ try {
+ fileStat = await lstat(filePath);
+ } catch {
+ throw new Error(`${label} must be a non-empty regular file`);
+ }
+ if (fileStat.isSymbolicLink()) {
+ throw new Error(`symbolic links are forbidden: ${label}`);
+ }
+ if (!fileStat.isFile() || fileStat.size === 0) {
+ throw new Error(`${label} must be a non-empty regular file`);
+ }
+}
+
+function collectManifestResources(manifest) {
+ if (!Array.isArray(manifest.Actions)) {
+ throw new Error("manifest Actions must be an array");
+ }
+
+ const resources = [
+ ["Icon", manifest.Icon],
+ ["CategoryIcon", manifest.CategoryIcon],
+ ["CodePath", manifest.CodePath]
+ ];
+ manifest.Actions.forEach((action, actionIndex) => {
+ resources.push(
+ [`Actions[${actionIndex}].Icon`, action?.Icon],
+ [
+ `Actions[${actionIndex}].PropertyInspectorPath`,
+ action?.PropertyInspectorPath
+ ]
+ );
+ if (!Array.isArray(action?.States)) {
+ throw new Error(`manifest Actions[${actionIndex}].States must be an array`);
+ }
+ action.States.forEach((state, stateIndex) => {
+ resources.push([
+ `Actions[${actionIndex}].States[${stateIndex}].Image`,
+ state?.Image
+ ]);
+ });
+ });
+ return resources;
+}
+
+async function validateManifestResources(packageDirectory, manifest) {
+ for (const [label, relativePath] of collectManifestResources(manifest)) {
+ if (
+ typeof relativePath !== "string" ||
+ relativePath.length === 0 ||
+ path.isAbsolute(relativePath) ||
+ path.win32.isAbsolute(relativePath)
+ ) {
+ throw new Error(`manifest resource path escapes package: ${label}`);
+ }
+
+ const resourcePath = path.resolve(packageDirectory, relativePath);
+ if (!isInside(packageDirectory, resourcePath)) {
+ throw new Error(`manifest resource path escapes package: ${label}`);
+ }
+ await requireNonEmptyFile(
+ resourcePath,
+ `manifest resource ${relativePath}`
+ );
+ const realResourcePath = await realpath(resourcePath);
+ if (!isInside(packageDirectory, realResourcePath)) {
+ throw new Error(`manifest resource path escapes package: ${label}`);
+ }
+ }
+}
+
+function isForbidden(relativePath, entryName, isDirectory) {
+ if (
+ entryName === ".DS_Store" ||
+ entryName.startsWith("._") ||
+ entryName === "__MACOSX" ||
+ entryName === "node_modules"
+ ) {
+ return true;
+ }
+ if (isDirectory && (entryName === "test" || entryName === "tests")) {
+ return true;
+ }
+ return (
+ relativePath.endsWith(".map") ||
+ /(?:^|[._-])(?:test|spec)\.[^/]+$/i.test(entryName)
+ );
+}
+
+async function validatePackageEntries(packageDirectory, relativeDirectory = "") {
+ const directory = path.join(packageDirectory, relativeDirectory);
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
+ const relativePath = path.join(relativeDirectory, entry.name);
+ const entryStat = await lstat(path.join(packageDirectory, relativePath));
+ if (entry.isSymbolicLink() || entryStat.isSymbolicLink()) {
+ throw new Error(`symbolic links are forbidden: ${relativePath}`);
+ }
+ if (isForbidden(relativePath, entry.name, entry.isDirectory())) {
+ throw new Error(`forbidden package entry: ${relativePath}`);
+ }
+ if (entry.isDirectory()) {
+ await validatePackageEntries(packageDirectory, relativePath);
+ }
+ }
+}
+
+async function validateDist(packageDirectory) {
+ const distPath = path.join(packageDirectory, "dist");
+ let distStat;
+ try {
+ distStat = await lstat(distPath);
+ } catch {
+ throw new Error("dist must contain only app.js as a regular file");
+ }
+ if (distStat.isSymbolicLink()) {
+ throw new Error("symbolic links are forbidden: dist");
+ }
+ if (!distStat.isDirectory()) {
+ throw new Error("dist must contain only app.js as a regular file");
+ }
+
+ const entries = await readdir(distPath, { withFileTypes: true });
+ if (
+ entries.length !== 1 ||
+ entries[0].name !== "app.js" ||
+ !entries[0].isFile()
+ ) {
+ throw new Error("dist must contain only app.js as a regular file");
+ }
+ await requireNonEmptyFile(path.join(distPath, "app.js"), "dist/app.js");
+}
+
+function containsPathAtBoundary(source, absolutePath) {
+ let searchFrom = 0;
+ while (searchFrom < source.length) {
+ const index = source.indexOf(absolutePath, searchFrom);
+ if (index === -1) {
+ return false;
+ }
+ const nextCharacter = source[index + absolutePath.length];
+ if (
+ nextCharacter === undefined ||
+ /[\/\\\s'"`()\]{},;:?#]/.test(nextCharacter)
+ ) {
+ return true;
+ }
+ searchFrom = index + absolutePath.length;
+ }
+ return false;
+}
+
+async function validateBundlePaths(packageDirectory) {
+ const bundle = await readFile(
+ path.join(packageDirectory, "dist", "app.js"),
+ "utf8"
+ );
+ const forbiddenRoots = new Set([
+ SOURCE_PLUGIN_ROOT,
+ COMMAND_EXECUTOR_ROOT,
+ WORKTREE_ROOT
+ ]);
+ for (const absolutePath of forbiddenRoots) {
+ const pathSegments = path.relative(
+ path.parse(absolutePath).root,
+ absolutePath
+ ).split(path.sep).filter(Boolean);
+ if (
+ pathSegments.length >= 3 &&
+ containsPathAtBoundary(bundle, absolutePath)
+ ) {
+ throw new Error("dist/app.js contains absolute build path");
+ }
+ }
+}
+
+async function validatePackage(packageArgument) {
+ if (!packageArgument) {
+ throw new Error(
+ `usage: node scripts/validate-package.mjs <${PLUGIN_NAME} directory>`
+ );
+ }
+
+ const requestedDirectory = path.resolve(packageArgument);
+ if (path.basename(requestedDirectory) !== PLUGIN_NAME) {
+ throw new Error(`package basename must be ${PLUGIN_NAME}`);
+ }
+ const packageStat = await lstat(requestedDirectory);
+ if (packageStat.isSymbolicLink()) {
+ throw new Error("symbolic links are forbidden: package root");
+ }
+ if (!packageStat.isDirectory()) {
+ throw new Error("package path must be a directory");
+ }
+ const packageDirectory = await realpath(requestedDirectory);
+
+ await validatePackageEntries(packageDirectory);
+ for (const relativePath of REQUIRED_RUNTIME_FILES) {
+ await requireNonEmptyFile(
+ path.join(packageDirectory, relativePath),
+ relativePath
+ );
+ }
+
+ const manifest = await readJson(
+ path.join(packageDirectory, "manifest.json"),
+ "manifest.json"
+ );
+ await readJson(path.join(packageDirectory, "package.json"), "package.json");
+ await validateManifestResources(packageDirectory, manifest);
+ await validateDist(packageDirectory);
+ await validateBundlePaths(packageDirectory);
+
+ const noticesPath = path.join(packageDirectory, "THIRD_PARTY_NOTICES.md");
+ const notices = await readFile(noticesPath, "utf8");
+ for (const commit of SDK_COMMITS) {
+ if (!notices.includes(commit)) {
+ throw new Error(`THIRD_PARTY_NOTICES.md must declare SDK commit ${commit}`);
+ }
+ }
+}
+
+try {
+ await validatePackage(process.argv[2]);
+ console.log(`Package validation passed: ${path.resolve(process.argv[2])}`);
+} catch (error) {
+ console.error(`Package validation failed: ${error.message}`);
+ process.exitCode = 1;
+}
diff --git a/plugins/unlanzi_d200x/command_executor/tests/command-plugin.test.mjs b/plugins/unlanzi_d200x/command_executor/tests/command-plugin.test.mjs
new file mode 100644
index 0000000..5966501
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/tests/command-plugin.test.mjs
@@ -0,0 +1,638 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { registerCommandPlugin } from "../com.ulanzi.commandexecutor.ulanziPlugin/plugin/command-plugin.js";
+import UlanziApi from "../com.ulanzi.commandexecutor.ulanziPlugin/plugin/vendor/ulanzi-api/ulanziApi.js";
+
+const EVENT_METHODS = {
+ add: "onAdd",
+ paramFromApp: "onParamFromApp",
+ paramFromPlugin: "onParamFromPlugin",
+ run: "onRun",
+ setActive: "onSetActive",
+ clear: "onClear",
+ error: "onError"
+};
+
+class FakeUlanziApi {
+ constructor() {
+ this.callbacks = new Map();
+ this.states = [];
+ this.alerts = [];
+ this.toasts = [];
+ this.logs = [];
+ }
+
+ register(method, callback) {
+ assert.equal(typeof callback, "function");
+ this.callbacks.set(method, callback);
+ return this;
+ }
+
+ onAdd(callback) {
+ return this.register("onAdd", callback);
+ }
+
+ onParamFromApp(callback) {
+ return this.register("onParamFromApp", callback);
+ }
+
+ onParamFromPlugin(callback) {
+ return this.register("onParamFromPlugin", callback);
+ }
+
+ onRun(callback) {
+ return this.register("onRun", callback);
+ }
+
+ onSetActive(callback) {
+ return this.register("onSetActive", callback);
+ }
+
+ onClear(callback) {
+ return this.register("onClear", callback);
+ }
+
+ onError(callback) {
+ return this.register("onError", callback);
+ }
+
+ setStateIcon(context, state, text) {
+ this.states.push({ context, state, text });
+ }
+
+ showAlert(context) {
+ this.alerts.push(context);
+ }
+
+ toast(message) {
+ this.toasts.push(message);
+ }
+
+ logMessage(message, level) {
+ this.logs.push({ message, level });
+ }
+
+ trigger(event, payload) {
+ const method = EVENT_METHODS[event];
+ const callback = this.callbacks.get(method);
+ assert.ok(callback, `${method} callback was not registered`);
+ return callback(payload);
+ }
+}
+
+class Deferred {
+ constructor() {
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ }
+}
+
+function createFakeTimers() {
+ let nextId = 1;
+ const timers = new Map();
+ const cleared = [];
+
+ return {
+ cleared,
+ setTimeoutFn(callback, delay) {
+ const token = nextId;
+ nextId += 1;
+ timers.set(token, { callback, delay });
+ return token;
+ },
+ clearTimeoutFn(token) {
+ cleared.push(token);
+ timers.delete(token);
+ },
+ pending() {
+ return [...timers.entries()].map(([token, timer]) => ({
+ token,
+ delay: timer.delay
+ }));
+ },
+ fire(token) {
+ const timer = timers.get(token);
+ assert.ok(timer, `timer ${token} does not exist`);
+ timers.delete(token);
+ timer.callback();
+ }
+ };
+}
+
+function successResult(overrides = {}) {
+ return {
+ code: 0,
+ signal: null,
+ stdout: "",
+ stderr: "",
+ stdoutTruncated: false,
+ stderrTruncated: false,
+ spawnError: null,
+ ...overrides
+ };
+}
+
+async function settleAsyncWork() {
+ await new Promise((resolve) => setImmediate(resolve));
+ await new Promise((resolve) => setImmediate(resolve));
+}
+
+function statesFor(api, context) {
+ return api.states
+ .filter((entry) => entry.context === context)
+ .map(({ state, text }) => ({ state, text }));
+}
+
+test("registers every required official SDK event", () => {
+ const api = new FakeUlanziApi();
+
+ registerCommandPlugin(api, {
+ runCommandFn: async () => successResult()
+ });
+
+ assert.deepEqual(
+ [...api.callbacks.keys()].sort(),
+ Object.values(EVENT_METHODS).sort()
+ );
+});
+
+test("handles the official SDK error event with a local diagnostic", () => {
+ const api = new UlanziApi();
+ const diagnostics = [];
+ const originalConsoleError = console.error;
+ console.error = (...parts) => {
+ diagnostics.push(parts.map(String).join(" "));
+ };
+
+ try {
+ registerCommandPlugin(api);
+
+ assert.doesNotThrow(() => {
+ api.emit("error", "connection refused");
+ });
+ assert.deepEqual(diagnostics, [
+ "[Ulanzi] 连接错误: connection refused"
+ ]);
+ } finally {
+ console.error = originalConsoleError;
+ }
+});
+
+test("routes normalized snapshots independently by context", async () => {
+ const api = new FakeUlanziApi();
+ const calls = [];
+ registerCommandPlugin(api, {
+ runCommandFn: async (settings) => {
+ calls.push(settings);
+ return successResult();
+ }
+ });
+
+ api.trigger("add", {
+ context: "context-a",
+ param: {
+ title: " 第一项 ",
+ command: " printf 'a' ",
+ workingDirectory: "/tmp/a",
+ environment: "A=1"
+ }
+ });
+ api.trigger("add", {
+ context: "context-b",
+ param: {
+ title: "第二项",
+ command: "printf 'b'",
+ workingDirectory: "/tmp/b",
+ environment: "B=2"
+ }
+ });
+ api.trigger("run", { context: "context-a" });
+ api.trigger("run", { context: "context-b" });
+ await settleAsyncWork();
+
+ assert.deepEqual(calls, [
+ {
+ title: "第一项",
+ command: " printf 'a' ",
+ workingDirectory: "/tmp/a",
+ environment: "A=1"
+ },
+ {
+ title: "第二项",
+ command: "printf 'b'",
+ workingDirectory: "/tmp/b",
+ environment: "B=2"
+ }
+ ]);
+ assert.deepEqual(statesFor(api, "context-a").slice(0, 2), [
+ { state: 0, text: "第一项" },
+ { state: 1, text: "第一项" }
+ ]);
+ assert.deepEqual(statesFor(api, "context-b").slice(0, 2), [
+ { state: 0, text: "第二项" },
+ { state: 1, text: "第二项" }
+ ]);
+});
+
+test("plugin updates affect only later runs and explicit empty command clears", async () => {
+ const api = new FakeUlanziApi();
+ const calls = [];
+ registerCommandPlugin(api, {
+ runCommandFn: async (settings) => {
+ calls.push(settings);
+ return successResult();
+ }
+ });
+
+ api.trigger("add", {
+ context: "context-a",
+ param: {
+ title: "原始",
+ command: "old",
+ workingDirectory: "/tmp/old",
+ environment: "OLD=1"
+ }
+ });
+ api.trigger("paramFromPlugin", {
+ context: "context-a",
+ param: {
+ title: " ",
+ command: "new"
+ }
+ });
+ api.trigger("run", { context: "context-a", param: {} });
+ await settleAsyncWork();
+
+ assert.deepEqual(calls[0], {
+ title: "执行命令",
+ command: "new",
+ workingDirectory: "/tmp/old",
+ environment: "OLD=1"
+ });
+
+ api.trigger("paramFromPlugin", {
+ context: "context-a",
+ param: { command: "" }
+ });
+ api.trigger("run", { context: "context-a" });
+ await settleAsyncWork();
+
+ assert.equal(calls[1].command, "");
+});
+
+test("run can initialize uncached context from params and reports validation throws", async () => {
+ const api = new FakeUlanziApi();
+ const calls = [];
+ registerCommandPlugin(api, {
+ runCommandFn: async (settings) => {
+ calls.push(settings);
+ if (settings.command === "") {
+ throw new Error("命令不能为空");
+ }
+ return successResult();
+ }
+ });
+
+ api.trigger("run", {
+ context: "created-on-run",
+ param: {
+ title: " 即时执行 ",
+ command: "date",
+ workingDirectory: null,
+ environment: 42
+ }
+ });
+ api.trigger("run", { context: "empty-on-run" });
+ await settleAsyncWork();
+
+ assert.deepEqual(calls, [
+ {
+ title: "即时执行",
+ command: "date",
+ workingDirectory: "",
+ environment: ""
+ },
+ {
+ title: "执行命令",
+ command: "",
+ workingDirectory: "",
+ environment: ""
+ }
+ ]);
+ assert.deepEqual(api.alerts, ["empty-on-run"]);
+ assert.match(api.toasts[0], /命令不能为空/);
+ assert.equal(api.logs.at(-1).level, "error");
+ assert.match(api.logs.at(-1).message, /validation error: 命令不能为空/);
+ assert.deepEqual(statesFor(api, "empty-on-run"), [
+ { state: 1, text: "执行命令" },
+ { state: 0, text: "执行命令" }
+ ]);
+});
+
+test("a rejected runner is contained and cannot create an unhandled rejection", async () => {
+ const api = new FakeUlanziApi();
+ const unhandled = [];
+ const onUnhandled = (reason) => {
+ unhandled.push(reason);
+ };
+ process.on("unhandledRejection", onUnhandled);
+
+ try {
+ registerCommandPlugin(api, {
+ runCommandFn: async () => {
+ throw new Error("runner rejected");
+ }
+ });
+ api.trigger("add", {
+ context: "context-a",
+ param: { command: "private-command", environment: "SECRET=value" }
+ });
+
+ const callbackResult = api.trigger("run", { context: "context-a" });
+ assert.equal(callbackResult, undefined);
+ await settleAsyncWork();
+
+ assert.deepEqual(unhandled, []);
+ assert.deepEqual(api.alerts, ["context-a"]);
+ assert.equal(api.logs[0].level, "error");
+ assert.match(api.logs[0].message, /validation error: runner rejected/);
+ assert.deepEqual(statesFor(api, "context-a").slice(-2), [
+ { state: 1, text: "执行命令" },
+ { state: 0, text: "执行命令" }
+ ]);
+ } finally {
+ process.off("unhandledRejection", onUnhandled);
+ }
+});
+
+test("spawn errors, nonzero exits, and signals are failures with safe feedback", async () => {
+ const api = new FakeUlanziApi();
+ const results = [
+ successResult({ code: null, spawnError: new Error("spawn EACCES") }),
+ successResult({ code: 7 }),
+ successResult({ code: null, signal: "SIGTERM" })
+ ];
+ registerCommandPlugin(api, {
+ runCommandFn: async () => results.shift()
+ });
+ api.trigger("add", {
+ context: "context-a",
+ param: {
+ command: "do-not-show-this-command",
+ environment: "SECRET=do-not-show-this-value"
+ }
+ });
+
+ for (let index = 0; index < 3; index += 1) {
+ api.trigger("run", { context: "context-a" });
+ await settleAsyncWork();
+ }
+
+ assert.equal(api.alerts.length, 3);
+ assert.equal(api.toasts.length, 3);
+ assert.match(api.toasts[0], /启动失败/);
+ assert.match(api.toasts[1], /退出码 7/);
+ assert.match(api.toasts[2], /SIGTERM/);
+ assert.doesNotMatch(
+ api.toasts.join("\n"),
+ /do-not-show-this-command|do-not-show-this-value/
+ );
+ assert.match(api.logs[0].message, /spawn error: spawn EACCES/);
+ assert.match(api.logs[1].message, /code: 7/);
+ assert.match(api.logs[2].message, /signal: SIGTERM/);
+ assert.ok(api.logs.every((entry) => entry.level === "error"));
+});
+
+test("logs bounded output and restores a successful state after 1200 ms", async () => {
+ const api = new FakeUlanziApi();
+ const timers = createFakeTimers();
+ registerCommandPlugin(api, {
+ runCommandFn: async () =>
+ successResult({
+ stdout: "standard output",
+ stderr: "standard error",
+ stdoutTruncated: true,
+ stderrTruncated: true
+ }),
+ setTimeoutFn: timers.setTimeoutFn,
+ clearTimeoutFn: timers.clearTimeoutFn
+ });
+ api.trigger("add", {
+ context: "context-a",
+ param: { title: "成功按钮", command: "run" }
+ });
+
+ api.trigger("run", { context: "context-a" });
+ await settleAsyncWork();
+
+ assert.deepEqual(statesFor(api, "context-a"), [
+ { state: 0, text: "成功按钮" },
+ { state: 1, text: "成功按钮" },
+ { state: 2, text: "成功按钮" }
+ ]);
+ assert.deepEqual(timers.pending().map(({ delay }) => delay), [1200]);
+ assert.equal(api.logs[0].level, "info");
+ assert.match(api.logs[0].message, /context: context-a/);
+ assert.match(api.logs[0].message, /code: 0/);
+ assert.match(api.logs[0].message, /signal: none/);
+ assert.match(api.logs[0].message, /stdout: standard output/);
+ assert.match(api.logs[0].message, /stderr: standard error/);
+ assert.match(api.logs[0].message, /stdout 输出已截断/);
+ assert.match(api.logs[0].message, /stderr 输出已截断/);
+
+ timers.fire(timers.pending()[0].token);
+ assert.deepEqual(statesFor(api, "context-a").at(-1), {
+ state: 0,
+ text: "成功按钮"
+ });
+});
+
+test("keeps running state until all concurrent runs succeed", async () => {
+ const api = new FakeUlanziApi();
+ const first = new Deferred();
+ const second = new Deferred();
+ const pending = [first, second];
+ registerCommandPlugin(api, {
+ runCommandFn: () => pending.shift().promise
+ });
+ api.trigger("add", {
+ context: "context-a",
+ param: { title: "并发", command: "run" }
+ });
+
+ api.trigger("run", { context: "context-a" });
+ api.trigger("run", { context: "context-a" });
+ first.resolve(successResult());
+ await settleAsyncWork();
+
+ const statesAfterFirst = statesFor(api, "context-a");
+ assert.deepEqual(statesAfterFirst.at(-1), { state: 1, text: "并发" });
+ assert.equal(statesAfterFirst.some(({ state }) => state === 2), false);
+
+ second.resolve(successResult());
+ await settleAsyncWork();
+ assert.deepEqual(statesFor(api, "context-a").at(-1), {
+ state: 2,
+ text: "并发"
+ });
+});
+
+test("one concurrent failure prevents a final success state", async () => {
+ const api = new FakeUlanziApi();
+ const first = new Deferred();
+ const second = new Deferred();
+ const pending = [first, second];
+ registerCommandPlugin(api, {
+ runCommandFn: () => pending.shift().promise
+ });
+ api.trigger("add", {
+ context: "context-a",
+ param: { title: "并发", command: "run" }
+ });
+
+ api.trigger("run", { context: "context-a" });
+ api.trigger("run", { context: "context-a" });
+ first.resolve(successResult({ code: 2 }));
+ second.resolve(successResult());
+ await settleAsyncWork();
+
+ const states = statesFor(api, "context-a");
+ assert.deepEqual(states.at(-1), { state: 0, text: "并发" });
+ assert.equal(states.some(({ state }) => state === 2), false);
+});
+
+test("running commands keep their settings snapshot across later edits", async () => {
+ const api = new FakeUlanziApi();
+ const deferred = new Deferred();
+ const calls = [];
+ registerCommandPlugin(api, {
+ runCommandFn: (settings) => {
+ calls.push(settings);
+ return deferred.promise;
+ }
+ });
+ api.trigger("add", {
+ context: "context-a",
+ param: {
+ title: "旧标题",
+ command: "old",
+ workingDirectory: "/tmp/old",
+ environment: "OLD=1"
+ }
+ });
+
+ api.trigger("run", { context: "context-a" });
+ api.trigger("paramFromApp", {
+ context: "context-a",
+ param: {
+ title: "新标题",
+ command: "new",
+ workingDirectory: "/tmp/new",
+ environment: "NEW=1"
+ }
+ });
+
+ assert.deepEqual(calls[0], {
+ title: "旧标题",
+ command: "old",
+ workingDirectory: "/tmp/old",
+ environment: "OLD=1"
+ });
+ deferred.resolve(successResult());
+ await settleAsyncWork();
+
+ assert.deepEqual(statesFor(api, "context-a").at(-1), {
+ state: 2,
+ text: "新标题"
+ });
+});
+
+test("clear is safe, clears timers, and suppresses late run UI updates", async () => {
+ const api = new FakeUlanziApi();
+ const timers = createFakeTimers();
+ const successfulRun = new Deferred();
+ const lateRun = new Deferred();
+ const pending = [successfulRun, lateRun];
+ registerCommandPlugin(api, {
+ runCommandFn: () => pending.shift().promise,
+ setTimeoutFn: timers.setTimeoutFn,
+ clearTimeoutFn: timers.clearTimeoutFn
+ });
+
+ assert.doesNotThrow(() => {
+ api.trigger("clear", { param: [{ context: "missing" }] });
+ });
+
+ api.trigger("add", {
+ context: "context-a",
+ param: { command: "first" }
+ });
+ api.trigger("run", { context: "context-a" });
+ successfulRun.resolve(successResult());
+ await settleAsyncWork();
+ const successTimer = timers.pending()[0].token;
+
+ api.trigger("paramFromApp", {
+ context: "context-a",
+ param: { command: "second" }
+ });
+ assert.deepEqual(timers.cleared, [successTimer]);
+
+ api.trigger("run", { context: "context-a" });
+ const stateCountBeforeClear = api.states.length;
+ api.trigger("clear", { param: [{ context: "context-a" }] });
+ lateRun.resolve(successResult({ code: 9 }));
+ await settleAsyncWork();
+
+ assert.equal(api.states.length, stateCountBeforeClear);
+ assert.deepEqual(api.alerts, []);
+ assert.deepEqual(api.toasts, []);
+});
+
+test("active redraws the current state and can initialize from settings", () => {
+ const api = new FakeUlanziApi();
+ registerCommandPlugin(api, {
+ runCommandFn: async () => successResult()
+ });
+ api.trigger("add", {
+ context: "context-a",
+ param: { title: "已有", command: "old" }
+ });
+
+ api.trigger("setActive", { context: "context-a", active: false });
+ api.trigger("setActive", { context: "context-a", active: true });
+ api.trigger("setActive", {
+ context: "context-b",
+ active: true,
+ param: { title: " 新建 ", command: "new" }
+ });
+
+ assert.deepEqual(statesFor(api, "context-a"), [
+ { state: 0, text: "已有" },
+ { state: 0, text: "已有" }
+ ]);
+ assert.deepEqual(statesFor(api, "context-b"), [
+ { state: 0, text: "新建" }
+ ]);
+});
+
+test("an empty successful result still writes a completion log", async () => {
+ const api = new FakeUlanziApi();
+ registerCommandPlugin(api, {
+ runCommandFn: async () => successResult()
+ });
+ api.trigger("add", {
+ context: "context-a",
+ param: { command: "true" }
+ });
+
+ api.trigger("run", { context: "context-a" });
+ await settleAsyncWork();
+
+ assert.equal(api.logs.length, 1);
+ assert.equal(api.logs[0].level, "info");
+ assert.match(api.logs[0].message, /命令执行完成/);
+});
diff --git a/plugins/unlanzi_d200x/command_executor/tests/command-runner.test.mjs b/plugins/unlanzi_d200x/command_executor/tests/command-runner.test.mjs
new file mode 100644
index 0000000..b8364cc
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/tests/command-runner.test.mjs
@@ -0,0 +1,744 @@
+import assert from "node:assert/strict";
+import { EventEmitter } from "node:events";
+import { constants as fileSystemConstants } from "node:fs";
+import { mkdtemp, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import { PassThrough } from "node:stream";
+import test from "node:test";
+
+import {
+ OUTPUT_LIMIT_BYTES,
+ buildShellScript,
+ parseEnvironment,
+ quoteShellValue,
+ runCommand,
+ resolveShell,
+ resolveWorkingDirectory
+} from "../com.ulanzi.commandexecutor.ulanziPlugin/plugin/command-runner.js";
+
+test("exports a 16 KiB default output limit", () => {
+ assert.equal(OUTPUT_LIMIT_BYTES, 16 * 1024);
+});
+
+test("parseEnvironment accepts empty input and ignores empty CRLF lines", () => {
+ assert.deepEqual(parseEnvironment(), {});
+ assert.deepEqual(parseEnvironment("\r\n \r\n"), {});
+ assert.deepEqual(parseEnvironment("FIRST=one\r\n\r\nSECOND=two\r\n"), {
+ FIRST: "one",
+ SECOND: "two"
+ });
+});
+
+test("parseEnvironment splits on the first equals and lets the last name win", () => {
+ assert.deepEqual(
+ parseEnvironment(" TOKEN =first=value\nTOKEN=last=value=kept"),
+ { TOKEN: "last=value=kept" }
+ );
+});
+
+test("parseEnvironment preserves __proto__ as an own enumerable variable", () => {
+ const parsed = parseEnvironment("__proto__=first\n__proto__=explicit");
+
+ assert.equal(Object.hasOwn(parsed, "__proto__"), true);
+ assert.deepEqual(Object.keys(parsed), ["__proto__"]);
+ assert.equal(parsed.__proto__, "explicit");
+ assert.equal(
+ buildShellScript("printf '%s' \"$__proto__\"", parsed),
+ "export __proto__='explicit'\nprintf '%s' \"$__proto__\""
+ );
+});
+
+test("parseEnvironment reports an invalid name by line without leaking its value", () => {
+ assert.throws(
+ () => parseEnvironment("VALID=ok\nBAD-NAME=do-not-leak"),
+ (error) => {
+ assert.match(error.message, /2/);
+ assert.doesNotMatch(error.message, /do-not-leak/);
+ return true;
+ }
+ );
+});
+
+test("parseEnvironment reports a NUL value by line without leaking its value", () => {
+ assert.throws(
+ () => parseEnvironment("VALID=ok\nSECRET=before\0after"),
+ (error) => {
+ assert.match(error.message, /2/);
+ assert.doesNotMatch(error.message, /before|after/);
+ return true;
+ }
+ );
+});
+
+test("quoteShellValue preserves single quotes and empty values", () => {
+ assert.equal(quoteShellValue("a'b"), "'a'\"'\"'b'");
+ assert.equal(quoteShellValue(""), "''");
+});
+
+test("buildShellScript exports configured values before the original command", () => {
+ const command = "printf '%s' \"$VALUE\"";
+
+ assert.equal(
+ buildShellScript(command, { VALUE: "a'b", EMPTY: "" }),
+ `export VALUE='a'"'"'b'\nexport EMPTY=''\n${command}`
+ );
+ assert.equal(buildShellScript(command, {}), command);
+});
+
+test("buildShellScript rejects blank and NUL-containing commands", () => {
+ assert.throws(() => buildShellScript(" \n", {}), /命令不能为空/);
+ assert.throws(() => buildShellScript("printf x\0ignored", {}), /NUL/);
+});
+
+function createFileSystem({
+ directories = [],
+ files,
+ executablePaths = []
+} = {}) {
+ const statCalls = [];
+ const accessCalls = [];
+ const regularFiles =
+ files ?? executablePaths.filter((path) => !directories.includes(path));
+
+ return {
+ statCalls,
+ accessCalls,
+ async stat(path) {
+ statCalls.push(path);
+ if (!directories.includes(path) && !regularFiles.includes(path)) {
+ throw new Error("path does not exist");
+ }
+ return {
+ isDirectory() {
+ return directories.includes(path);
+ },
+ isFile() {
+ return regularFiles.includes(path);
+ }
+ };
+ },
+ async access(path, mode) {
+ accessCalls.push([path, mode]);
+ if (!executablePaths.includes(path)) {
+ throw new Error("path is not executable");
+ }
+ }
+ };
+}
+
+test("resolveWorkingDirectory expands home forms and accepts absolute paths", async () => {
+ const homeDirectory = "/Users/example";
+ const fileSystem = createFileSystem({
+ directories: [homeDirectory, "/Users/example/work", "/private/tmp/work"],
+ executablePaths: [homeDirectory, "/Users/example/work", "/private/tmp/work"]
+ });
+
+ assert.equal(
+ await resolveWorkingDirectory("", { fileSystem, homeDirectory }),
+ homeDirectory
+ );
+ assert.equal(
+ await resolveWorkingDirectory("~", { fileSystem, homeDirectory }),
+ homeDirectory
+ );
+ assert.equal(
+ await resolveWorkingDirectory("~/work", { fileSystem, homeDirectory }),
+ "/Users/example/work"
+ );
+ assert.equal(
+ await resolveWorkingDirectory("/private/tmp/work", {
+ fileSystem,
+ homeDirectory
+ }),
+ "/private/tmp/work"
+ );
+ assert.deepEqual(
+ fileSystem.accessCalls.map(([, mode]) => mode),
+ Array(4).fill(fileSystemConstants.X_OK)
+ );
+});
+
+test("resolveWorkingDirectory rejects relative and named-home paths before stat", async () => {
+ const fileSystem = createFileSystem();
+ const options = { fileSystem, homeDirectory: "/Users/example" };
+
+ await assert.rejects(resolveWorkingDirectory("relative/path", options), /绝对路径/);
+ await assert.rejects(resolveWorkingDirectory("~other/work", options), /绝对路径/);
+ assert.deepEqual(fileSystem.statCalls, []);
+});
+
+test("resolveWorkingDirectory rejects non-directories and inaccessible directories", async () => {
+ const notDirectory = {
+ async stat() {
+ return {
+ isDirectory() {
+ return false;
+ }
+ };
+ },
+ async access() {
+ assert.fail("access must not run for a non-directory");
+ }
+ };
+ const inaccessible = createFileSystem({ directories: ["/private/tmp/locked"] });
+
+ await assert.rejects(
+ resolveWorkingDirectory("/private/tmp/file", {
+ fileSystem: notDirectory,
+ homeDirectory: "/Users/example"
+ }),
+ /目录/
+ );
+ await assert.rejects(
+ resolveWorkingDirectory("/private/tmp/locked", {
+ fileSystem: inaccessible,
+ homeDirectory: "/Users/example"
+ }),
+ /访问/
+ );
+});
+
+test("resolveShell uses an executable absolute SHELL candidate", async () => {
+ const fileSystem = createFileSystem({
+ executablePaths: ["/opt/homebrew/bin/zsh"]
+ });
+
+ assert.equal(
+ await resolveShell({
+ fileSystem,
+ baseEnvironment: { SHELL: "/opt/homebrew/bin/zsh" }
+ }),
+ "/opt/homebrew/bin/zsh"
+ );
+ assert.deepEqual(fileSystem.accessCalls, [
+ ["/opt/homebrew/bin/zsh", fileSystemConstants.X_OK]
+ ]);
+});
+
+test("resolveShell falls back for missing, relative, or inaccessible candidates", async () => {
+ for (const shell of [undefined, "bin/zsh", "/missing/shell"]) {
+ const fileSystem = createFileSystem({ executablePaths: ["/bin/zsh"] });
+
+ assert.equal(
+ await resolveShell({
+ fileSystem,
+ baseEnvironment: shell === undefined ? {} : { SHELL: shell }
+ }),
+ "/bin/zsh"
+ );
+ }
+});
+
+test(
+ "resolveShell rejects an executable directory candidate and falls back",
+ { skip: process.platform !== "darwin" },
+ async (t) => {
+ const temporaryDirectory = await mkdtemp(
+ path.join(tmpdir(), "command-runner-shell-dir-")
+ );
+ t.after(async () => {
+ await rm(temporaryDirectory, { recursive: true, force: true });
+ });
+
+ assert.equal(
+ await resolveShell({
+ baseEnvironment: { SHELL: temporaryDirectory }
+ }),
+ "/bin/zsh"
+ );
+ }
+);
+
+test("runCommand rejects a directory fallback before spawning", async () => {
+ let spawnCalls = 0;
+ const fileSystem = createFileSystem({
+ directories: ["/Users/example", "/bin/zsh"],
+ executablePaths: ["/Users/example", "/bin/zsh"]
+ });
+
+ await assert.rejects(
+ runCommand(
+ { command: "printf ok", workingDirectory: "", environment: "" },
+ {
+ spawnFn() {
+ spawnCalls += 1;
+ return createChildProcess();
+ },
+ fileSystem,
+ homeDirectory: "/Users/example",
+ baseEnvironment: { SHELL: "relative/zsh" }
+ }
+ ),
+ /Shell/
+ );
+ assert.equal(spawnCalls, 0);
+});
+
+test("resolveShell fails when the fallback is not executable", async () => {
+ const fileSystem = createFileSystem();
+
+ await assert.rejects(
+ resolveShell({
+ fileSystem,
+ baseEnvironment: { SHELL: "/missing/shell" }
+ }),
+ /Shell/
+ );
+});
+
+function createChildProcess({
+ stdoutChunks = [],
+ stderrChunks = [],
+ code = 0,
+ signal = null,
+ spawnError = null,
+ closeAfterError = false
+} = {}) {
+ const child = new EventEmitter();
+ child.stdout = new PassThrough();
+ child.stderr = new PassThrough();
+
+ queueMicrotask(() => {
+ for (const chunk of stdoutChunks) {
+ child.stdout.write(chunk);
+ }
+ for (const chunk of stderrChunks) {
+ child.stderr.write(chunk);
+ }
+ child.stdout.end();
+ child.stderr.end();
+
+ if (spawnError) {
+ child.emit("error", spawnError);
+ if (closeAfterError) {
+ child.emit("close", code, signal);
+ }
+ return;
+ }
+ child.emit("close", code, signal);
+ });
+
+ return child;
+}
+
+test("runCommand passes the complete script and exact spawn options", async () => {
+ const baseEnvironment = {
+ SHELL: "/custom/zsh",
+ INHERITED: "kept"
+ };
+ const fileSystem = createFileSystem({
+ directories: ["/Users/example"],
+ executablePaths: ["/Users/example", "/custom/zsh"]
+ });
+ const calls = [];
+ const spawnFn = (...args) => {
+ calls.push(args);
+ return createChildProcess({
+ stdoutChunks: ["done"],
+ stderrChunks: ["warning"],
+ code: null,
+ signal: "SIGTERM"
+ });
+ };
+
+ const result = await runCommand(
+ {
+ command: "printf '%s' \"$VALUE\" | cat",
+ workingDirectory: "",
+ environment: "VALUE=a'b"
+ },
+ {
+ spawnFn,
+ fileSystem,
+ homeDirectory: "/Users/example",
+ baseEnvironment
+ }
+ );
+
+ assert.equal(calls.length, 1);
+ assert.deepEqual(calls[0], [
+ "/custom/zsh",
+ [
+ "-lc",
+ `export VALUE='a'"'"'b'\nprintf '%s' "$VALUE" | cat`
+ ],
+ {
+ cwd: "/Users/example",
+ env: baseEnvironment,
+ stdio: ["ignore", "pipe", "pipe"]
+ }
+ ]);
+ assert.deepEqual(result, {
+ code: null,
+ signal: "SIGTERM",
+ stdout: "done",
+ stderr: "warning",
+ stdoutTruncated: false,
+ stderrTruncated: false,
+ spawnError: null
+ });
+});
+
+test("runCommand drains streams, truncates each by bytes, and decodes after collection", async () => {
+ const multiByte = Buffer.from("你");
+ const fileSystem = createFileSystem({
+ directories: ["/Users/example"],
+ executablePaths: ["/Users/example", "/bin/zsh"]
+ });
+ const spawnFn = () =>
+ createChildProcess({
+ stdoutChunks: [
+ multiByte.subarray(0, 1),
+ multiByte.subarray(1),
+ Buffer.from("abcdef")
+ ],
+ stderrChunks: [Buffer.from("123"), Buffer.from("456")]
+ });
+
+ const result = await runCommand(
+ { command: "ignored by fake", workingDirectory: "", environment: "" },
+ {
+ spawnFn,
+ fileSystem,
+ homeDirectory: "/Users/example",
+ baseEnvironment: {},
+ outputLimitBytes: 5
+ }
+ );
+
+ assert.equal(Buffer.byteLength(result.stdout), 5);
+ assert.equal(result.stdout, "你ab");
+ assert.equal(result.stderr, "12345");
+ assert.equal(result.stdoutTruncated, true);
+ assert.equal(result.stderrTruncated, true);
+});
+
+test("runCommand drops an incomplete UTF-8 code point at the byte limit", async () => {
+ const outputLimitBytes = 16 * 1024;
+ const fileSystem = createFileSystem({
+ directories: ["/Users/example"],
+ executablePaths: ["/Users/example", "/bin/zsh"]
+ });
+ const result = await runCommand(
+ { command: "ignored by fake", workingDirectory: "", environment: "" },
+ {
+ spawnFn: () =>
+ createChildProcess({
+ stdoutChunks: [
+ Buffer.alloc(outputLimitBytes - 1, "a"),
+ Buffer.from("你")
+ ]
+ }),
+ fileSystem,
+ homeDirectory: "/Users/example",
+ baseEnvironment: {},
+ outputLimitBytes
+ }
+ );
+
+ assert.equal(result.stdoutTruncated, true);
+ assert.doesNotMatch(result.stdout, /\uFFFD/);
+ assert.equal(result.stdout, "a".repeat(outputLimitBytes - 1));
+ assert.ok(Buffer.byteLength(result.stdout) <= outputLimitBytes);
+});
+
+test("runCommand replaces incomplete UTF-8 when output was not truncated", async () => {
+ const fileSystem = createFileSystem({
+ directories: ["/Users/example"],
+ executablePaths: ["/Users/example", "/bin/zsh"]
+ });
+ const result = await runCommand(
+ { command: "ignored by fake", workingDirectory: "", environment: "" },
+ {
+ spawnFn: () =>
+ createChildProcess({
+ stdoutChunks: [Buffer.from([0xe4])]
+ }),
+ fileSystem,
+ homeDirectory: "/Users/example",
+ baseEnvironment: {},
+ outputLimitBytes: 16
+ }
+ );
+
+ assert.equal(result.stdoutTruncated, false);
+ assert.equal(result.stdout, "\uFFFD");
+});
+
+test("runCommand keeps multibyte UTF-8 intact across stream chunks", async () => {
+ const text = Buffer.from("命令");
+ const fileSystem = createFileSystem({
+ directories: ["/Users/example"],
+ executablePaths: ["/Users/example", "/bin/zsh"]
+ });
+
+ const result = await runCommand(
+ { command: "ignored by fake", workingDirectory: "", environment: "" },
+ {
+ spawnFn: () =>
+ createChildProcess({
+ stdoutChunks: [
+ text.subarray(0, 1),
+ text.subarray(1, 4),
+ text.subarray(4)
+ ]
+ }),
+ fileSystem,
+ homeDirectory: "/Users/example",
+ baseEnvironment: {}
+ }
+ );
+
+ assert.equal(result.stdout, "命令");
+ assert.equal(result.stdoutTruncated, false);
+});
+
+test("runCommand returns synchronous and asynchronous spawn failures", async () => {
+ const fileSystem = createFileSystem({
+ directories: ["/Users/example"],
+ executablePaths: ["/Users/example", "/bin/zsh"]
+ });
+ const synchronousError = new Error("synchronous spawn failure");
+ const asynchronousError = new Error("asynchronous spawn failure");
+ const settings = {
+ command: "ignored by fake",
+ workingDirectory: "",
+ environment: ""
+ };
+ const options = {
+ fileSystem,
+ homeDirectory: "/Users/example",
+ baseEnvironment: {}
+ };
+
+ const synchronousResult = await runCommand(settings, {
+ ...options,
+ spawnFn() {
+ throw synchronousError;
+ }
+ });
+ const asynchronousResult = await runCommand(settings, {
+ ...options,
+ spawnFn() {
+ return createChildProcess({
+ spawnError: asynchronousError,
+ closeAfterError: true,
+ code: 127
+ });
+ }
+ });
+
+ assert.equal(synchronousResult.spawnError, synchronousError);
+ assert.equal(synchronousResult.code, null);
+ assert.equal(asynchronousResult.spawnError, asynchronousError);
+ assert.equal(asynchronousResult.code, null);
+});
+
+test("runCommand completes only once when close follows an asynchronous error", async () => {
+ const fileSystem = createFileSystem({
+ directories: ["/Users/example"],
+ executablePaths: ["/Users/example", "/bin/zsh"]
+ });
+ let thenCalls = 0;
+
+ const result = await runCommand(
+ { command: "ignored by fake", workingDirectory: "", environment: "" },
+ {
+ spawnFn: () =>
+ createChildProcess({
+ spawnError: new Error("spawn failed"),
+ closeAfterError: true,
+ code: 127
+ }),
+ fileSystem,
+ homeDirectory: "/Users/example",
+ baseEnvironment: {}
+ }
+ ).then((value) => {
+ thenCalls += 1;
+ return value;
+ });
+
+ await new Promise((resolve) => setImmediate(resolve));
+ assert.equal(thenCalls, 1);
+ assert.equal(result.spawnError.message, "spawn failed");
+});
+
+test("runCommand rejects validation failures before spawning", async () => {
+ let spawnCalls = 0;
+ const spawnFn = () => {
+ spawnCalls += 1;
+ return createChildProcess();
+ };
+ const validFileSystem = createFileSystem({
+ directories: ["/Users/example"],
+ executablePaths: ["/Users/example", "/bin/zsh"]
+ });
+ const commonOptions = {
+ spawnFn,
+ fileSystem: validFileSystem,
+ homeDirectory: "/Users/example",
+ baseEnvironment: {}
+ };
+
+ await assert.rejects(
+ runCommand(
+ { command: " ", workingDirectory: "", environment: "" },
+ commonOptions
+ ),
+ /命令不能为空/
+ );
+ await assert.rejects(
+ runCommand(
+ {
+ command: "printf ok",
+ workingDirectory: "",
+ environment: "BAD-NAME=secret"
+ },
+ commonOptions
+ ),
+ /1/
+ );
+ await assert.rejects(
+ runCommand(
+ {
+ command: "printf ok",
+ workingDirectory: "relative/path",
+ environment: ""
+ },
+ commonOptions
+ ),
+ /绝对路径/
+ );
+
+ const unusableFileSystem = createFileSystem({
+ directories: ["/Users/example"],
+ executablePaths: ["/Users/example"]
+ });
+ await assert.rejects(
+ runCommand(
+ { command: "printf ok", workingDirectory: "", environment: "" },
+ { ...commonOptions, fileSystem: unusableFileSystem }
+ ),
+ /Shell/
+ );
+ assert.equal(spawnCalls, 0);
+});
+
+async function createIsolatedZshEnvironment(t, profile = "") {
+ const temporaryDirectory = await mkdtemp(
+ path.join(tmpdir(), "command-runner-zdotdir-")
+ );
+ t.after(async () => {
+ await rm(temporaryDirectory, { recursive: true, force: true });
+ });
+ await writeFile(path.join(temporaryDirectory, ".zprofile"), profile);
+
+ return {
+ temporaryDirectory,
+ baseEnvironment: {
+ ...process.env,
+ SHELL: "/bin/zsh",
+ ZDOTDIR: temporaryDirectory
+ }
+ };
+}
+
+test(
+ "configured environment overrides login profile and inherited values",
+ { skip: process.platform !== "darwin" },
+ async (t) => {
+ const { temporaryDirectory, baseEnvironment } =
+ await createIsolatedZshEnvironment(
+ t,
+ "export COMMAND_EXECUTOR_TEST_VALUE='profile'\n"
+ );
+ const result = await runCommand(
+ {
+ command: "printf '%s' \"$COMMAND_EXECUTOR_TEST_VALUE\"",
+ workingDirectory: temporaryDirectory,
+ environment: "COMMAND_EXECUTOR_TEST_VALUE=explicit"
+ },
+ {
+ baseEnvironment: {
+ ...baseEnvironment,
+ COMMAND_EXECUTOR_TEST_VALUE: "inherited"
+ }
+ }
+ );
+
+ assert.equal(result.code, 0);
+ assert.equal(result.signal, null);
+ assert.equal(result.stdout, "explicit");
+ assert.equal(result.stderr, "");
+ assert.equal(result.spawnError, null);
+ }
+);
+
+test(
+ "exports a configured __proto__ variable to the real shell",
+ { skip: process.platform !== "darwin" },
+ async (t) => {
+ const { temporaryDirectory, baseEnvironment } =
+ await createIsolatedZshEnvironment(t);
+ const result = await runCommand(
+ {
+ command: "printf '%s' \"$__proto__\"",
+ workingDirectory: temporaryDirectory,
+ environment: "__proto__=first\n__proto__=explicit"
+ },
+ { baseEnvironment }
+ );
+
+ assert.equal(result.code, 0);
+ assert.equal(result.stdout, "explicit");
+ assert.equal(result.stderr, "");
+ }
+);
+
+test(
+ "preserves complete pipeline and quote semantics in the login shell",
+ { skip: process.platform !== "darwin" },
+ async (t) => {
+ const { baseEnvironment } = await createIsolatedZshEnvironment(t);
+ const result = await runCommand(
+ {
+ command:
+ `printf '%s\\n' "first value" "a'b" | ` +
+ `awk 'NR == 2 { printf "%s", $0 }'`,
+ workingDirectory: "",
+ environment: ""
+ },
+ { baseEnvironment }
+ );
+
+ assert.equal(result.code, 0);
+ assert.equal(result.stdout, "a'b");
+ assert.equal(result.stderr, "");
+ }
+);
+
+test(
+ "passes 200 inline shell arguments without parsing them in JavaScript",
+ { skip: process.platform !== "darwin" },
+ async (t) => {
+ const { baseEnvironment } = await createIsolatedZshEnvironment(t);
+ const args = Array.from(
+ { length: 200 },
+ (_, index) => `arg${String(index + 1).padStart(3, "0")}`
+ );
+ const result = await runCommand(
+ {
+ command: `set -- ${args.join(" ")}; printf '%s' "$#"`,
+ workingDirectory: "",
+ environment: ""
+ },
+ { baseEnvironment }
+ );
+
+ assert.equal(result.code, 0);
+ assert.equal(result.stdout, "200");
+ assert.equal(result.stderr, "");
+ }
+);
diff --git a/plugins/unlanzi_d200x/command_executor/tests/fixtures/ulanzi-sdk-sha256.json b/plugins/unlanzi_d200x/command_executor/tests/fixtures/ulanzi-sdk-sha256.json
new file mode 100644
index 0000000..51b525f
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/tests/fixtures/ulanzi-sdk-sha256.json
@@ -0,0 +1,25 @@
+{
+ "LICENSES/UlanziDeckPlugin-SDK-APACHE-2.0.txt": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
+ "libs/assets/u_active.svg": "a96c394b8a175932b6509d6e7c48df13174d8d684b4b4ddaebfa047d9cd26110",
+ "libs/assets/u_active_none.svg": "28682b245bdc74c674e9da73b0dce3efd2dc7b804d70fb72e8ee0d2f09b752d7",
+ "libs/assets/u_check_checkbox.svg": "880fdd1f67c5a602e96533cfdbb76a92daa868f96cf4552b0392c225d5cc729e",
+ "libs/assets/u_check_none.svg": "cc126d3957f27931a6e0b46c1ad4e762b3735548cde890a592fba0ef826e1560",
+ "libs/assets/u_check_radio.svg": "2555d9f51a769a143b14aa9eec6035323499863eae3b1ba8a5dc5e3af5eab57f",
+ "libs/assets/u_down.svg": "5441f01ec9073d0e19ee823ff9f1fea66fdcb74cd583ca998da36a86c7cd5740",
+ "libs/assets/u_file.svg": "240ec4b92721589ca7238a7d6b5b497cd06fd6b3de8caa99b32060b9d463bcdf",
+ "libs/assets/u_folder.svg": "64ac6d341b9d2981db245e4d281410bc938960f4acf09bf71fc03b974cbbe2e1",
+ "libs/assets/u_refresh.svg": "af16c113d1101e5fcd73018fa7f008ad0cd3dd96678b47e237face67b43c5ed1",
+ "libs/assets/u_tip_error.svg": "d551c95407b44d17c6c6546c625fe6de9202aed00ea5e52bad025c95743a8de2",
+ "libs/assets/u_tip_info.svg": "b2a22809ce8e69bba12394a52a76690c8e632e7278a68230e76e55db0112e908",
+ "libs/assets/u_tip_success.svg": "81107680e49add68ca6efe864c4a2bdbc7fb372f38029cfb4e7457960f48ef0d",
+ "libs/assets/u_tip_warn.svg": "ff79b63f7deabffadd398ba5430d7a3ce649f2883fcaf0e46789936357a0962e",
+ "libs/css/uspi.css": "b559dd379d2b616ee75c3d96a26264145f365139a42bdadd9d7bb816e6ae736f",
+ "libs/js/constants.js": "4d6581c19e34379cf28f4c56835545a81f8c036e4395f40f1e449debdb57302e",
+ "libs/js/eventEmitter.js": "1167eaf1c4dce87942186bd35042851a3925ba8f19932dbd42e8050f991027c5",
+ "libs/js/timers.js": "3b6948510c2136d8e58c9337682fbcd7d02aad8d67a5befa7bd1691e897aaae2",
+ "libs/js/ulanziApi.js": "ebd369d1616ef77d93701ecd0ddf95f0c1dc1e4e248aa6c3e3db5f68d250112b",
+ "libs/js/utils.js": "6f13551f6e2d771401e2ddf8763d22d692b5ecfc0dd2ddcaa4ddde5e7274691e",
+ "plugin/vendor/ulanzi-api/constants.js": "35c2cd088ebdcecd256412f5436555ec15a2510d9f09fedaef03f9014d227c62",
+ "plugin/vendor/ulanzi-api/ulanziApi.js": "4f4f307ae556a658669ab0bdf36016b1e151b25e4a5ec40451ab2c0724935168",
+ "plugin/vendor/ulanzi-api/utils.js": "37a9ba4fc1f1346c733dfc55ced35d29325cf1a7252a4d96fcfecd9958f8b8d1"
+}
diff --git a/plugins/unlanzi_d200x/command_executor/tests/inspector-settings.test.mjs b/plugins/unlanzi_d200x/command_executor/tests/inspector-settings.test.mjs
new file mode 100644
index 0000000..8b44b0a
--- /dev/null
+++ b/plugins/unlanzi_d200x/command_executor/tests/inspector-settings.test.mjs
@@ -0,0 +1,531 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import path from "node:path";
+import test from "node:test";
+import vm from "node:vm";
+import { fileURLToPath } from "node:url";
+
+const testDirectory = path.dirname(fileURLToPath(import.meta.url));
+const pluginDirectory = path.resolve(
+ testDirectory,
+ "..",
+ "com.ulanzi.commandexecutor.ulanziPlugin"
+);
+const inspectorDirectory = path.join(pluginDirectory, "property-inspector");
+
+async function loadSettings() {
+ const source = await readFile(
+ path.join(inspectorDirectory, "settings.js"),
+ "utf8"
+ );
+ const context = vm.createContext({});
+ vm.runInContext(source, context);
+ return context.CommandExecutorSettings;
+}
+
+function copyFromVm(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+function createInspectorHarness({ language = "zh_CN" } = {}) {
+ const listeners = new Map();
+ const windowListeners = new Map();
+ const timers = [];
+ let now = 0;
+
+ class FakeElement {
+ constructor() {
+ this.listeners = new Map();
+ this.textContent = "";
+ Object.defineProperty(this, "innerHTML", {
+ set() {
+ assert.fail("inspector must not write user content through innerHTML");
+ }
+ });
+ }
+
+ addEventListener(name, callback) {
+ this.listeners.set(name, callback);
+ }
+
+ dispatch(name) {
+ const callback = this.listeners.get(name);
+ assert.ok(callback, `${name} listener was not registered`);
+ callback({ type: name });
+ }
+ }
+
+ const form = new FakeElement();
+ form.values = {
+ title: "执行命令",
+ command: "",
+ workingDirectory: "",
+ environment: ""
+ };
+ const validationMessage = new FakeElement();
+ const executionPreview = new FakeElement();
+ const wrapper = new FakeElement();
+ wrapper.hidden = true;
+ wrapper.classList = {
+ remove(name) {
+ assert.equal(name, "hidden");
+ wrapper.hidden = false;
+ }
+ };
+
+ const elements = new Map([
+ ["#property-inspector", form],
+ ["#validation-message", validationMessage],
+ ["#execution-preview", executionPreview],
+ [".udpi-wrapper", wrapper]
+ ]);
+ const document = {
+ querySelector(selector) {
+ return elements.get(selector) ?? null;
+ }
+ };
+ const fakeWindow = {
+ addEventListener(name, callback) {
+ windowListeners.set(name, callback);
+ },
+ dispatch(name) {
+ const callback = windowListeners.get(name);
+ assert.ok(callback, `${name} listener was not registered`);
+ callback({ type: name });
+ }
+ };
+ const Utils = {
+ debounce(callback, wait) {
+ let pendingTimer = null;
+ return (...args) => {
+ if (pendingTimer) {
+ pendingTimer.active = false;
+ }
+ pendingTimer = {
+ active: true,
+ callback,
+ args,
+ dueAt: now + wait
+ };
+ timers.push(pendingTimer);
+ };
+ },
+ getFormValue(target) {
+ assert.equal(target, form);
+ return { ...form.values };
+ },
+ setFormValue(settings, target) {
+ assert.equal(target, form);
+ form.values = { ...settings };
+ }
+ };
+ const api = {
+ language,
+ sent: [],
+ connect(uuid) {
+ this.connectedUuid = uuid;
+ },
+ onConnected(callback) {
+ listeners.set("connected", callback);
+ },
+ onAdd(callback) {
+ listeners.set("add", callback);
+ },
+ onParamFromApp(callback) {
+ listeners.set("paramFromApp", callback);
+ },
+ sendParamFromPlugin(settings) {
+ this.sent.push(JSON.parse(JSON.stringify(settings)));
+ },
+ emit(name, message = {}) {
+ const callback = listeners.get(name);
+ assert.ok(callback, `${name} callback was not registered`);
+ callback(message);
+ }
+ };
+
+ return {
+ api,
+ document,
+ executionPreview,
+ form,
+ Utils,
+ validationMessage,
+ window: fakeWindow,
+ advance(milliseconds) {
+ now += milliseconds;
+ let ranTimer;
+ do {
+ ranTimer = false;
+ for (const timer of timers) {
+ if (timer.active && timer.dueAt <= now) {
+ timer.active = false;
+ timer.callback(...timer.args);
+ ranTimer = true;
+ }
+ }
+ } while (ranTimer);
+ }
+ };
+}
+
+async function runInspector(harness) {
+ const [settingsSource, inspectorSource] = await Promise.all([
+ readFile(path.join(inspectorDirectory, "settings.js"), "utf8"),
+ readFile(path.join(inspectorDirectory, "inspector.js"), "utf8")
+ ]);
+ const context = vm.createContext({
+ document: harness.document,
+ window: harness.window,
+ Utils: harness.Utils,
+ $UD: harness.api
+ });
+
+ vm.runInContext(settingsSource, context);
+ vm.runInContext(inspectorSource, context);
+}
+
+test("normalizes defaults and preserves string configuration verbatim", async () => {
+ const { DEFAULT_SETTINGS, normalizeSettings } = await loadSettings();
+
+ assert.deepEqual(copyFromVm(DEFAULT_SETTINGS), {
+ title: "执行命令",
+ command: "",
+ workingDirectory: "",
+ environment: ""
+ });
+ assert.deepEqual(
+ copyFromVm(
+ normalizeSettings({
+ title: " 自定义标题 ",
+ command: " printf '%s' value\n",
+ workingDirectory: " ~/work ",
+ environment: " FOO=value \n"
+ })
+ ),
+ {
+ title: "自定义标题",
+ command: " printf '%s' value\n",
+ workingDirectory: " ~/work ",
+ environment: " FOO=value \n"
+ }
+ );
+ assert.deepEqual(
+ copyFromVm(
+ normalizeSettings({
+ title: " \t ",
+ command: 100,
+ workingDirectory: null,
+ environment: {}
+ })
+ ),
+ {
+ title: "执行命令",
+ command: "",
+ workingDirectory: "",
+ environment: ""
+ }
+ );
+});
+
+test("environment validation accepts blank CRLF input, first equals, and duplicates", async () => {
+ const { validateEnvironmentText } = await loadSettings();
+
+ assert.equal(validateEnvironmentText(""), "");
+ assert.equal(validateEnvironmentText("\r\n \r\n"), "");
+ assert.equal(
+ validateEnvironmentText(
+ " TOKEN =first=value\r\nTOKEN=last=value=kept\r\n_EMPTY=\r\n"
+ ),
+ ""
+ );
+});
+
+test("environment validation reports 1-based lines without leaking values", async () => {
+ const { validateEnvironmentText } = await loadSettings();
+ const invalidLine = validateEnvironmentText(
+ "VALID=ok\nmissing-equals-secret\nAFTER=ok"
+ );
+ const invalidName = validateEnvironmentText(
+ "VALID=ok\nBAD-NAME=do-not-leak"
+ );
+
+ assert.match(invalidLine, /2/);
+ assert.doesNotMatch(invalidLine, /missing-equals-secret/);
+ assert.match(invalidName, /2/);
+ assert.doesNotMatch(invalidName, /do-not-leak/);
+});
+
+test("environment validation rejects NUL without leaking the value", async () => {
+ const { validateEnvironmentText } = await loadSettings();
+ const message = validateEnvironmentText(
+ "VALID=ok\nSECRET=before\0after"
+ );
+
+ assert.match(message, /2/);
+ assert.match(message, /NUL/);
+ assert.doesNotMatch(message, /before|after/);
+});
+
+test("validation and preview follow the selected English or Chinese locale", async () => {
+ const {
+ validateEnvironmentText,
+ buildExecutionPreview
+ } = await loadSettings();
+ const environment = "VALID=ok\nBAD-NAME=do-not-leak";
+ const englishError = validateEnvironmentText(environment, "en_US");
+ const chineseError = validateEnvironmentText(environment, "zh_CN");
+ const englishPreview = buildExecutionPreview({}, "en");
+ const chinesePreview = buildExecutionPreview({}, "zh_CN");
+
+ assert.match(englishError, /Environment variable line 2/);
+ assert.doesNotMatch(englishError, /do-not-leak/);
+ assert.match(chineseError, /环境变量第 2 行/);
+ assert.doesNotMatch(chineseError, /do-not-leak/);
+ assert.match(
+ englishPreview,
+ /Working directory: \$HOME[\s\S]*Environment variables: None[\s\S]*Command:\n\(empty\)/
+ );
+ assert.doesNotMatch(englishPreview, /工作目录|环境变量|命令|(空)/);
+ assert.match(
+ chinesePreview,
+ /工作目录: \$HOME[\s\S]*环境变量: 无[\s\S]*命令:\n(空)/
+ );
+});
+
+test("execution preview contains names, cwd, and the complete 200-argument command", async () => {
+ const { buildExecutionPreview } = await loadSettings();
+ const command = [
+ "printf",
+ ...Array.from({ length: 200 }, (_, index) => `"arg-${index + 1}"`)
+ ].join(" ");
+ const preview = buildExecutionPreview({
+ title: "忽略",
+ command,
+ workingDirectory: "/Users/example/work",
+ environment: "TOKEN=top-secret\nFOO=value=with=equals\nTOKEN=last-secret"
+ });
+
+ assert.match(preview, /\$SHELL -lc/);
+ assert.match(preview, /\/Users\/example\/work/);
+ assert.match(preview, /TOKEN/);
+ assert.match(preview, /FOO/);
+ assert.doesNotMatch(preview, /top-secret|last-secret|value=with=equals/);
+ assert.ok(preview.includes(command));
+ assert.ok(preview.endsWith(command));
+});
+
+test("execution preview uses safe empty fallbacks", async () => {
+ const { buildExecutionPreview } = await loadSettings();
+ const preview = buildExecutionPreview({});
+
+ assert.match(preview, /\$HOME/);
+ assert.match(preview, /环境变量:\s*无/);
+ assert.match(preview, /命令:\s*\n(空)/);
+});
+
+test("pagehide flushes the latest complete command before the debounce expires", async () => {
+ const harness = createInspectorHarness({ language: "en" });
+ await runInspector(harness);
+ harness.api.emit("connected");
+ const command = [
+ "printf",
+ ...Array.from({ length: 200 }, (_, index) => `"value-${index + 1}"`)
+ ].join(" ");
+ harness.form.values = {
+ title: " Latest command ",
+ command,
+ workingDirectory: "/Users/example/work",
+ environment: "BAD-NAME=do-not-leak"
+ };
+
+ harness.form.dispatch("input");
+
+ assert.ok(harness.executionPreview.textContent.includes(command));
+ assert.match(
+ harness.validationMessage.textContent,
+ /Environment variable line 1/
+ );
+ assert.doesNotMatch(
+ harness.validationMessage.textContent,
+ /do-not-leak/
+ );
+ harness.advance(199);
+ assert.deepEqual(harness.api.sent, []);
+
+ harness.window.dispatch("pagehide");
+
+ assert.deepEqual(harness.api.sent, [
+ {
+ title: "Latest command",
+ command,
+ workingDirectory: "/Users/example/work",
+ environment: "BAD-NAME=do-not-leak"
+ }
+ ]);
+ harness.advance(1);
+ assert.equal(harness.api.sent.length, 1);
+});
+
+test("change flushes immediately without a later duplicate send", async () => {
+ const harness = createInspectorHarness();
+ await runInspector(harness);
+ harness.api.emit("connected");
+ harness.form.values = {
+ title: "改变",
+ command: "printf changed",
+ workingDirectory: "",
+ environment: ""
+ };
+
+ harness.form.dispatch("input");
+ harness.form.dispatch("change");
+
+ assert.deepEqual(harness.api.sent, [
+ {
+ title: "改变",
+ command: "printf changed",
+ workingDirectory: "",
+ environment: ""
+ }
+ ]);
+ harness.advance(200);
+ assert.equal(harness.api.sent.length, 1);
+});
+
+test("dirty local edits survive host echoes and later host updates are accepted", async () => {
+ const harness = createInspectorHarness();
+ await runInspector(harness);
+ harness.api.emit("add", {
+ param: {
+ title: "初始",
+ command: "printf initial",
+ workingDirectory: "/initial",
+ environment: "INITIAL=1"
+ }
+ });
+ harness.api.emit("connected");
+ assert.equal(harness.form.values.command, "printf initial");
+
+ harness.form.values = {
+ title: " 本地最新 ",
+ command: "printf local-latest",
+ workingDirectory: "/local",
+ environment: "LOCAL=latest"
+ };
+ harness.form.dispatch("input");
+ harness.api.emit("paramFromApp", {
+ param: {
+ title: "宿主旧值",
+ command: "printf stale-host",
+ workingDirectory: "/host",
+ environment: "HOST=stale"
+ }
+ });
+
+ assert.equal(harness.form.values.command, "printf local-latest");
+ assert.ok(
+ harness.executionPreview.textContent.includes("printf local-latest")
+ );
+ harness.advance(200);
+ assert.deepEqual(harness.api.sent, [
+ {
+ title: "本地最新",
+ command: "printf local-latest",
+ workingDirectory: "/local",
+ environment: "LOCAL=latest"
+ }
+ ]);
+
+ harness.api.emit("paramFromApp", {
+ param: {
+ title: "宿主新值",
+ command: "printf accepted-host",
+ workingDirectory: "/accepted",
+ environment: "HOST=accepted"
+ }
+ });
+ assert.equal(harness.form.values.command, "printf accepted-host");
+ assert.ok(
+ harness.executionPreview.textContent.includes("printf accepted-host")
+ );
+});
+
+test("inspector uses official classic scripts and safe automatic persistence", async () => {
+ const [html, source] = await Promise.all([
+ readFile(path.join(inspectorDirectory, "inspector.html"), "utf8"),
+ readFile(path.join(inspectorDirectory, "inspector.js"), "utf8")
+ ]);
+ const expectedScripts = [
+ "../libs/js/constants.js",
+ "../libs/js/eventEmitter.js",
+ "../libs/js/timers.js",
+ "../libs/js/utils.js",
+ "../libs/js/ulanziApi.js",
+ "./settings.js",
+ "./inspector.js"
+ ];
+ const actualScripts = [
+ ...html.matchAll(/
+
+
+
+
+
+
+```
+
+- [ ] `inspector.js` 与官方 Property Inspector 一样使用经典脚本作用域中的 `$UD`,并同时监听 `onAdd` 与 `onParamFromApp`:
+
+```js
+const ACTION_UUID =
+ "com.ulanzi.ulanzistudio.commandexecutor.runcommand";
+const {
+ normalizeSettings,
+ validateEnvironmentText,
+ buildExecutionPreview
+} = globalThis.CommandExecutorSettings;
+const api = $UD;
+let currentSettings = normalizeSettings();
+let form = null;
+const debouncedFlush = Utils.debounce(flushSettings, 200);
+
+function handleInput() {
+ captureSettings();
+ debouncedFlush();
+}
+
+api.connect(ACTION_UUID);
+api.onConnected(() => {
+ form = document.querySelector("#property-inspector");
+ document.querySelector(".udpi-wrapper").classList.remove("hidden");
+ form.addEventListener("input", handleInput);
+ renderSettings();
+});
+api.onAdd((message) => applyIncomingSettings(message?.param));
+api.onParamFromApp((message) => applyIncomingSettings(message?.param));
+```
+
+`handleInput()` 先通过 `captureSettings()` 收集表单值并更新校验和预览,再调用 `debouncedFlush()`。`flushSettings()` 只负责把 `currentSettings` 通过 `api.sendParamFromPlugin(currentSettings)` 发送给 Studio。代码中不得 `console.log(currentSettings)`。
+
+- [ ] 环境变量格式错误显示在 `validation-message`;预览使用 `textContent`,不得使用 `innerHTML` 拼入用户命令。
+
+- [ ] 运行:
+
+```bash
+npm test
+```
+
+预期:所有测试通过。
+
+- [ ] 提交:
+
+```bash
+git add plugins/unlanzi_d200x/command_executor
+git commit -m "feat: add D200X command configuration panel"
+```
+
+## 7. Task 5:构建、校验和可安装包
+
+**Files:**
+
+- Create: `plugins/unlanzi_d200x/command_executor/webpack.config.js`
+- Create: `plugins/unlanzi_d200x/command_executor/scripts/validate-package.mjs`
+- Create: `plugins/unlanzi_d200x/command_executor/build.sh`
+- Modify: `plugins/unlanzi_d200x/command_executor/tests/manifest.test.mjs`
+
+- [ ] 先扩展 manifest 测试:当 `dist/app.js` 存在时,要求它非空且不包含绝对 worktree 路径。
+
+- [ ] 创建最小 Webpack 配置;不引入 Babel、Terser、CopyWebpackPlugin:
+
+```js
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const root = path.dirname(fileURLToPath(import.meta.url));
+const pluginRoot = path.join(
+ root,
+ "com.ulanzi.commandexecutor.ulanziPlugin"
+);
+
+export default {
+ mode: "production",
+ target: "node20",
+ entry: path.join(pluginRoot, "plugin/app.js"),
+ output: {
+ path: path.join(pluginRoot, "dist"),
+ filename: "app.js",
+ library: {
+ type: "module"
+ },
+ chunkFormat: "module"
+ },
+ experiments: {
+ outputModule: true
+ },
+ optimization: {
+ minimize: false
+ },
+ devtool: false
+};
+```
+
+`ws` 必须打入单个 `dist/app.js`,安装包不携带 `node_modules/`。
+
+- [ ] `validate-package.mjs` 接收一个 `.ulanziPlugin` 目录,检查:
+
+ 1. basename 严格为 `com.ulanzi.commandexecutor.ulanziPlugin`。
+ 2. manifest 和包内 package JSON 可解析。
+ 3. 所有 manifest 资源路径留在插件目录内,禁止 `..` 逃逸。
+ 4. 所有声明文件存在。
+ 5. `dist/app.js` 非空。
+ 6. 包内不存在 `.DS_Store`、`._*`、`__MACOSX`、`node_modules`、测试文件或源码映射。
+ 7. vendored Node/HTML SDK 文件和固定 commit 声明存在。
+ 8. LICENSE 和 THIRD_PARTY_NOTICES 存在。
+
+- [ ] `build.sh` 只清理自己的两个精确生成目标,随后构建、复制、校验、压缩:
+
+```bash
+#!/usr/bin/env bash
+set -euo pipefail
+
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+plugin_name="com.ulanzi.commandexecutor.ulanziPlugin"
+source_plugin="$script_dir/$plugin_name"
+output_root="$script_dir/output"
+output_plugin="$output_root/$plugin_name"
+archive="$output_root/life_tools_ulanzi_d200x_command_executor.zip"
+
+cd "$script_dir"
+npm run bundle
+rm -rf "$output_plugin"
+rm -f "$archive"
+mkdir -p "$output_root"
+/usr/bin/ditto --norsrc "$source_plugin" "$output_plugin"
+node scripts/validate-package.mjs "$output_plugin"
+(
+ cd "$output_root"
+ /usr/bin/ditto -c -k --norsrc --keepParent "$plugin_name" "$archive"
+)
+/usr/bin/unzip -t "$archive"
+```
+
+- [ ] 给脚本增加可执行位,执行:
+
+```bash
+bash -n build.sh
+npm test
+./build.sh
+node scripts/validate-package.mjs \
+ output/com.ulanzi.commandexecutor.ulanziPlugin
+unzip -Z1 output/life_tools_ulanzi_d200x_command_executor.zip
+```
+
+预期:
+
+- 测试退出码 0。
+- `output/com.ulanzi.commandexecutor.ulanziPlugin/dist/app.js` 存在。
+- zip 校验为 `No errors detected`。
+- zip 列表不含 `__MACOSX`、`.DS_Store`、`._*`、`node_modules`。
+
+- [ ] 提交:
+
+```bash
+git add plugins/unlanzi_d200x/command_executor
+git commit -m "build: package Ulanzi command executor"
+```
+
+## 8. Task 6:接入 CI 和仓库入口
+
+**Files:**
+
+- Create: `.github/workflows/ulanzi-command-executor.yml`
+- Modify: `README.MD`
+- Modify: `AGENTS.md`
+- Modify: `plugins/unlanzi_d200x/docs/README.md`
+
+- [ ] 新建 macOS CI,paths 至少覆盖:
+
+```yaml
+on:
+ pull_request:
+ paths:
+ - ".github/workflows/ulanzi-command-executor.yml"
+ - "plugins/unlanzi_d200x/**"
+ - "README.MD"
+ - "AGENTS.md"
+ push:
+ branches:
+ - master
+ paths:
+ - ".github/workflows/ulanzi-command-executor.yml"
+ - "plugins/unlanzi_d200x/**"
+```
+
+- [ ] Job 使用 `macos-latest` 和 Node 20,步骤固定为:
+
+```yaml
+- uses: actions/checkout@v4
+- uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ cache: npm
+ cache-dependency-path: plugins/unlanzi_d200x/command_executor/package-lock.json
+- run: npm ci
+ working-directory: plugins/unlanzi_d200x/command_executor
+- run: npm test
+ working-directory: plugins/unlanzi_d200x/command_executor
+- run: bash -n build.sh
+ working-directory: plugins/unlanzi_d200x/command_executor
+- run: ./build.sh
+ working-directory: plugins/unlanzi_d200x/command_executor
+- uses: actions/upload-artifact@v4
+ with:
+ name: life_tools_ulanzi_d200x_command_executor_ci
+ path: plugins/unlanzi_d200x/command_executor/output/life_tools_ulanzi_d200x_command_executor.zip
+ if-no-files-found: error
+```
+
+- [ ] README 工具清单新增一行:
+
+| 分类 | 工具 | 源码目录 | 产物/入口 | 简述 | 详细文档 |
+|---|---|---|---|---|---|
+| Plugin | Ulanzi D200X Command Executor | `plugins/unlanzi_d200x/command_executor/` | `.ulanziPlugin` | 每个按键独立配置并执行 macOS Shell 命令 | `plugins/unlanzi_d200x/docs/command-executor-user-guide.md` |
+
+- [ ] README“构建与验证”新增可复制命令:
+
+```bash
+cd plugins/unlanzi_d200x/command_executor
+npm ci
+npm test
+./build.sh
+```
+
+- [ ] AGENTS 新增插件目录职责、macOS-only 边界、测试/构建命令、实机验证要求,以及 SDK 日志可能记录完整配置的本机调试边界。
+
+- [ ] 更新插件 docs 索引,登记实现计划、开发指南、用户指南、验证报告。
+
+- [ ] 不修改 `.github/workflows/release.yml`。当前交付通过本地 build 和 PR dry-run artifact 生成安装包;tag release 资产命名和长期发布策略没有用户需求,不在本次引入。
+
+- [ ] 检查 YAML 和 diff:
+
+```bash
+git diff --check
+git status --short
+```
+
+- [ ] 提交:
+
+```bash
+git add .github/workflows/ulanzi-command-executor.yml README.MD AGENTS.md \
+ plugins/unlanzi_d200x/docs/README.md
+git commit -m "ci: verify Ulanzi command executor"
+```
+
+## 9. Task 7:补齐开发、使用和测试文档
+
+**Files:**
+
+- Create: `plugins/unlanzi_d200x/docs/command-executor-development-guide.md`
+- Create: `plugins/unlanzi_d200x/docs/command-executor-installation.md`
+- Create: `plugins/unlanzi_d200x/docs/command-executor-user-guide.md`
+- Create: `plugins/unlanzi_d200x/docs/command-executor-validation.md`
+- Modify: `plugins/unlanzi_d200x/docs/ulanzi-plugin-development-reference.md`
+
+- [ ] 开发指南必须记录:
+
+ - 最终目录和每个文件的职责。
+ - 三个 SDK commit、原样 vendoring 约定和完整 payload 日志行为。
+ - manifest UUID、D200X/Keypad/macOS 限制。
+ - `context` 配置隔离和并发计数。
+ - `$SHELL -lc` 会读取登录配置但不会自动读取交互式 `.zshrc`。
+ - 显式环境变量覆盖顺序。
+ - stdout/stderr 各 16 KiB 截断。
+ - `npm ci`、`npm test`、`./build.sh`。
+ - 本地安装目录、调试启动方式、日志定位命令。
+ - SDK 升级时重新核对 commit、许可证和日志行为的步骤。
+
+- [ ] 安装说明必须记录 macOS 构建、目录包和 zip 安装、升级备份、回滚、验收、日志、调试端口和可恢复卸载。
+
+- [ ] 用户指南必须按“安装 / 拖拽 / 配置 / 示例 / 状态 / 限制 / 排障”组织,至少给出:
+
+```bash
+pwd
+ls -alh "$HOME"
+find . -type f | sort > /private/tmp/files.txt
+printf '%s\n' "$HOME" "$USER" "$PATH"
+```
+
+环境变量示例:
+
+```text
+CUSTOM_ENV=hello
+HTTP_PROXY=http://127.0.0.1:7890
+```
+
+明确说明:
+
+- 100~200 个参数直接粘贴在命令框,不需要拆成 200 个表单项。
+- 插件不提供终端输入,`sudo`、SSH 密码、交互式 REPL 会等待或失败。
+- 每次按键都会启动一次;长时间后台任务由用户自己的命令负责。
+- 命令以当前 macOS 用户权限执行,粘贴陌生命令等价于在终端执行。
+
+- [ ] 验证报告先建立固定表格,自动化和实机结果未执行前使用“待验证”,不得提前写“通过”:
+
+| 验证项 | 命令/操作 | 证据 | 结果 |
+|---|---|---|---|
+| Node 单测 | `npm test` | 测试摘要 | 待验证 |
+| 构建 | `./build.sh` | 产物路径与 zip 校验 | 待验证 |
+| 插件加载 | Studio 重启 | 截图 | 待验证 |
+| 两按键隔离 | 分别配置 A/B | 截图与输出 | 待验证 |
+| 200 参数 | inline `set --` | `arg-count.txt` | 待验证 |
+| 用户环境 | `$HOME/$USER/$PATH` | `environment.txt` | 待验证 |
+| 自定义环境/目录 | 面板配置 | `environment.txt` | 待验证 |
+| 失败反馈 | `exit 7` | Alert/Toast/日志 | 待验证 |
+| 配置持久化 | 重启 Studio | 重启前后截图 | 待验证 |
+
+- [ ] 开发参考增加“官方 SDK 会默认记录完整 WebSocket payload,处理命令或密钥配置的插件必须审查并最小修改日志”的经验,引用当前 vendored 文件和回归测试。
+
+- [ ] 文档若新增 Mermaid,逐个用 `mmdc` 渲染;没有 Mermaid 时执行 Markdown 链接和路径检查。
+
+- [ ] 提交:
+
+```bash
+git add plugins/unlanzi_d200x/docs
+git commit -m "docs: document Ulanzi command executor workflow"
+```
+
+## 10. Task 8:执行自动化验证
+
+**Files:**
+
+- Modify only if a real defect is found: files under `plugins/unlanzi_d200x/command_executor/`
+- Modify: `plugins/unlanzi_d200x/docs/command-executor-validation.md`
+
+- [ ] 记录运行环境:
+
+```bash
+/usr/local/bin/node --version
+/usr/bin/sw_vers
+git rev-parse HEAD
+```
+
+- [ ] 从干净依赖状态执行:
+
+```bash
+cd plugins/unlanzi_d200x/command_executor
+npm ci
+npm test
+bash -n build.sh
+./build.sh
+node scripts/validate-package.mjs \
+ output/com.ulanzi.commandexecutor.ulanziPlugin
+```
+
+- [ ] 单独执行真实 Shell 验证并保留摘要:
+
+```bash
+node --test tests/command-runner.test.mjs
+node --test tests/command-plugin.test.mjs
+```
+
+- [ ] 检查构建包:
+
+```bash
+find output/com.ulanzi.commandexecutor.ulanziPlugin -type f | sort
+unzip -t output/life_tools_ulanzi_d200x_command_executor.zip
+unzip -Z1 output/life_tools_ulanzi_d200x_command_executor.zip |
+ rg '(__MACOSX|\\.DS_Store|/\\._|node_modules|\\.map$)'
+```
+
+最后一个 `rg` 预期无输出且退出码 1;这是“没有污染文件”,不是测试失败。
+
+- [ ] 将真实测试数、耗时、Node/macOS 版本、产物大小和 commit 写入验证报告。只记录摘要,不粘贴用户环境值。
+
+- [ ] 若修复实现缺陷,先补能复现的测试,再改代码,并使用 `fix:` commit 单独提交。
+
+## 11. Task 9:安装到 Ulanzi Studio 并完成 D200X 实机实验
+
+**Files:**
+
+- Create: `plugins/unlanzi_d200x/docs/assets/command_executor/installed-plugin.jpg`
+- Modify: `plugins/unlanzi_d200x/docs/command-executor-validation.md`
+- Modify: `plugins/unlanzi_d200x/docs/command-executor-development-guide.md`
+- Modify: `plugins/unlanzi_d200x/docs/command-executor-user-guide.md`
+
+- [ ] 使用 `computer-use:computer-use` 控制 Ulanzi Studio。安装前只解析精确目标,不删除插件目录中的其他内容:
+
+```text
+源:
+plugins/unlanzi_d200x/command_executor/output/
+ com.ulanzi.commandexecutor.ulanziPlugin
+
+目标:
+~/Library/Application Support/Ulanzi/UlanziDeck/Plugins/
+ com.ulanzi.commandexecutor.ulanziPlugin
+```
+
+若同名目标已存在,先移动为同目录带时间戳的 `.backup-YYYYmmdd-HHMMSS`;不要删除现有 `__MACOSX` 或其他插件。
+
+- [ ] 完全退出 Ulanzi Studio,复制构建包,再用以下参数启动:
+
+```bash
+open /Applications/Ulanzi\ Studio.app --args \
+ --log \
+ --webRemoteDebug
+```
+
+当前 manifest 没有 `Inspect`,对应测试也明确要求该字段不存在,因此本轮不启用 Node inspector。若确实需要 Node inspector,必须先单独增加 manifest `Inspect`、回归测试和 Studio / D200X 实机验证,再补充对应启动与连接说明。
+
+- [ ] 确认 Studio 显示 D200X 已连接;在插件列表找到“命令执行器 / 执行命令”,拖到两个普通按键。
+
+- [ ] 创建专用安全目录:
+
+```bash
+mkdir -p /private/tmp/life_tools_ulanzi_command_executor_e2e
+```
+
+只允许本轮实机命令写这个目录。
+
+- [ ] 按键 A 配置:
+
+```text
+标题:环境验证
+工作目录:/private/tmp/life_tools_ulanzi_command_executor_e2e
+环境变量:CUSTOM_ENV=from_ulanzi
+命令:
+printf 'HOME=%s\nUSER=%s\nPWD=%s\nCUSTOM_ENV=%s\nPATH=%s\n' \
+ "$HOME" "$USER" "$PWD" "$CUSTOM_ENV" "$PATH" \
+ > /private/tmp/life_tools_ulanzi_command_executor_e2e/environment.txt
+```
+
+- [ ] 按键 B 依次验证下列命令,每次读取结果文件后再进入下一项:
+
+无参数命令:
+
+```bash
+pwd > /private/tmp/life_tools_ulanzi_command_executor_e2e/no-args.txt
+```
+
+管道、引号和重定向:
+
+```bash
+printf '%s\n' "alpha beta" "gamma" |
+ /usr/bin/sort \
+ > /private/tmp/life_tools_ulanzi_command_executor_e2e/shell-syntax.txt
+```
+
+200 个 inline 参数:
+
+```bash
+set -- arg001 arg002 arg003 arg004 arg005 arg006 arg007 arg008 arg009 arg010 \
+arg011 arg012 arg013 arg014 arg015 arg016 arg017 arg018 arg019 arg020 \
+arg021 arg022 arg023 arg024 arg025 arg026 arg027 arg028 arg029 arg030 \
+arg031 arg032 arg033 arg034 arg035 arg036 arg037 arg038 arg039 arg040 \
+arg041 arg042 arg043 arg044 arg045 arg046 arg047 arg048 arg049 arg050 \
+arg051 arg052 arg053 arg054 arg055 arg056 arg057 arg058 arg059 arg060 \
+arg061 arg062 arg063 arg064 arg065 arg066 arg067 arg068 arg069 arg070 \
+arg071 arg072 arg073 arg074 arg075 arg076 arg077 arg078 arg079 arg080 \
+arg081 arg082 arg083 arg084 arg085 arg086 arg087 arg088 arg089 arg090 \
+arg091 arg092 arg093 arg094 arg095 arg096 arg097 arg098 arg099 arg100 \
+arg101 arg102 arg103 arg104 arg105 arg106 arg107 arg108 arg109 arg110 \
+arg111 arg112 arg113 arg114 arg115 arg116 arg117 arg118 arg119 arg120 \
+arg121 arg122 arg123 arg124 arg125 arg126 arg127 arg128 arg129 arg130 \
+arg131 arg132 arg133 arg134 arg135 arg136 arg137 arg138 arg139 arg140 \
+arg141 arg142 arg143 arg144 arg145 arg146 arg147 arg148 arg149 arg150 \
+arg151 arg152 arg153 arg154 arg155 arg156 arg157 arg158 arg159 arg160 \
+arg161 arg162 arg163 arg164 arg165 arg166 arg167 arg168 arg169 arg170 \
+arg171 arg172 arg173 arg174 arg175 arg176 arg177 arg178 arg179 arg180 \
+arg181 arg182 arg183 arg184 arg185 arg186 arg187 arg188 arg189 arg190 \
+arg191 arg192 arg193 arg194 arg195 arg196 arg197 arg198 arg199 arg200
+printf '%s\n' "$#" \
+ > /private/tmp/life_tools_ulanzi_command_executor_e2e/arg-count.txt
+```
+
+并发:
+
+```bash
+sleep 2
+/usr/bin/uuidgen \
+ >> /private/tmp/life_tools_ulanzi_command_executor_e2e/concurrent.txt
+```
+
+失败反馈:
+
+```bash
+exit 7
+```
+
+- [ ] 通过实体 D200X 按键触发。若 Studio 的设备预览点击也能产生 `onRun`,额外记录该行为;若预览只负责选中按键,不把它误写成硬件触发证据。
+
+- [ ] 回读并验证:
+
+```bash
+wc -l /private/tmp/life_tools_ulanzi_command_executor_e2e/*
+cat /private/tmp/life_tools_ulanzi_command_executor_e2e/arg-count.txt
+cat /private/tmp/life_tools_ulanzi_command_executor_e2e/no-args.txt
+cat /private/tmp/life_tools_ulanzi_command_executor_e2e/shell-syntax.txt
+```
+
+环境文件只核对键存在、`PWD` 和 `CUSTOM_ENV` 精确匹配,不在文档或最终消息中复制完整 PATH。
+
+- [ ] 快速连续按两次并发命令,确认运行态持续到两次都结束,`concurrent.txt` 新增两行。
+
+- [ ] 配置两个不同按键后完全退出并重新打开 Studio;重新选择两个按键,确认标题、命令、目录和环境变量都分别恢复。
+
+- [ ] 查找当前实际日志文件,安装前不预设日志目录;确认后把实测路径写入文档:
+
+```bash
+find "$HOME/Library/Application Support/Ulanzi/UlanziDeck/logs/com.ulanzi.ulanzistudio.commandexecutor" \
+ -type f \
+ -name '*.log' \
+ -print
+```
+
+检查日志包含官方 SDK 的事件 payload、退出码和受限 stdout/stderr,能够定位按键配置与执行问题。日志只留在本机,不把包含命令或环境变量的原文复制进仓库文档。
+
+- [ ] 用 Studio 截图记录:
+
+ 1. 完整 Property Inspector。
+ 2. 两个按键的不同标题和切换后的不同配置。
+ 3. 运行/成功或失败反馈。
+
+截图前清除或遮挡用户名、设备序列号、真实路径中的个人标识、PATH、令牌和其他插件敏感配置。
+
+- [ ] 把 Studio 版本、设备型号、安装路径、日志路径、每项结果和截图链接写入验证报告;更新开发/用户文档中只有实测后才能确定的细节。
+
+- [ ] 实机发现缺陷时按“复现测试 -> 修复 -> 自动化回归 -> 重新安装 -> 重跑失败用例”闭环,不直接在安装目录手改产物。
+
+- [ ] 提交实机证据:
+
+```bash
+git add plugins/unlanzi_d200x/docs
+git commit -m "docs: record Ulanzi Studio D200X validation"
+```
+
+## 12. Task 10:最终审查与交付
+
+**Files:** Review all changed files only.
+
+- [ ] 执行最终验证:
+
+```bash
+cd plugins/unlanzi_d200x/command_executor
+npm ci
+npm test
+./build.sh
+cd ../../..
+git diff --check master...HEAD
+git status --short
+git log --oneline master..HEAD
+```
+
+- [ ] 搜索不允许遗留的内容:
+
+```bash
+rg -n 'TODO|TBD|FIXME|console\.log\(.*(settings|command|environment)' \
+ plugins/unlanzi_d200x \
+ --glob '!docs/2026-07-25-command-executor-implementation-plan.md'
+rg -n '/Users/[^/[:space:]]+|/home/[^/[:space:]]+|/var/folders/[^[:space:]]+' \
+ plugins/unlanzi_d200x README.MD AGENTS.md
+git log --format='%an <%ae>' master..HEAD
+```
+
+第二条允许命中测试专用的 `/Users/example`;其他真实用户名、用户目录和本机临时目录片段不得进入插件源码、构建包、截图或文档。提交作者信息应使用公开的 GitHub 身份和 `users.noreply.github.com` 地址,不提交个人或公司邮箱。
+
+- [ ] 使用 `superpowers:requesting-code-review` 做一次独立 review,优先检查:
+
+ - Shell 注入是否只来自用户明确输入,环境变量名是否严格验证。
+ - 显式环境变量是否在登录配置之后覆盖。
+ - 长命令和 200 参数是否没有 JS 层拆分。
+ - SDK 完整 payload 日志是否保持上游行为,业务输出日志是否按 16 KiB 上限截断。
+ - `context` 隔离、清理和并发完成竞态。
+ - 构建包是否自包含且没有 AppleDouble/node_modules。
+ - 文档是否与真实 Studio 行为一致。
+
+- [ ] 只修复 review 中有证据的问题;每个修复补测试并重新执行受影响验证。
+
+- [ ] 向用户汇报本地分支、commit 列表、测试结果、构建产物和实机证据。未经用户确认,不推送分支、不创建 GitHub PR。
+
+- [ ] 用户确认后:
+
+```bash
+git push -u origin feat/cq/ulanzi_d200x
+gh pr create \
+ --base master \
+ --head feat/cq/ulanzi_d200x \
+ --title "feat: add Ulanzi D200X command executor" \
+ --body-file /private/tmp/life_tools_ulanzi_d200x_pr.md
+```
+
+PR 正文必须包含范围、配置字段、测试/构建结果、Studio/D200X 实机证据、macOS-only 限制和 SDK 固定版本。
+
+## 13. 完成判定
+
+只有同时满足以下条件才可以声称完成:
+
+- `npm test`、`build.sh`、包校验全部通过。
+- 安装包不依赖仓库外的 `node_modules`。
+- Studio 能显示 Action 和 Property Inspector。
+- 两个按键配置隔离,Studio 重启后仍存在。
+- 无参数、Shell 语法、200 参数、用户环境和自定义环境/目录有实体回读证据;并发状态有自动化回归。
+- `exit 7` 的 Alert、Toast 和日志行为有自动化回归;发布前只有相关代码变化时才要求补实体失败回归。
+- 实体 D200X 至少成功触发一次受控临时目录命令。
+- SDK 保留完整 payload 诊断日志,业务 stdout/stderr 日志按每个流 16 KiB 截断,文档明确本机落盘边界。
+- 安装说明、开发指南、用户指南、验证报告和截图与当前实现一致。
+- 推送和 PR 只在用户再次确认后执行。
diff --git a/plugins/unlanzi_d200x/docs/README.md b/plugins/unlanzi_d200x/docs/README.md
new file mode 100644
index 0000000..aaac6e1
--- /dev/null
+++ b/plugins/unlanzi_d200x/docs/README.md
@@ -0,0 +1,31 @@
+# Ulanzi D200X 插件文档
+
+`plugins/unlanzi_d200x/` 用于维护面向 Ulanzi D200X 的插件。不同插件放在独立子目录中,共用本目录里的 SDK 接入、安装、调试和测试经验。
+
+## 文档索引
+
+| 文档 | 状态 | 用途 |
+|---|---|---|
+| [命令执行器设计](2026-07-25-command-executor-design.md) | 已有 | 定义 `command_executor` 的范围、配置模型、运行行为和验收标准 |
+| [命令执行器实现计划](2026-07-25-command-executor-implementation-plan.md) | 已有 | 按 TDD 顺序拆解源码、构建、文档和 Studio/D200X 实机验证 |
+| [Ulanzi 插件开发参考](ulanzi-plugin-development-reference.md) | 已有 | 提炼官方开发指南、本地安装指南和 SDK 的核心接入知识 |
+| [命令执行器安装说明](command-executor-installation.md) | 已实机验证 | 说明 macOS 构建、安装、升级、回滚、验收、日志和卸载 |
+| [命令执行器开发指南](command-executor-development-guide.md) | 已完成 | 记录目录职责、SDK 快照、运行模型、测试、构建、安装和调试 |
+| [命令执行器用户指南](command-executor-user-guide.md) | 已实机验证 | 说明安装、拖拽、配置、长命令、状态、限制和排障 |
+| [命令执行器验证报告](command-executor-validation.md) | 自动化与实机证据已记录 | 区分自动化、Studio 安装和实体 D200X 验证证据 |
+
+## 目录约定
+
+```text
+plugins/unlanzi_d200x/
+├── command_executor/ # 命令执行器源码、测试和构建入口
+└── docs/ # 共用资料和各插件文档
+```
+
+新增插件时应保持以下边界:
+
+- 插件源码、依赖和构建脚本放在自己的子目录内。
+- 可复用的 Ulanzi SDK 接入经验写入共用开发参考。
+- 插件特有的配置、行为、风险和验收方式写入独立文档。
+- 实机截图放在 `docs/assets/