@jonathanleelx/meddle 0.5.0-beta.2 → 0.5.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIG_STRUCTURE.md +8 -2
- package/README.md +2 -0
- package/bin/commands/mock/add.js +33 -4
- package/bin/commands/mock/delete.js +8 -1
- package/bin/commands/mock/index.js +8 -4
- package/bin/commands/mock/list.js +1 -0
- package/bin/commands/mock/toggle.js +8 -1
- package/bin/commands/mock/update.js +31 -5
- package/bin/commands/route/active.js +6 -2
- package/bin/commands/route/add.js +12 -2
- package/bin/commands/route/create.js +8 -1
- package/bin/commands/route/delete.js +12 -2
- package/bin/commands/route/update.js +12 -2
- package/bin/commands/update.js +4 -0
- package/bin/lib/file-access.js +68 -25
- package/bin/lib/output.js +20 -1
- package/bin/lib/proxy-detect.js +99 -20
- package/bin/lib/update-check.js +67 -6
- package/dist/core/config-diagnostics.d.ts.map +1 -1
- package/dist/core/config-diagnostics.js +6 -1
- package/dist/core/config-diagnostics.js.map +1 -1
- package/dist/core/custom-plugin-loader.d.ts.map +1 -1
- package/dist/core/custom-plugin-loader.js +2 -0
- package/dist/core/custom-plugin-loader.js.map +1 -1
- package/dist/core/h2-pool.d.ts +1 -0
- package/dist/core/h2-pool.d.ts.map +1 -1
- package/dist/core/h2-pool.js +79 -23
- package/dist/core/h2-pool.js.map +1 -1
- package/dist/core/mock-handler.d.ts +65 -0
- package/dist/core/mock-handler.d.ts.map +1 -1
- package/dist/core/mock-handler.js +525 -29
- package/dist/core/mock-handler.js.map +1 -1
- package/dist/core/mock-utils.d.ts +58 -0
- package/dist/core/mock-utils.d.ts.map +1 -0
- package/dist/core/mock-utils.js +167 -0
- package/dist/core/mock-utils.js.map +1 -0
- package/dist/core/plugin-bootstrap-runner.d.ts +1 -0
- package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -1
- package/dist/core/plugin-bootstrap-runner.js +37 -1
- package/dist/core/plugin-bootstrap-runner.js.map +1 -1
- package/dist/core/plugin-intercept.d.ts.map +1 -1
- package/dist/core/plugin-intercept.js +1 -0
- package/dist/core/plugin-intercept.js.map +1 -1
- package/dist/core/plugin-source.d.ts +24 -0
- package/dist/core/plugin-source.d.ts.map +1 -0
- package/dist/core/plugin-source.js +47 -0
- package/dist/core/plugin-source.js.map +1 -0
- package/dist/core/types.d.ts +13 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/helpers.d.ts +8 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +22 -5
- package/dist/helpers.js.map +1 -1
- package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -1
- package/dist/plugins/builtin/mock-plugin.js +6 -5
- package/dist/plugins/builtin/mock-plugin.js.map +1 -1
- package/dist/server/agent/tools.d.ts.map +1 -1
- package/dist/server/agent/tools.js +4 -0
- package/dist/server/agent/tools.js.map +1 -1
- package/dist/server/app-info.d.ts +19 -0
- package/dist/server/app-info.d.ts.map +1 -0
- package/dist/server/app-info.js +84 -0
- package/dist/server/app-info.js.map +1 -0
- package/dist/server/config.d.ts +1 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +57 -7
- package/dist/server/config.js.map +1 -1
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js +8 -0
- package/dist/server/health.js.map +1 -1
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/mocks.d.ts +26 -0
- package/dist/server/mocks.d.ts.map +1 -1
- package/dist/server/mocks.js +110 -13
- package/dist/server/mocks.js.map +1 -1
- package/dist/server/plugins.d.ts.map +1 -1
- package/dist/server/plugins.js +56 -11
- package/dist/server/plugins.js.map +1 -1
- package/dist/server/rule-files.d.ts +53 -0
- package/dist/server/rule-files.d.ts.map +1 -1
- package/dist/server/rule-files.js +281 -30
- package/dist/server/rule-files.js.map +1 -1
- package/index.js +33 -5
- package/mcp-server.js +3 -0
- package/package.json +3 -2
- package/plugins/builtin/mock-plugin.ts +9 -5
- package/web/dist/assets/alert-BmsRCcSL.js +1 -0
- package/web/dist/assets/arrow-right-BNum_060.js +1 -0
- package/web/dist/assets/{body-diff-view-D7dCWajG.js → body-diff-view-ByCHr9bv.js} +2 -2
- package/web/dist/assets/button-BFNlGZ5p.js +1 -0
- package/web/dist/assets/check-BkfHQ6re.js +1 -0
- package/web/dist/assets/checkbox-v2kzp7K8.js +1 -0
- package/web/dist/assets/chevron-down-DHwUtL4O.js +1 -0
- package/web/dist/assets/{circle-alert-Q8cZk9Rv.js → circle-alert-zMagrFuI.js} +1 -1
- package/web/dist/assets/{circle-x-C9USji_R.js → circle-x-BEkinhez.js} +1 -1
- package/web/dist/assets/code-DD-AYxxu.js +1 -0
- package/web/dist/assets/{copy-BYjq1hdN.js → copy-BRnscYy1.js} +1 -1
- package/web/dist/assets/detail-panel-DK7pM43g.js +4 -0
- package/web/dist/assets/{dist-D5Dj17P8.js → dist-BCvvYbmI.js} +1 -1
- package/web/dist/assets/dist-BNcjMpca.js +1 -0
- package/web/dist/assets/{dist-Bb9TeEub.js → dist-C-E2R2jk.js} +1 -1
- package/web/dist/assets/{dist-C6eaYCUo.js → dist-CFZ4xMSF.js} +1 -1
- package/web/dist/assets/{dist-C5nMErq8.js → dist-CRmX_NvH.js} +1 -1
- package/web/dist/assets/dist-DD7AvbXP.js +1 -0
- package/web/dist/assets/{dist-Cn0y0IRn.js → dist-LiAcI8CK.js} +1 -1
- package/web/dist/assets/dist-UcOFuoN0.js +5 -0
- package/web/dist/assets/{dist-B8XUKgXe.js → dist-YM3jbhT-.js} +1 -1
- package/web/dist/assets/editor-pane-actions-DN-pxFsl.js +1 -0
- package/web/dist/assets/es2015-CRmLuA2o.js +41 -0
- package/web/dist/assets/{global-panel-shell-R-EZQRhs.js → global-panel-shell-D32I98dR.js} +4 -4
- package/web/dist/assets/health-panel-Dw7qdFVS.js +1 -0
- package/web/dist/assets/index-CYUNdxqh.css +2 -0
- package/web/dist/assets/index-CkoYwomI.js +18 -0
- package/web/dist/assets/mobile-proxy-panel-DtiDkpAa.js +8 -0
- package/web/dist/assets/mock-config-C3jSpNNu.js +1 -0
- package/web/dist/assets/mock-editor-panel-ByCYWJjO.js +20 -0
- package/web/dist/assets/mocks-file-missing-note-DmrrCBYY.js +1 -0
- package/web/dist/assets/monaco-editor-6oLiu4W6.js +11 -0
- package/web/dist/assets/pane-split-DYB05onX.js +1 -0
- package/web/dist/assets/panel-heading-BS8loc49.js +1 -0
- package/web/dist/assets/{pencil-line-Cqe41E4h.js → pencil-line-CEZ7D-de.js} +1 -1
- package/web/dist/assets/plugin-code-editor-DGzCBEpg.js +2 -0
- package/web/dist/assets/plugin-config-NVw234EF.js +2 -0
- package/web/dist/assets/plugin-generator-Ba4hP5rg.js +2 -0
- package/web/dist/assets/plugin-test-dialog-B4ojc2_9.js +7 -0
- package/web/dist/assets/plus-ixZQRprm.js +1 -0
- package/web/dist/assets/{refresh-cw-CG3HaiQT.js → refresh-cw-BfLoHqg5.js} +1 -1
- package/web/dist/assets/rotate-ccw-CRguJya4.js +1 -0
- package/web/dist/assets/route-preview-BspvfNdW.js +1 -0
- package/web/dist/assets/{rule-ai-assistant-panel-CNL1O0GY.js → rule-ai-assistant-panel-CQTsZMJu.js} +2 -2
- package/web/dist/assets/rule-config-DDOeMRXa.js +15 -0
- package/web/dist/assets/rule-overview-panel-CmWdMs4I.js +1 -0
- package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
- package/web/dist/assets/settings-panel-aB0ojA9D.js +1 -0
- package/web/dist/assets/{shield-check-OqUmVLQt.js → shield-check-hQXjdfZ7.js} +1 -1
- package/web/dist/assets/smartphone-D4Q7cQfm.js +1 -0
- package/web/dist/assets/switch-DpLnRvOJ.js +1 -0
- package/web/dist/assets/{syntax-highlight-BHsSDUha.js → syntax-highlight-EJCxGikd.js} +1 -1
- package/web/dist/assets/table-2-DvGHhMm4.js +1 -0
- package/web/dist/assets/table-C3mgEjkP.js +1 -0
- package/web/dist/assets/textarea-BjWF0pk4.js +1 -0
- package/web/dist/assets/{wand-sparkles-wkul5vje.js → wand-sparkles-DDGjzjnX.js} +1 -1
- package/web/dist/assets/{wifi-D9M35Ajj.js → wifi-QkTcTrED.js} +1 -1
- package/web/dist/assets/{zap-Djo4cgux.js → zap-CEVoVkAg.js} +1 -1
- package/web/dist/index.html +17 -12
- package/web/dist/assets/alert-Bve02Ufr.js +0 -1
- package/web/dist/assets/arrow-right-B4Mrlj2-.js +0 -1
- package/web/dist/assets/button-BTToMaBW.js +0 -1
- package/web/dist/assets/checkbox-DK9f9Mxu.js +0 -1
- package/web/dist/assets/chevron-down-CPngAx4M.js +0 -1
- package/web/dist/assets/chevron-right-V1DGIDbL.js +0 -1
- package/web/dist/assets/code-5_eJ_XpJ.js +0 -1
- package/web/dist/assets/code-fixer-IRTVs-a6.js +0 -20
- package/web/dist/assets/detail-panel--2r_Fgpi.js +0 -4
- package/web/dist/assets/dist-CxdY_A67.js +0 -5
- package/web/dist/assets/es2015-CfXu88Y7.js +0 -41
- package/web/dist/assets/file-system-access-DK7FI6pN.js +0 -1
- package/web/dist/assets/health-panel-CKt2-Gkj.js +0 -1
- package/web/dist/assets/index-DhzkcDnh.js +0 -18
- package/web/dist/assets/index-PcoCj3ST.css +0 -2
- package/web/dist/assets/mobile-proxy-panel-Dxz-7iYx.js +0 -8
- package/web/dist/assets/mock-config-84LSNP6M.js +0 -1
- package/web/dist/assets/mock-editor-panel-RJ1abekr.js +0 -1
- package/web/dist/assets/monaco-editor-CbL970eS.js +0 -11
- package/web/dist/assets/plugin-code-editor-DfLc4JdP.js +0 -2
- package/web/dist/assets/plugin-config-CT-u4XYw.js +0 -1
- package/web/dist/assets/plugin-generator-CyBK9FuM.js +0 -2
- package/web/dist/assets/plugin-test-dialog-B-NF0sGH.js +0 -7
- package/web/dist/assets/plus-tVxSydJ8.js +0 -1
- package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +0 -1
- package/web/dist/assets/route-preview-C8nW9w7i.js +0 -1
- package/web/dist/assets/rule-config-DXfovZnm.js +0 -15
- package/web/dist/assets/select-DQ6s7CL8.js +0 -1
- package/web/dist/assets/settings-panel-BhXgMr_a.js +0 -1
- package/web/dist/assets/smartphone-DKeDuJTU.js +0 -1
- package/web/dist/assets/switch-C-Xk56TK.js +0 -1
- package/web/dist/assets/table-DymgLlcc.js +0 -1
- package/web/dist/assets/textarea-vuxJwU0x.js +0 -1
- /package/web/dist/assets/{ai-config-store-DQXVJVn_.js → ai-config-store-B_VpGvwx.js} +0 -0
- /package/web/dist/assets/{babel-DsprIpXZ.js → babel-D1quDdNt.js} +0 -0
- /package/web/dist/assets/{clipboard-Dz6MgwV-.js → clipboard-DUgC_tNv.js} +0 -0
- /package/web/dist/assets/{dist-DZJ9SMlJ.js → dist-Bq2klZ3g.js} +0 -0
- /package/web/dist/assets/{estree-BP7k0NIt.js → estree--Vq99DGn.js} +0 -0
- /package/web/dist/assets/{html-6FIWbqxX.js → html-D2PZBPnP.js} +0 -0
- /package/web/dist/assets/{postcss-Dnf30fQU.js → postcss-DfIslWug.js} +0 -0
- /package/web/dist/assets/{standalone-CjxtLKw-.js → standalone-D-fjaAIs.js} +0 -0
package/CONFIG_STRUCTURE.md
CHANGED
|
@@ -173,6 +173,11 @@ https://localhost:13001/micro/app?a=1
|
|
|
173
173
|
"默认规则",
|
|
174
174
|
"beta-rules"
|
|
175
175
|
],
|
|
176
|
+
"ruleFileOrder": [
|
|
177
|
+
"默认规则",
|
|
178
|
+
"beta-rules",
|
|
179
|
+
"disabled-rules"
|
|
180
|
+
],
|
|
176
181
|
"mocksFilePath": "",
|
|
177
182
|
"pluginMode": "off",
|
|
178
183
|
"clientAliases": {
|
|
@@ -198,6 +203,7 @@ https://localhost:13001/micro/app?a=1
|
|
|
198
203
|
| `fontSize` | 界面缩放百分比字符串,例如 `100` |
|
|
199
204
|
| `detailBodySizeKB` | 日志详情保存的请求/响应体截断阈值(KB),默认 `256`;调大可保留更大 body 便于排查,但会增加内存占用(最多 `MAX_DETAIL_SIZE=200` 条同时驻留)。作用范围为单个 session,解析顺序:当前 session 的 `settings.json` → 默认 session(`~/.meddle/settings.json`,仅非默认 session 继承)→ 环境变量 `MEDDLE_MAX_DETAIL_BODY_KB` → 默认 `256`。修改后下次请求即生效(基于 mtime 缓存检测) |
|
|
200
205
|
| `activeRuleFiles` | 当前启用的路由规则文件名称,顺序即合并顺序 |
|
|
206
|
+
| `ruleFileOrder` | Web 规则文件 Tab 的完整显示顺序(含禁用文件);启用文件的合并顺序与其在该数组中的相对顺序一致 |
|
|
201
207
|
| `mocksFilePath` | 自定义 Mock JSON 路径;空值使用默认位置 |
|
|
202
208
|
| `pluginMode` | 插件 Pipeline 模式:`off`、`shadow` 或 `on` |
|
|
203
209
|
| `clientAliases` | 客户端 IP 到设备名称的映射 |
|
|
@@ -223,8 +229,8 @@ https://localhost:13001/micro/app?a=1
|
|
|
223
229
|
|
|
224
230
|
### 路由规则
|
|
225
231
|
|
|
226
|
-
1. 读取 `~/.meddle/settings.json` 中的 `activeRuleFiles
|
|
227
|
-
2.
|
|
232
|
+
1. 读取 `~/.meddle/settings.json` 中的 `activeRuleFiles`(及可选的 `ruleFileOrder`)。
|
|
233
|
+
2. 按 `activeRuleFiles` 数组顺序读取 `~/.meddle/route-rules/<名称>.txt`。启用文件的相对顺序与 Tab 栏(`ruleFileOrder`)中启用文件的相对顺序保持一致。
|
|
228
234
|
3. 合并规则并按顺序匹配。
|
|
229
235
|
4. 如果目录为空,首次启动会创建一个默认规则文件并将其设为启用状态。
|
|
230
236
|
5. 规则文件修改后会自动重新加载。
|
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ irm https://raw.githubusercontent.com/JonathanLee-LX/meddle/main/scripts/install
|
|
|
36
36
|
|
|
37
37
|
二进制产物随 [GitHub Release](https://github.com/JonathanLee-LX/meddle/releases) 发布,也可手动下载。安装目录默认 `~/.local/bin`(已有 `~/.meddle/bin` 旧安装时原地升级),可通过 `MEDDLE_BIN_DIR` 环境变量自定义。`meddle update` 始终更新到当前运行二进制所在目录。
|
|
38
38
|
|
|
39
|
+
**私有仓库:** `meddle update`(含 `meddle update --beta`)/ 安装脚本下载 Release 资产时会读取 `GH_TOKEN`(优先)或 `GITHUB_TOKEN`(`repo` 或 fine-grained `contents:read`)。私有仓安装后升级也需要设置 token;未设置时会表现为 404(checksum sidecar missing)。也可手动:`gh release download -R JonathanLee-LX/meddle -p 'meddle-*'`。
|
|
40
|
+
|
|
39
41
|
### 命令
|
|
40
42
|
|
|
41
43
|
```bash
|
package/bin/commands/mock/add.js
CHANGED
|
@@ -12,23 +12,40 @@ const args = process.argv.slice(3)
|
|
|
12
12
|
const jsonFlag = args.includes('--json')
|
|
13
13
|
output.setJsonMode(jsonFlag)
|
|
14
14
|
|
|
15
|
+
function parseCorsFlag(options) {
|
|
16
|
+
if (!options.cors) return
|
|
17
|
+
const corsHeaders = {
|
|
18
|
+
'Access-Control-Allow-Origin': '{origin}',
|
|
19
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
20
|
+
'Access-Control-Allow-Methods': '*',
|
|
21
|
+
'Access-Control-Allow-Headers': '*',
|
|
22
|
+
}
|
|
23
|
+
options.headers = { ...corsHeaders, ...(options.headers || {}) }
|
|
24
|
+
delete options.cors
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
function parseArgs(args) {
|
|
16
28
|
const options = {}
|
|
17
29
|
for (let i = 0; i < args.length; i++) {
|
|
18
30
|
if (args[i] === '--name') options.name = args[++i]
|
|
19
31
|
else if (args[i] === '--pattern') options.urlPattern = args[++i]
|
|
32
|
+
else if (args[i] === '--query') options.query = args[++i]
|
|
20
33
|
else if (args[i] === '--method') options.method = args[++i]
|
|
21
34
|
else if (args[i] === '--status') options.statusCode = parseInt(args[++i], 10)
|
|
22
35
|
else if (args[i] === '--body') options.body = args[++i]
|
|
23
36
|
else if (args[i] === '--delay') options.delay = parseInt(args[++i], 10)
|
|
37
|
+
else if (args[i] === '--cors') options.cors = true
|
|
24
38
|
else if (args[i] === '--headers') {
|
|
39
|
+
const raw = args[++i]
|
|
25
40
|
try {
|
|
26
|
-
options.headers = JSON.parse(
|
|
41
|
+
options.headers = JSON.parse(raw)
|
|
27
42
|
} catch (_) {
|
|
28
|
-
|
|
43
|
+
output.error('Invalid --headers JSON: ' + raw)
|
|
44
|
+
process.exit(1)
|
|
29
45
|
}
|
|
30
46
|
}
|
|
31
47
|
}
|
|
48
|
+
parseCorsFlag(options)
|
|
32
49
|
return options
|
|
33
50
|
}
|
|
34
51
|
|
|
@@ -49,6 +66,7 @@ async function run() {
|
|
|
49
66
|
const rule = {
|
|
50
67
|
name: options.name,
|
|
51
68
|
urlPattern: options.urlPattern,
|
|
69
|
+
query: options.query || '',
|
|
52
70
|
method: options.method || '*',
|
|
53
71
|
statusCode: options.statusCode || 200,
|
|
54
72
|
delay: options.delay || 0,
|
|
@@ -61,6 +79,7 @@ async function run() {
|
|
|
61
79
|
const running = await isProxyRunning()
|
|
62
80
|
|
|
63
81
|
let result
|
|
82
|
+
let fileOnly = false
|
|
64
83
|
if (running) {
|
|
65
84
|
try {
|
|
66
85
|
result = await apiPost('/api/mocks', rule)
|
|
@@ -68,25 +87,35 @@ async function run() {
|
|
|
68
87
|
} catch (e) {
|
|
69
88
|
// Fallback to file mode
|
|
70
89
|
result = addMockRule(rule)
|
|
90
|
+
fileOnly = true
|
|
71
91
|
}
|
|
72
92
|
} else {
|
|
73
93
|
result = addMockRule(rule)
|
|
94
|
+
fileOnly = true
|
|
74
95
|
}
|
|
75
96
|
|
|
97
|
+
if (fileOnly) output.warnFileOnlyMode()
|
|
98
|
+
|
|
76
99
|
if (jsonFlag) {
|
|
77
|
-
output.jsonRaw(
|
|
100
|
+
output.jsonRaw(fileOnly
|
|
101
|
+
? { ...result, fileOnly: true, warning: output.FILE_ONLY_WARNING }
|
|
102
|
+
: result)
|
|
78
103
|
return
|
|
79
104
|
}
|
|
80
105
|
|
|
81
106
|
output.header('Mock Rule Added')
|
|
82
107
|
output.success(`#${result.id}: ${result.name}`)
|
|
83
108
|
output.kv('Pattern', result.urlPattern)
|
|
109
|
+
if (result.query) output.kv('Query', result.query)
|
|
84
110
|
output.kv('Method', result.method)
|
|
85
111
|
output.kv('Status', result.statusCode)
|
|
86
112
|
output.kv('Delay', `${result.delay}ms`)
|
|
113
|
+
if (result.headers && Object.keys(result.headers).length > 0) {
|
|
114
|
+
output.kv('Headers', JSON.stringify(result.headers))
|
|
115
|
+
}
|
|
87
116
|
}
|
|
88
117
|
|
|
89
118
|
run().catch(e => {
|
|
90
119
|
output.error(e.message)
|
|
91
120
|
process.exit(1)
|
|
92
|
-
})
|
|
121
|
+
})
|
|
@@ -24,6 +24,7 @@ async function run() {
|
|
|
24
24
|
|
|
25
25
|
const running = await isProxyRunning()
|
|
26
26
|
|
|
27
|
+
let fileOnly = false
|
|
27
28
|
if (running) {
|
|
28
29
|
try {
|
|
29
30
|
await apiDelete(`/api/mocks/${id}`)
|
|
@@ -34,6 +35,7 @@ async function run() {
|
|
|
34
35
|
output.error(`Mock rule not found: id=${id}`)
|
|
35
36
|
process.exit(1)
|
|
36
37
|
}
|
|
38
|
+
fileOnly = true
|
|
37
39
|
}
|
|
38
40
|
} else {
|
|
39
41
|
const deleted = deleteMockRule(id)
|
|
@@ -41,10 +43,15 @@ async function run() {
|
|
|
41
43
|
output.error(`Mock rule not found: id=${id}`)
|
|
42
44
|
process.exit(1)
|
|
43
45
|
}
|
|
46
|
+
fileOnly = true
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
if (fileOnly) output.warnFileOnlyMode()
|
|
50
|
+
|
|
46
51
|
if (jsonFlag) {
|
|
47
|
-
output.jsonRaw(
|
|
52
|
+
output.jsonRaw(fileOnly
|
|
53
|
+
? { success: true, id, fileOnly: true, warning: output.FILE_ONLY_WARNING }
|
|
54
|
+
: { success: true, id })
|
|
48
55
|
return
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -32,19 +32,23 @@ Mock Commands:
|
|
|
32
32
|
meddle mock enable <id> Enable a mock rule
|
|
33
33
|
meddle mock disable <id> Disable a mock rule
|
|
34
34
|
|
|
35
|
-
Add Options:
|
|
35
|
+
Add / Update Options:
|
|
36
36
|
--name <n> Rule name
|
|
37
|
-
--pattern <p> URL pattern (regex or string)
|
|
37
|
+
--pattern <p> URL pattern (regex or string; may include query)
|
|
38
|
+
--query <q> Query match (exact key=value params, e.g. window_key=A; not prefix)
|
|
38
39
|
--method <m> HTTP method (GET, POST, *, default: *)
|
|
39
40
|
--status <s> Response status code (default: 200)
|
|
40
41
|
--body <b> Response body content
|
|
41
42
|
--delay <d> Response delay in ms (default: 0)
|
|
43
|
+
--headers <json> Response headers JSON (values may use {origin})
|
|
44
|
+
--cors Shortcut: credentials-friendly CORS headers with {origin}
|
|
42
45
|
|
|
43
46
|
Examples:
|
|
44
47
|
meddle mock list
|
|
45
48
|
meddle mock add --name "API Mock" --pattern "example.com/api" --status 200
|
|
46
|
-
meddle mock
|
|
49
|
+
meddle mock add --name "Policy A" --pattern "/ops/.*/policy" --query "window_key=A" --body '{"key":"A"}' --cors
|
|
50
|
+
meddle mock update 1 --status 404 --headers '{"X-Debug":"1"}'
|
|
47
51
|
meddle mock delete 1
|
|
48
52
|
`)
|
|
49
53
|
process.exit(1)
|
|
50
|
-
}
|
|
54
|
+
}
|
|
@@ -48,6 +48,7 @@ async function run() {
|
|
|
48
48
|
output.section('Active Rules')
|
|
49
49
|
active.forEach(rule => {
|
|
50
50
|
output.success(`#${rule.id}: ${rule.name} (${rule.urlPattern})`)
|
|
51
|
+
if (rule.query) output.kv('Query', rule.query, 4)
|
|
51
52
|
output.kv('Status', rule.statusCode, 4)
|
|
52
53
|
output.kv('Method', rule.method || '*', 4)
|
|
53
54
|
output.kv('Delay', `${rule.delay || 0}ms`, 4)
|
|
@@ -28,6 +28,7 @@ async function run() {
|
|
|
28
28
|
const running = await isProxyRunning()
|
|
29
29
|
|
|
30
30
|
let result
|
|
31
|
+
let fileOnly = false
|
|
31
32
|
if (running) {
|
|
32
33
|
try {
|
|
33
34
|
result = await apiPut(`/api/mocks/${id}`, { enabled })
|
|
@@ -39,6 +40,7 @@ async function run() {
|
|
|
39
40
|
output.error(`Mock rule not found: id=${id}`)
|
|
40
41
|
process.exit(1)
|
|
41
42
|
}
|
|
43
|
+
fileOnly = true
|
|
42
44
|
}
|
|
43
45
|
} else {
|
|
44
46
|
result = updateMockRule(id, { enabled })
|
|
@@ -46,10 +48,15 @@ async function run() {
|
|
|
46
48
|
output.error(`Mock rule not found: id=${id}`)
|
|
47
49
|
process.exit(1)
|
|
48
50
|
}
|
|
51
|
+
fileOnly = true
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
if (fileOnly) output.warnFileOnlyMode()
|
|
55
|
+
|
|
51
56
|
if (jsonFlag) {
|
|
52
|
-
output.jsonRaw(
|
|
57
|
+
output.jsonRaw(fileOnly
|
|
58
|
+
? { ...result, fileOnly: true, warning: output.FILE_ONLY_WARNING }
|
|
59
|
+
: result)
|
|
53
60
|
return
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -5,13 +5,25 @@
|
|
|
5
5
|
const output = require('../../lib/output')
|
|
6
6
|
const { isProxyRunning } = require('../../lib/proxy-detect')
|
|
7
7
|
const { apiPut } = require('../../lib/api-client')
|
|
8
|
-
const { updateMockRule
|
|
8
|
+
const { updateMockRule } = require('../../lib/file-access')
|
|
9
9
|
|
|
10
10
|
// Parse arguments (skip 'mock' and 'update' from argv)
|
|
11
11
|
const args = process.argv.slice(4)
|
|
12
12
|
const jsonFlag = args.includes('--json')
|
|
13
13
|
output.setJsonMode(jsonFlag)
|
|
14
14
|
|
|
15
|
+
function parseCorsFlag(options) {
|
|
16
|
+
if (!options.cors) return
|
|
17
|
+
const corsHeaders = {
|
|
18
|
+
'Access-Control-Allow-Origin': '{origin}',
|
|
19
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
20
|
+
'Access-Control-Allow-Methods': '*',
|
|
21
|
+
'Access-Control-Allow-Headers': '*',
|
|
22
|
+
}
|
|
23
|
+
options.headers = { ...corsHeaders, ...(options.headers || {}) }
|
|
24
|
+
delete options.cors
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
function parseArgs(args) {
|
|
16
28
|
// First positional arg is id
|
|
17
29
|
let id = null
|
|
@@ -20,15 +32,19 @@ function parseArgs(args) {
|
|
|
20
32
|
for (let i = 0; i < args.length; i++) {
|
|
21
33
|
if (args[i] === '--name') options.name = args[++i]
|
|
22
34
|
else if (args[i] === '--pattern') options.urlPattern = args[++i]
|
|
35
|
+
else if (args[i] === '--query') options.query = args[++i]
|
|
23
36
|
else if (args[i] === '--method') options.method = args[++i]
|
|
24
37
|
else if (args[i] === '--status') options.statusCode = parseInt(args[++i], 10)
|
|
25
38
|
else if (args[i] === '--body') options.body = args[++i]
|
|
26
39
|
else if (args[i] === '--delay') options.delay = parseInt(args[++i], 10)
|
|
40
|
+
else if (args[i] === '--cors') options.cors = true
|
|
27
41
|
else if (args[i] === '--headers') {
|
|
42
|
+
const raw = args[++i]
|
|
28
43
|
try {
|
|
29
|
-
options.headers = JSON.parse(
|
|
44
|
+
options.headers = JSON.parse(raw)
|
|
30
45
|
} catch (_) {
|
|
31
|
-
|
|
46
|
+
output.error('Invalid --headers JSON: ' + raw)
|
|
47
|
+
process.exit(1)
|
|
32
48
|
}
|
|
33
49
|
}
|
|
34
50
|
else if (!args[i].startsWith('--') && id === null) {
|
|
@@ -36,6 +52,7 @@ function parseArgs(args) {
|
|
|
36
52
|
}
|
|
37
53
|
}
|
|
38
54
|
|
|
55
|
+
parseCorsFlag(options)
|
|
39
56
|
return { id, options }
|
|
40
57
|
}
|
|
41
58
|
|
|
@@ -55,6 +72,7 @@ async function run() {
|
|
|
55
72
|
const running = await isProxyRunning()
|
|
56
73
|
|
|
57
74
|
let result
|
|
75
|
+
let fileOnly = false
|
|
58
76
|
if (running) {
|
|
59
77
|
try {
|
|
60
78
|
result = await apiPut(`/api/mocks/${id}`, options)
|
|
@@ -66,6 +84,7 @@ async function run() {
|
|
|
66
84
|
output.error(`Mock rule not found: id=${id}`)
|
|
67
85
|
process.exit(1)
|
|
68
86
|
}
|
|
87
|
+
fileOnly = true
|
|
69
88
|
}
|
|
70
89
|
} else {
|
|
71
90
|
result = updateMockRule(id, options)
|
|
@@ -73,22 +92,29 @@ async function run() {
|
|
|
73
92
|
output.error(`Mock rule not found: id=${id}`)
|
|
74
93
|
process.exit(1)
|
|
75
94
|
}
|
|
95
|
+
fileOnly = true
|
|
76
96
|
}
|
|
77
97
|
|
|
98
|
+
if (fileOnly) output.warnFileOnlyMode()
|
|
99
|
+
|
|
78
100
|
if (jsonFlag) {
|
|
79
|
-
output.jsonRaw(
|
|
101
|
+
output.jsonRaw(fileOnly
|
|
102
|
+
? { ...result, fileOnly: true, warning: output.FILE_ONLY_WARNING }
|
|
103
|
+
: result)
|
|
80
104
|
return
|
|
81
105
|
}
|
|
82
106
|
|
|
83
107
|
output.header('Mock Rule Updated')
|
|
84
108
|
output.success(`#${result.id}: ${result.name}`)
|
|
85
109
|
if (options.urlPattern) output.kv('Pattern', result.urlPattern)
|
|
110
|
+
if (options.query !== undefined) output.kv('Query', result.query || '(none)')
|
|
86
111
|
if (options.method) output.kv('Method', result.method)
|
|
87
112
|
if (options.statusCode) output.kv('Status', result.statusCode)
|
|
88
113
|
if (options.delay) output.kv('Delay', `${result.delay}ms`)
|
|
114
|
+
if (options.headers) output.kv('Headers', JSON.stringify(result.headers || {}))
|
|
89
115
|
}
|
|
90
116
|
|
|
91
117
|
run().catch(e => {
|
|
92
118
|
output.error(e.message)
|
|
93
119
|
process.exit(1)
|
|
94
|
-
})
|
|
120
|
+
})
|
|
@@ -64,8 +64,10 @@ async function run() {
|
|
|
64
64
|
// Disable all, then enable target
|
|
65
65
|
files.forEach(f => setRuleFileEnabled(f.name, f.name === targetFile))
|
|
66
66
|
|
|
67
|
+
output.warnFileOnlyMode()
|
|
68
|
+
|
|
67
69
|
if (jsonFlag) {
|
|
68
|
-
output.jsonRaw({ success: true, activeFile: targetFile })
|
|
70
|
+
output.jsonRaw({ success: true, activeFile: targetFile, fileOnly: true, warning: output.FILE_ONLY_WARNING })
|
|
69
71
|
return
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -83,8 +85,10 @@ async function run() {
|
|
|
83
85
|
|
|
84
86
|
files.forEach(f => setRuleFileEnabled(f.name, f.name === targetFile))
|
|
85
87
|
|
|
88
|
+
output.warnFileOnlyMode()
|
|
89
|
+
|
|
86
90
|
if (jsonFlag) {
|
|
87
|
-
output.jsonRaw({ success: true, activeFile: targetFile })
|
|
91
|
+
output.jsonRaw({ success: true, activeFile: targetFile, fileOnly: true, warning: output.FILE_ONLY_WARNING })
|
|
88
92
|
return
|
|
89
93
|
}
|
|
90
94
|
|
|
@@ -81,12 +81,17 @@ async function run() {
|
|
|
81
81
|
|
|
82
82
|
const newContent = ruleMapToEprcText(ruleMap)
|
|
83
83
|
saveRuleFileContent(fileName, newContent)
|
|
84
|
+
output.warnFileOnlyMode()
|
|
84
85
|
|
|
85
86
|
if (jsonFlag) {
|
|
86
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt
|
|
87
|
+
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt,
|
|
88
|
+
fileOnly: true,
|
|
89
|
+
warning: output.FILE_ONLY_WARNING
|
|
90
|
+
})
|
|
87
91
|
return
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
|
|
90
95
|
output.header('Route Rule Added')
|
|
91
96
|
output.success(`${pat} -> ${tgt}`)
|
|
92
97
|
output.kv('File', fileName)
|
|
@@ -109,12 +114,17 @@ async function run() {
|
|
|
109
114
|
|
|
110
115
|
const newContent = ruleMapToEprcText(ruleMap)
|
|
111
116
|
saveRuleFileContent(fileName, newContent)
|
|
117
|
+
output.warnFileOnlyMode()
|
|
112
118
|
|
|
113
119
|
if (jsonFlag) {
|
|
114
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt
|
|
120
|
+
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt,
|
|
121
|
+
fileOnly: true,
|
|
122
|
+
warning: output.FILE_ONLY_WARNING
|
|
123
|
+
})
|
|
115
124
|
return
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
|
|
118
128
|
output.header('Route Rule Added')
|
|
119
129
|
output.success(`${pat} -> ${tgt}`)
|
|
120
130
|
output.kv('File', fileName)
|
|
@@ -34,6 +34,7 @@ async function run() {
|
|
|
34
34
|
const running = await isProxyRunning()
|
|
35
35
|
|
|
36
36
|
let result
|
|
37
|
+
let fileOnly = false
|
|
37
38
|
if (running) {
|
|
38
39
|
try {
|
|
39
40
|
result = await apiPost('/api/rule-files', {
|
|
@@ -46,6 +47,7 @@ async function run() {
|
|
|
46
47
|
// Fallback to file mode
|
|
47
48
|
try {
|
|
48
49
|
result = createRuleFile(fileName, content, true)
|
|
50
|
+
fileOnly = true
|
|
49
51
|
} catch (err) {
|
|
50
52
|
output.error(err.message)
|
|
51
53
|
process.exit(1)
|
|
@@ -54,14 +56,19 @@ async function run() {
|
|
|
54
56
|
} else {
|
|
55
57
|
try {
|
|
56
58
|
result = createRuleFile(fileName, content, true)
|
|
59
|
+
fileOnly = true
|
|
57
60
|
} catch (err) {
|
|
58
61
|
output.error(err.message)
|
|
59
62
|
process.exit(1)
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
if (fileOnly) output.warnFileOnlyMode()
|
|
67
|
+
|
|
63
68
|
if (jsonFlag) {
|
|
64
|
-
output.jsonRaw(
|
|
69
|
+
output.jsonRaw(fileOnly
|
|
70
|
+
? { ...result, fileOnly: true, warning: output.FILE_ONLY_WARNING }
|
|
71
|
+
: result)
|
|
65
72
|
return
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -75,12 +75,17 @@ async function run() {
|
|
|
75
75
|
delete ruleMap[internalKey]
|
|
76
76
|
const newContent = ruleMapToEprcText(ruleMap)
|
|
77
77
|
saveRuleFileContent(fileName, newContent)
|
|
78
|
+
output.warnFileOnlyMode()
|
|
78
79
|
|
|
79
80
|
if (jsonFlag) {
|
|
80
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat
|
|
81
|
+
output.jsonRaw({ success: true, file: fileName, pattern: pat,
|
|
82
|
+
fileOnly: true,
|
|
83
|
+
warning: output.FILE_ONLY_WARNING
|
|
84
|
+
})
|
|
81
85
|
return
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
|
|
84
89
|
output.header('Route Rule Deleted')
|
|
85
90
|
output.success(pat)
|
|
86
91
|
output.kv('File', fileName)
|
|
@@ -102,12 +107,17 @@ async function run() {
|
|
|
102
107
|
delete ruleMap[internalKey]
|
|
103
108
|
const newContent = ruleMapToEprcText(ruleMap)
|
|
104
109
|
saveRuleFileContent(fileName, newContent)
|
|
110
|
+
output.warnFileOnlyMode()
|
|
105
111
|
|
|
106
112
|
if (jsonFlag) {
|
|
107
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat
|
|
113
|
+
output.jsonRaw({ success: true, file: fileName, pattern: pat,
|
|
114
|
+
fileOnly: true,
|
|
115
|
+
warning: output.FILE_ONLY_WARNING
|
|
116
|
+
})
|
|
108
117
|
return
|
|
109
118
|
}
|
|
110
119
|
|
|
120
|
+
|
|
111
121
|
output.header('Route Rule Deleted')
|
|
112
122
|
output.success(pat)
|
|
113
123
|
output.kv('File', fileName)
|
|
@@ -81,12 +81,17 @@ async function run() {
|
|
|
81
81
|
ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
|
|
82
82
|
const newContent = ruleMapToEprcText(ruleMap)
|
|
83
83
|
saveRuleFileContent(fileName, newContent)
|
|
84
|
+
output.warnFileOnlyMode()
|
|
84
85
|
|
|
85
86
|
if (jsonFlag) {
|
|
86
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt
|
|
87
|
+
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt,
|
|
88
|
+
fileOnly: true,
|
|
89
|
+
warning: output.FILE_ONLY_WARNING
|
|
90
|
+
})
|
|
87
91
|
return
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
|
|
90
95
|
output.header('Route Rule Updated')
|
|
91
96
|
output.success(`${pat} -> ${tgt}`)
|
|
92
97
|
output.kv('File', fileName)
|
|
@@ -108,12 +113,17 @@ async function run() {
|
|
|
108
113
|
ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
|
|
109
114
|
const newContent = ruleMapToEprcText(ruleMap)
|
|
110
115
|
saveRuleFileContent(fileName, newContent)
|
|
116
|
+
output.warnFileOnlyMode()
|
|
111
117
|
|
|
112
118
|
if (jsonFlag) {
|
|
113
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt
|
|
119
|
+
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt,
|
|
120
|
+
fileOnly: true,
|
|
121
|
+
warning: output.FILE_ONLY_WARNING
|
|
122
|
+
})
|
|
114
123
|
return
|
|
115
124
|
}
|
|
116
125
|
|
|
126
|
+
|
|
117
127
|
output.header('Route Rule Updated')
|
|
118
128
|
output.success(`${pat} -> ${tgt}`)
|
|
119
129
|
output.kv('File', fileName)
|
package/bin/commands/update.js
CHANGED
|
@@ -77,6 +77,10 @@ meddle update - 检查并升级 meddle
|
|
|
77
77
|
meddle update --auto on|off 启用/禁用自动更新 (默认关闭)
|
|
78
78
|
meddle update status 查看安装方式、当前版本和自动更新开关
|
|
79
79
|
meddle update --help, -h 显示帮助
|
|
80
|
+
|
|
81
|
+
私有仓库二进制更新需要设置 GH_TOKEN 或 GITHUB_TOKEN(优先 GH_TOKEN;
|
|
82
|
+
权限:repo,或 fine-grained contents:read)。也可:
|
|
83
|
+
gh release download -R JonathanLee-LX/meddle -p 'meddle-*'
|
|
80
84
|
`)
|
|
81
85
|
}
|
|
82
86
|
|
package/bin/lib/file-access.js
CHANGED
|
@@ -75,23 +75,49 @@ function loadMockRules() {
|
|
|
75
75
|
return []
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function readMocksFileMeta(mocksPath) {
|
|
79
|
+
try {
|
|
80
|
+
if (fs.existsSync(mocksPath)) {
|
|
81
|
+
const data = JSON.parse(fs.readFileSync(mocksPath, 'utf8'))
|
|
82
|
+
return { nextId: Number(data.nextId) || 0, lastId: Number(data.lastId) || 0 }
|
|
83
|
+
}
|
|
84
|
+
} catch (_) {}
|
|
85
|
+
return { nextId: 0, lastId: 0 }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function maxRuleId(rules) {
|
|
89
|
+
let maxId = 0
|
|
90
|
+
rules.forEach(r => {
|
|
91
|
+
if (r && Number.isInteger(r.id) && r.id > maxId) maxId = r.id
|
|
92
|
+
})
|
|
93
|
+
return maxId
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Next id to hand out. Shares the server's `nextId` field so ids stay
|
|
98
|
+
* monotonic across CLI file-mode and the running proxy (issue #115); also
|
|
99
|
+
* honours the legacy CLI `lastId` field.
|
|
100
|
+
*/
|
|
101
|
+
function nextMockId(rules, meta) {
|
|
102
|
+
return Math.max(1, meta.nextId || 0, (meta.lastId || 0) + 1, maxRuleId(rules) + 1)
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
/**
|
|
79
106
|
* Save mock rules to file
|
|
80
107
|
*/
|
|
81
|
-
function saveMockRules(rules) {
|
|
108
|
+
function saveMockRules(rules, nextId) {
|
|
82
109
|
const mocksPath = getMocksPath()
|
|
83
110
|
const dir = path.dirname(mocksPath)
|
|
84
111
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
85
112
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
rules.forEach(r => {
|
|
89
|
-
if (r.id > maxId) maxId = r.id
|
|
90
|
-
})
|
|
91
|
-
|
|
113
|
+
const meta = readMocksFileMeta(mocksPath)
|
|
114
|
+
const seq = Math.max(nextId || 0, nextMockId(rules, meta))
|
|
92
115
|
const data = {
|
|
116
|
+
// `nextId` is what the proxy reads; keep it so deleting the newest rule
|
|
117
|
+
// does not make its id reusable (issue #115). `lastId` kept for older CLIs.
|
|
118
|
+
nextId: seq,
|
|
93
119
|
rules,
|
|
94
|
-
lastId:
|
|
120
|
+
lastId: seq - 1
|
|
95
121
|
}
|
|
96
122
|
fs.writeFileSync(mocksPath, JSON.stringify(data, null, 2), 'utf8')
|
|
97
123
|
}
|
|
@@ -103,23 +129,14 @@ function addMockRule(rule) {
|
|
|
103
129
|
const rules = loadMockRules()
|
|
104
130
|
const mocksPath = getMocksPath()
|
|
105
131
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (fs.existsSync(mocksPath)) {
|
|
110
|
-
const data = JSON.parse(fs.readFileSync(mocksPath, 'utf8'))
|
|
111
|
-
nextId = (data.lastId || 0) + 1
|
|
112
|
-
}
|
|
113
|
-
} catch (_) {}
|
|
114
|
-
if (rules.length > 0) {
|
|
115
|
-
const maxId = Math.max(...rules.map(r => r.id))
|
|
116
|
-
if (maxId >= nextId) nextId = maxId + 1
|
|
117
|
-
}
|
|
132
|
+
// Issue #115: never reuse an existing (or previously handed-out) id, and
|
|
133
|
+
// ignore any id the caller put on the rule — add never overwrites.
|
|
134
|
+
const nextId = nextMockId(rules, readMocksFileMeta(mocksPath))
|
|
118
135
|
|
|
119
|
-
rule
|
|
120
|
-
rules.push(
|
|
121
|
-
saveMockRules(rules)
|
|
122
|
-
return
|
|
136
|
+
const created = { ...rule, id: nextId }
|
|
137
|
+
rules.push(created)
|
|
138
|
+
saveMockRules(rules, nextId + 1)
|
|
139
|
+
return created
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
/**
|
|
@@ -167,6 +184,30 @@ function getRuleFilePath(name) {
|
|
|
167
184
|
/**
|
|
168
185
|
* List all rule files
|
|
169
186
|
*/
|
|
187
|
+
function getRuleFileOrderNames() {
|
|
188
|
+
const settings = loadSettings()
|
|
189
|
+
const arr = settings.ruleFileOrder
|
|
190
|
+
return Array.isArray(arr) ? arr.filter((name) => typeof name === 'string') : []
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveRuleFileOrder(diskNames, storedOrder, activeNames) {
|
|
194
|
+
const diskSet = new Set(diskNames)
|
|
195
|
+
const ordered = []
|
|
196
|
+
const seen = new Set()
|
|
197
|
+
const pushUnique = (name) => {
|
|
198
|
+
if (!diskSet.has(name) || seen.has(name)) return
|
|
199
|
+
ordered.push(name)
|
|
200
|
+
seen.add(name)
|
|
201
|
+
}
|
|
202
|
+
if (storedOrder.length > 0) {
|
|
203
|
+
for (const name of storedOrder) pushUnique(name)
|
|
204
|
+
} else {
|
|
205
|
+
for (const name of activeNames) pushUnique(name)
|
|
206
|
+
}
|
|
207
|
+
for (const name of diskNames) pushUnique(name)
|
|
208
|
+
return ordered
|
|
209
|
+
}
|
|
210
|
+
|
|
170
211
|
function listRuleFiles() {
|
|
171
212
|
ensureRouteRulesDir()
|
|
172
213
|
const activeNames = getActiveRuleFileNames()
|
|
@@ -175,7 +216,9 @@ function listRuleFiles() {
|
|
|
175
216
|
.filter(f => f.endsWith('.txt'))
|
|
176
217
|
.map(f => f.replace(/\.txt$/, ''))
|
|
177
218
|
|
|
178
|
-
|
|
219
|
+
const ordered = resolveRuleFileOrder(files, getRuleFileOrderNames(), activeNames)
|
|
220
|
+
|
|
221
|
+
return ordered.map(name => {
|
|
179
222
|
const filePath = getRuleFilePath(name)
|
|
180
223
|
let ruleCount = 0
|
|
181
224
|
let excludeCount = 0
|