@jonathanleelx/meddle 0.5.0-beta.2 → 0.5.0-beta.20

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.
Files changed (185) hide show
  1. package/CONFIG_STRUCTURE.md +8 -2
  2. package/README.md +2 -0
  3. package/bin/commands/mock/add.js +33 -4
  4. package/bin/commands/mock/delete.js +8 -1
  5. package/bin/commands/mock/index.js +8 -4
  6. package/bin/commands/mock/list.js +1 -0
  7. package/bin/commands/mock/toggle.js +8 -1
  8. package/bin/commands/mock/update.js +31 -5
  9. package/bin/commands/route/active.js +6 -2
  10. package/bin/commands/route/add.js +12 -2
  11. package/bin/commands/route/create.js +8 -1
  12. package/bin/commands/route/delete.js +12 -2
  13. package/bin/commands/route/update.js +12 -2
  14. package/bin/commands/update.js +4 -0
  15. package/bin/lib/file-access.js +68 -25
  16. package/bin/lib/output.js +20 -1
  17. package/bin/lib/proxy-detect.js +99 -20
  18. package/bin/lib/update-check.js +67 -6
  19. package/dist/core/custom-plugin-loader.d.ts.map +1 -1
  20. package/dist/core/custom-plugin-loader.js +2 -0
  21. package/dist/core/custom-plugin-loader.js.map +1 -1
  22. package/dist/core/h2-pool.d.ts +1 -0
  23. package/dist/core/h2-pool.d.ts.map +1 -1
  24. package/dist/core/h2-pool.js +79 -23
  25. package/dist/core/h2-pool.js.map +1 -1
  26. package/dist/core/mock-handler.d.ts +40 -0
  27. package/dist/core/mock-handler.d.ts.map +1 -1
  28. package/dist/core/mock-handler.js +453 -28
  29. package/dist/core/mock-handler.js.map +1 -1
  30. package/dist/core/mock-utils.d.ts +58 -0
  31. package/dist/core/mock-utils.d.ts.map +1 -0
  32. package/dist/core/mock-utils.js +167 -0
  33. package/dist/core/mock-utils.js.map +1 -0
  34. package/dist/core/plugin-bootstrap-runner.d.ts +1 -0
  35. package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -1
  36. package/dist/core/plugin-bootstrap-runner.js +37 -1
  37. package/dist/core/plugin-bootstrap-runner.js.map +1 -1
  38. package/dist/core/plugin-intercept.d.ts.map +1 -1
  39. package/dist/core/plugin-intercept.js +1 -0
  40. package/dist/core/plugin-intercept.js.map +1 -1
  41. package/dist/core/plugin-source.d.ts +24 -0
  42. package/dist/core/plugin-source.d.ts.map +1 -0
  43. package/dist/core/plugin-source.js +47 -0
  44. package/dist/core/plugin-source.js.map +1 -0
  45. package/dist/core/types.d.ts +13 -1
  46. package/dist/core/types.d.ts.map +1 -1
  47. package/dist/helpers.d.ts +8 -1
  48. package/dist/helpers.d.ts.map +1 -1
  49. package/dist/helpers.js +22 -5
  50. package/dist/helpers.js.map +1 -1
  51. package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -1
  52. package/dist/plugins/builtin/mock-plugin.js +6 -5
  53. package/dist/plugins/builtin/mock-plugin.js.map +1 -1
  54. package/dist/server/agent/tools.d.ts.map +1 -1
  55. package/dist/server/agent/tools.js +4 -0
  56. package/dist/server/agent/tools.js.map +1 -1
  57. package/dist/server/app-info.d.ts +19 -0
  58. package/dist/server/app-info.d.ts.map +1 -0
  59. package/dist/server/app-info.js +84 -0
  60. package/dist/server/app-info.js.map +1 -0
  61. package/dist/server/config.d.ts +1 -0
  62. package/dist/server/config.d.ts.map +1 -1
  63. package/dist/server/config.js +23 -3
  64. package/dist/server/config.js.map +1 -1
  65. package/dist/server/health.d.ts.map +1 -1
  66. package/dist/server/health.js +8 -0
  67. package/dist/server/health.js.map +1 -1
  68. package/dist/server/index.d.ts +15 -0
  69. package/dist/server/index.d.ts.map +1 -1
  70. package/dist/server/index.js +2 -0
  71. package/dist/server/index.js.map +1 -1
  72. package/dist/server/mocks.d.ts +23 -0
  73. package/dist/server/mocks.d.ts.map +1 -1
  74. package/dist/server/mocks.js +108 -13
  75. package/dist/server/mocks.js.map +1 -1
  76. package/dist/server/plugins.d.ts.map +1 -1
  77. package/dist/server/plugins.js +56 -11
  78. package/dist/server/plugins.js.map +1 -1
  79. package/dist/server/rule-files.d.ts +53 -0
  80. package/dist/server/rule-files.d.ts.map +1 -1
  81. package/dist/server/rule-files.js +281 -30
  82. package/dist/server/rule-files.js.map +1 -1
  83. package/index.js +26 -5
  84. package/mcp-server.js +3 -0
  85. package/package.json +3 -2
  86. package/plugins/builtin/mock-plugin.ts +9 -5
  87. package/web/dist/assets/alert-BmsRCcSL.js +1 -0
  88. package/web/dist/assets/arrow-right-BNum_060.js +1 -0
  89. package/web/dist/assets/{body-diff-view-D7dCWajG.js → body-diff-view-ByCHr9bv.js} +2 -2
  90. package/web/dist/assets/button-BFNlGZ5p.js +1 -0
  91. package/web/dist/assets/check-BkfHQ6re.js +1 -0
  92. package/web/dist/assets/checkbox-v2kzp7K8.js +1 -0
  93. package/web/dist/assets/chevron-down-DHwUtL4O.js +1 -0
  94. package/web/dist/assets/{circle-alert-Q8cZk9Rv.js → circle-alert-zMagrFuI.js} +1 -1
  95. package/web/dist/assets/{circle-x-C9USji_R.js → circle-x-BEkinhez.js} +1 -1
  96. package/web/dist/assets/code-DD-AYxxu.js +1 -0
  97. package/web/dist/assets/{copy-BYjq1hdN.js → copy-BRnscYy1.js} +1 -1
  98. package/web/dist/assets/detail-panel-CO_7xRlW.js +4 -0
  99. package/web/dist/assets/{dist-D5Dj17P8.js → dist-BCvvYbmI.js} +1 -1
  100. package/web/dist/assets/dist-BNcjMpca.js +1 -0
  101. package/web/dist/assets/{dist-Bb9TeEub.js → dist-C-E2R2jk.js} +1 -1
  102. package/web/dist/assets/{dist-C6eaYCUo.js → dist-CFZ4xMSF.js} +1 -1
  103. package/web/dist/assets/{dist-C5nMErq8.js → dist-CRmX_NvH.js} +1 -1
  104. package/web/dist/assets/dist-DD7AvbXP.js +1 -0
  105. package/web/dist/assets/{dist-Cn0y0IRn.js → dist-LiAcI8CK.js} +1 -1
  106. package/web/dist/assets/dist-UcOFuoN0.js +5 -0
  107. package/web/dist/assets/{dist-B8XUKgXe.js → dist-YM3jbhT-.js} +1 -1
  108. package/web/dist/assets/editor-pane-actions-DN-pxFsl.js +1 -0
  109. package/web/dist/assets/es2015-CRmLuA2o.js +41 -0
  110. package/web/dist/assets/{global-panel-shell-R-EZQRhs.js → global-panel-shell-dKKTLNpP.js} +4 -4
  111. package/web/dist/assets/health-panel-CUEWcU8g.js +1 -0
  112. package/web/dist/assets/index-B9-_OFNJ.js +18 -0
  113. package/web/dist/assets/index-D9jGD_NA.css +2 -0
  114. package/web/dist/assets/{mobile-proxy-panel-Dxz-7iYx.js → mobile-proxy-panel-smm4Pg7o.js} +6 -6
  115. package/web/dist/assets/mock-config-BA0GNr6q.js +1 -0
  116. package/web/dist/assets/mock-editor-panel-ttVorhGI.js +20 -0
  117. package/web/dist/assets/mocks-file-missing-note-DBYZjOpC.js +1 -0
  118. package/web/dist/assets/monaco-editor-DH2CE6FN.js +11 -0
  119. package/web/dist/assets/pane-split-40_YQZ0-.js +1 -0
  120. package/web/dist/assets/panel-heading-BaLG_KAE.js +1 -0
  121. package/web/dist/assets/{pencil-line-Cqe41E4h.js → pencil-line-CEZ7D-de.js} +1 -1
  122. package/web/dist/assets/plugin-code-editor-B2Sc5Ngy.js +2 -0
  123. package/web/dist/assets/plugin-config-nnVWs8jW.js +2 -0
  124. package/web/dist/assets/plugin-generator-B2CAEqPa.js +2 -0
  125. package/web/dist/assets/plugin-test-dialog-BU0As6z0.js +7 -0
  126. package/web/dist/assets/plus-ixZQRprm.js +1 -0
  127. package/web/dist/assets/{refresh-cw-CG3HaiQT.js → refresh-cw-BfLoHqg5.js} +1 -1
  128. package/web/dist/assets/rotate-ccw-CRguJya4.js +1 -0
  129. package/web/dist/assets/route-preview-Do3jsXLI.js +1 -0
  130. package/web/dist/assets/{rule-ai-assistant-panel-CNL1O0GY.js → rule-ai-assistant-panel-g-UkLb_B.js} +2 -2
  131. package/web/dist/assets/rule-config-Bzw8J0q1.js +15 -0
  132. package/web/dist/assets/rule-overview-panel-DIBRGnrb.js +1 -0
  133. package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
  134. package/web/dist/assets/settings-panel-CrvG4M_b.js +1 -0
  135. package/web/dist/assets/{shield-check-OqUmVLQt.js → shield-check-hQXjdfZ7.js} +1 -1
  136. package/web/dist/assets/smartphone-D4Q7cQfm.js +1 -0
  137. package/web/dist/assets/switch-DpLnRvOJ.js +1 -0
  138. package/web/dist/assets/{syntax-highlight-BHsSDUha.js → syntax-highlight-EJCxGikd.js} +1 -1
  139. package/web/dist/assets/table-2-DvGHhMm4.js +1 -0
  140. package/web/dist/assets/table-C3mgEjkP.js +1 -0
  141. package/web/dist/assets/textarea-BjWF0pk4.js +1 -0
  142. package/web/dist/assets/{wand-sparkles-wkul5vje.js → wand-sparkles-DDGjzjnX.js} +1 -1
  143. package/web/dist/assets/{wifi-D9M35Ajj.js → wifi-QkTcTrED.js} +1 -1
  144. package/web/dist/assets/{zap-Djo4cgux.js → zap-CEVoVkAg.js} +1 -1
  145. package/web/dist/index.html +17 -12
  146. package/web/dist/assets/alert-Bve02Ufr.js +0 -1
  147. package/web/dist/assets/arrow-right-B4Mrlj2-.js +0 -1
  148. package/web/dist/assets/button-BTToMaBW.js +0 -1
  149. package/web/dist/assets/checkbox-DK9f9Mxu.js +0 -1
  150. package/web/dist/assets/chevron-down-CPngAx4M.js +0 -1
  151. package/web/dist/assets/chevron-right-V1DGIDbL.js +0 -1
  152. package/web/dist/assets/code-5_eJ_XpJ.js +0 -1
  153. package/web/dist/assets/code-fixer-IRTVs-a6.js +0 -20
  154. package/web/dist/assets/detail-panel--2r_Fgpi.js +0 -4
  155. package/web/dist/assets/dist-CxdY_A67.js +0 -5
  156. package/web/dist/assets/es2015-CfXu88Y7.js +0 -41
  157. package/web/dist/assets/file-system-access-DK7FI6pN.js +0 -1
  158. package/web/dist/assets/health-panel-CKt2-Gkj.js +0 -1
  159. package/web/dist/assets/index-DhzkcDnh.js +0 -18
  160. package/web/dist/assets/index-PcoCj3ST.css +0 -2
  161. package/web/dist/assets/mock-config-84LSNP6M.js +0 -1
  162. package/web/dist/assets/mock-editor-panel-RJ1abekr.js +0 -1
  163. package/web/dist/assets/monaco-editor-CbL970eS.js +0 -11
  164. package/web/dist/assets/plugin-code-editor-DfLc4JdP.js +0 -2
  165. package/web/dist/assets/plugin-config-CT-u4XYw.js +0 -1
  166. package/web/dist/assets/plugin-generator-CyBK9FuM.js +0 -2
  167. package/web/dist/assets/plugin-test-dialog-B-NF0sGH.js +0 -7
  168. package/web/dist/assets/plus-tVxSydJ8.js +0 -1
  169. package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +0 -1
  170. package/web/dist/assets/route-preview-C8nW9w7i.js +0 -1
  171. package/web/dist/assets/rule-config-DXfovZnm.js +0 -15
  172. package/web/dist/assets/select-DQ6s7CL8.js +0 -1
  173. package/web/dist/assets/settings-panel-BhXgMr_a.js +0 -1
  174. package/web/dist/assets/smartphone-DKeDuJTU.js +0 -1
  175. package/web/dist/assets/switch-C-Xk56TK.js +0 -1
  176. package/web/dist/assets/table-DymgLlcc.js +0 -1
  177. package/web/dist/assets/textarea-vuxJwU0x.js +0 -1
  178. /package/web/dist/assets/{ai-config-store-DQXVJVn_.js → ai-config-store-B_VpGvwx.js} +0 -0
  179. /package/web/dist/assets/{babel-DsprIpXZ.js → babel-D1quDdNt.js} +0 -0
  180. /package/web/dist/assets/{clipboard-Dz6MgwV-.js → clipboard-DUgC_tNv.js} +0 -0
  181. /package/web/dist/assets/{dist-DZJ9SMlJ.js → dist-Bq2klZ3g.js} +0 -0
  182. /package/web/dist/assets/{estree-BP7k0NIt.js → estree--Vq99DGn.js} +0 -0
  183. /package/web/dist/assets/{html-6FIWbqxX.js → html-D2PZBPnP.js} +0 -0
  184. /package/web/dist/assets/{postcss-Dnf30fQU.js → postcss-DfIslWug.js} +0 -0
  185. /package/web/dist/assets/{standalone-CjxtLKw-.js → standalone-D-fjaAIs.js} +0 -0
@@ -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. 按数组顺序读取 `~/.meddle/route-rules/<名称>.txt`。
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
@@ -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(args[++i])
41
+ options.headers = JSON.parse(raw)
27
42
  } catch (_) {
28
- options.headers = {}
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(result)
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({ success: true, id })
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 update 1 --status 404
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(result)
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, loadMockRules, saveMockRules } = require('../../lib/file-access')
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(args[++i])
44
+ options.headers = JSON.parse(raw)
30
45
  } catch (_) {
31
- options.headers = {}
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(result)
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(result)
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)
@@ -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
 
@@ -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
- // Find max id for sequence
87
- let maxId = 0
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: maxId
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
- // Get next id
107
- let nextId = 1
108
- try {
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.id = nextId
120
- rules.push(rule)
121
- saveMockRules(rules)
122
- return rule
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
- return files.map(name => {
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