@jonathanleelx/meddle 0.5.0-beta.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/CONFIG_STRUCTURE.md +9 -2
  2. package/README.md +18 -0
  3. package/bin/commands/mock/add.js +37 -21
  4. package/bin/commands/mock/delete.js +16 -24
  5. package/bin/commands/mock/index.js +8 -4
  6. package/bin/commands/mock/list.js +35 -18
  7. package/bin/commands/mock/toggle.js +17 -27
  8. package/bin/commands/mock/update.js +39 -30
  9. package/bin/commands/route/active.js +20 -3
  10. package/bin/commands/route/add.js +113 -94
  11. package/bin/commands/route/create.js +8 -1
  12. package/bin/commands/route/delete.js +85 -51
  13. package/bin/commands/route/index.js +20 -2
  14. package/bin/commands/route/list.js +2 -2
  15. package/bin/commands/route/show.js +10 -1
  16. package/bin/commands/route/update.js +135 -92
  17. package/bin/commands/update.js +4 -0
  18. package/bin/index.js +7 -3
  19. package/bin/lib/api-client.js +14 -2
  20. package/bin/lib/file-access.js +217 -35
  21. package/bin/lib/output.js +24 -3
  22. package/bin/lib/proxy-detect.js +99 -20
  23. package/bin/lib/proxy-or-file.js +90 -0
  24. package/bin/lib/route-write-args.js +150 -0
  25. package/bin/lib/rule-file-edit.js +263 -0
  26. package/bin/lib/ruleset-readonly.js +61 -0
  27. package/bin/lib/update-check.js +67 -6
  28. package/dist/cert.d.ts.map +1 -1
  29. package/dist/cert.js +43 -0
  30. package/dist/cert.js.map +1 -1
  31. package/dist/core/browser.d.ts +8 -1
  32. package/dist/core/browser.d.ts.map +1 -1
  33. package/dist/core/browser.js +239 -36
  34. package/dist/core/browser.js.map +1 -1
  35. package/dist/core/config-diagnostics.d.ts.map +1 -1
  36. package/dist/core/config-diagnostics.js +6 -1
  37. package/dist/core/config-diagnostics.js.map +1 -1
  38. package/dist/core/custom-plugin-loader.d.ts.map +1 -1
  39. package/dist/core/custom-plugin-loader.js +2 -0
  40. package/dist/core/custom-plugin-loader.js.map +1 -1
  41. package/dist/core/h2-pool.d.ts +1 -0
  42. package/dist/core/h2-pool.d.ts.map +1 -1
  43. package/dist/core/h2-pool.js +17 -1
  44. package/dist/core/h2-pool.js.map +1 -1
  45. package/dist/core/mock-handler.d.ts +59 -0
  46. package/dist/core/mock-handler.d.ts.map +1 -1
  47. package/dist/core/mock-handler.js +627 -29
  48. package/dist/core/mock-handler.js.map +1 -1
  49. package/dist/core/mock-utils.d.ts +58 -0
  50. package/dist/core/mock-utils.d.ts.map +1 -0
  51. package/dist/core/mock-utils.js +167 -0
  52. package/dist/core/mock-utils.js.map +1 -0
  53. package/dist/core/plugin-bootstrap-runner.d.ts +1 -0
  54. package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -1
  55. package/dist/core/plugin-bootstrap-runner.js +37 -1
  56. package/dist/core/plugin-bootstrap-runner.js.map +1 -1
  57. package/dist/core/plugin-source.d.ts +24 -0
  58. package/dist/core/plugin-source.d.ts.map +1 -0
  59. package/dist/core/plugin-source.js +47 -0
  60. package/dist/core/plugin-source.js.map +1 -0
  61. package/dist/core/rule-file-edit.d.ts +81 -0
  62. package/dist/core/rule-file-edit.d.ts.map +1 -0
  63. package/dist/core/rule-file-edit.js +243 -0
  64. package/dist/core/rule-file-edit.js.map +1 -0
  65. package/dist/core/static-server.d.ts.map +1 -1
  66. package/dist/core/static-server.js +15 -13
  67. package/dist/core/static-server.js.map +1 -1
  68. package/dist/core/types.d.ts +13 -1
  69. package/dist/core/types.d.ts.map +1 -1
  70. package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -1
  71. package/dist/plugins/builtin/mock-plugin.js +6 -5
  72. package/dist/plugins/builtin/mock-plugin.js.map +1 -1
  73. package/dist/server/agent/tools.d.ts.map +1 -1
  74. package/dist/server/agent/tools.js +24 -18
  75. package/dist/server/agent/tools.js.map +1 -1
  76. package/dist/server/app-info.d.ts +33 -0
  77. package/dist/server/app-info.d.ts.map +1 -0
  78. package/dist/server/app-info.js +116 -0
  79. package/dist/server/app-info.js.map +1 -0
  80. package/dist/server/config.d.ts +1 -0
  81. package/dist/server/config.d.ts.map +1 -1
  82. package/dist/server/config.js +101 -9
  83. package/dist/server/config.js.map +1 -1
  84. package/dist/server/health.d.ts.map +1 -1
  85. package/dist/server/health.js +8 -0
  86. package/dist/server/health.js.map +1 -1
  87. package/dist/server/index.d.ts +15 -0
  88. package/dist/server/index.d.ts.map +1 -1
  89. package/dist/server/index.js +3 -0
  90. package/dist/server/index.js.map +1 -1
  91. package/dist/server/mocks.d.ts +26 -0
  92. package/dist/server/mocks.d.ts.map +1 -1
  93. package/dist/server/mocks.js +110 -13
  94. package/dist/server/mocks.js.map +1 -1
  95. package/dist/server/plugins.d.ts.map +1 -1
  96. package/dist/server/plugins.js +56 -11
  97. package/dist/server/plugins.js.map +1 -1
  98. package/dist/server/rule-files.d.ts +36 -0
  99. package/dist/server/rule-files.d.ts.map +1 -1
  100. package/dist/server/rule-files.js +395 -107
  101. package/dist/server/rule-files.js.map +1 -1
  102. package/dist/server/ruleset-lock.d.ts +62 -0
  103. package/dist/server/ruleset-lock.d.ts.map +1 -0
  104. package/dist/server/ruleset-lock.js +234 -0
  105. package/dist/server/ruleset-lock.js.map +1 -0
  106. package/index.js +36 -7
  107. package/mcp-server.js +90 -88
  108. package/package.json +1 -1
  109. package/plugins/builtin/mock-plugin.ts +9 -5
  110. package/web/dist/assets/alert-B514Frte.js +1 -0
  111. package/web/dist/assets/arrow-right-BNum_060.js +1 -0
  112. package/web/dist/assets/{body-diff-view-B-4XWmL6.js → body-diff-view-SfLCLzs9.js} +2 -2
  113. package/web/dist/assets/button-BFNlGZ5p.js +1 -0
  114. package/web/dist/assets/check-BkfHQ6re.js +1 -0
  115. package/web/dist/assets/checkbox-CQyFsZzS.js +1 -0
  116. package/web/dist/assets/chevron-down-DHwUtL4O.js +1 -0
  117. package/web/dist/assets/{circle-alert-Q8cZk9Rv.js → circle-alert-zMagrFuI.js} +1 -1
  118. package/web/dist/assets/{circle-x-C9USji_R.js → circle-x-BEkinhez.js} +1 -1
  119. package/web/dist/assets/code-DD-AYxxu.js +1 -0
  120. package/web/dist/assets/{copy-BYjq1hdN.js → copy-BRnscYy1.js} +1 -1
  121. package/web/dist/assets/detail-panel-BtgWWeu3.js +4 -0
  122. package/web/dist/assets/dist-BQN0rzUS.js +1 -0
  123. package/web/dist/assets/{dist-CEd_wnRC.js → dist-C-E2R2jk.js} +1 -1
  124. package/web/dist/assets/{dist-C6eaYCUo.js → dist-CFZ4xMSF.js} +1 -1
  125. package/web/dist/assets/{dist-DWmDaXVN.js → dist-CRmX_NvH.js} +1 -1
  126. package/web/dist/assets/{dist-C8RHq5At.js → dist-DLQKRjuW.js} +1 -1
  127. package/web/dist/assets/{dist-Blyulj4_.js → dist-LiAcI8CK.js} +1 -1
  128. package/web/dist/assets/{dist-Ce4ujolc.js → dist-fzPL-8F9.js} +1 -1
  129. package/web/dist/assets/{dist-DoE7uybN.js → dist-wv9ebJK_.js} +1 -1
  130. package/web/dist/assets/editor-pane-actions-_ckwcCII.js +1 -0
  131. package/web/dist/assets/{es2015-DG46NGN7.js → es2015-DPJTBYa9.js} +1 -1
  132. package/web/dist/assets/{global-panel-shell-5cJneeis.js → global-panel-shell-NaUhzX7J.js} +3 -3
  133. package/web/dist/assets/health-panel-DhXLqcOU.js +1 -0
  134. package/web/dist/assets/index-B-6nUrqW.js +21 -0
  135. package/web/dist/assets/index-EWNlAdFB.css +2 -0
  136. package/web/dist/assets/mobile-proxy-panel-BY8Oi-qC.js +8 -0
  137. package/web/dist/assets/mock-config-TPmfJwAt.js +1 -0
  138. package/web/dist/assets/mock-editor-panel-WQwcHSTD.js +20 -0
  139. package/web/dist/assets/mocks-file-missing-note-BR5oWFLh.js +1 -0
  140. package/web/dist/assets/monaco-editor-BGLPrb0C.js +11 -0
  141. package/web/dist/assets/pane-bar-gFRISZLD.js +1 -0
  142. package/web/dist/assets/pane-split-B6aLmY7G.js +1 -0
  143. package/web/dist/assets/panel-heading-DT_nXayM.js +1 -0
  144. package/web/dist/assets/{pencil-line-Cqe41E4h.js → pencil-line-CEZ7D-de.js} +1 -1
  145. package/web/dist/assets/plugin-code-editor--Gf4ZwGb.js +2 -0
  146. package/web/dist/assets/plugin-config-DsX9xCW_.js +2 -0
  147. package/web/dist/assets/plugin-generator-Bj4f6dxo.js +2 -0
  148. package/web/dist/assets/plugin-test-dialog-DICuykBK.js +7 -0
  149. package/web/dist/assets/plus-ixZQRprm.js +1 -0
  150. package/web/dist/assets/{refresh-cw-CG3HaiQT.js → refresh-cw-BfLoHqg5.js} +1 -1
  151. package/web/dist/assets/rotate-ccw-CRguJya4.js +1 -0
  152. package/web/dist/assets/route-preview-C-LzS1lT.js +1 -0
  153. package/web/dist/assets/{rule-ai-assistant-panel-DMh0F9dr.js → rule-ai-assistant-panel-BEfPNBbx.js} +2 -2
  154. package/web/dist/assets/rule-config-Dw9pcuah.js +15 -0
  155. package/web/dist/assets/rule-overview-panel-C4QQ7U3S.js +1 -0
  156. package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
  157. package/web/dist/assets/settings-panel-D8DEH8aH.js +1 -0
  158. package/web/dist/assets/{shield-check-OqUmVLQt.js → shield-check-hQXjdfZ7.js} +1 -1
  159. package/web/dist/assets/smartphone-D4Q7cQfm.js +1 -0
  160. package/web/dist/assets/switch-94vgsMHm.js +1 -0
  161. package/web/dist/assets/{syntax-highlight-Dk0-ewLI.js → syntax-highlight-BFgN_C0Y.js} +1 -1
  162. package/web/dist/assets/{table-2-BR5e3z8m.js → table-2-DvGHhMm4.js} +1 -1
  163. package/web/dist/assets/table-DaXD6jJv.js +1 -0
  164. package/web/dist/assets/textarea-9wB8Hsz-.js +1 -0
  165. package/web/dist/assets/toast-9yeTf7d9.js +1 -0
  166. package/web/dist/assets/use-mocks-file-status-BUIUJ4oQ.js +1 -0
  167. package/web/dist/assets/{wand-sparkles-CJ7fSnom.js → wand-sparkles-Bmt4dDOf.js} +1 -1
  168. package/web/dist/assets/{wifi-CCLKyySQ.js → wifi-Cjj_Br1u.js} +1 -1
  169. package/web/dist/assets/{zap-DR-xiUMe.js → zap-BuOtogAv.js} +1 -1
  170. package/web/dist/index.html +16 -10
  171. package/web/dist/assets/alert-DCvlZAiv.js +0 -1
  172. package/web/dist/assets/arrow-right-B4Mrlj2-.js +0 -1
  173. package/web/dist/assets/button-BTToMaBW.js +0 -1
  174. package/web/dist/assets/checkbox-D1Z9DtjF.js +0 -1
  175. package/web/dist/assets/chevron-down-CPngAx4M.js +0 -1
  176. package/web/dist/assets/chevron-right-V1DGIDbL.js +0 -1
  177. package/web/dist/assets/code-5_eJ_XpJ.js +0 -1
  178. package/web/dist/assets/code-fixer-D48sCDtp.js +0 -20
  179. package/web/dist/assets/detail-panel-BSFz9CUw.js +0 -4
  180. package/web/dist/assets/file-system-access-_og-Tg9o.js +0 -1
  181. package/web/dist/assets/health-panel-BS3L2e5d.js +0 -1
  182. package/web/dist/assets/index-CvTR1IVi.js +0 -18
  183. package/web/dist/assets/index-bfV7sxx5.css +0 -2
  184. package/web/dist/assets/mobile-proxy-panel-tU0NyJ3P.js +0 -8
  185. package/web/dist/assets/mock-config-CULJP6Vt.js +0 -1
  186. package/web/dist/assets/mock-editor-panel-9wlDaySA.js +0 -1
  187. package/web/dist/assets/monaco-editor-BttaUoOz.js +0 -11
  188. package/web/dist/assets/plugin-code-editor-BAIBp4lg.js +0 -2
  189. package/web/dist/assets/plugin-config-ButUV2SZ.js +0 -1
  190. package/web/dist/assets/plugin-generator-uceUPSCv.js +0 -2
  191. package/web/dist/assets/plugin-test-dialog-BrJ4HfRX.js +0 -7
  192. package/web/dist/assets/plus-tVxSydJ8.js +0 -1
  193. package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +0 -1
  194. package/web/dist/assets/route-preview-C4cGrS1-.js +0 -1
  195. package/web/dist/assets/rule-config-BTTGNwMs.js +0 -15
  196. package/web/dist/assets/rule-overview-panel-SFR2OloL.js +0 -1
  197. package/web/dist/assets/select-aBXQ0zNa.js +0 -1
  198. package/web/dist/assets/settings-panel-BMReDG8b.js +0 -1
  199. package/web/dist/assets/smartphone-DKeDuJTU.js +0 -1
  200. package/web/dist/assets/switch-Bes_YiJu.js +0 -1
  201. package/web/dist/assets/table-nW7uQVcl.js +0 -1
  202. package/web/dist/assets/textarea-BSI0rnW6.js +0 -1
  203. /package/web/dist/assets/{babel-DCr4VA1c.js → babel-bwZm0qi3.js} +0 -0
  204. /package/web/dist/assets/{clipboard-VhPXNN5m.js → clipboard-Bt1qY01V.js} +0 -0
  205. /package/web/dist/assets/{estree-qO_m2Qaw.js → estree-MatbZDwq.js} +0 -0
  206. /package/web/dist/assets/{html-VDGahtyk.js → html-CCrsbFYX.js} +0 -0
  207. /package/web/dist/assets/{postcss-BUm8EKMw.js → postcss-BxN7Gr8n.js} +0 -0
  208. /package/web/dist/assets/{standalone-Bj8SSd8Q.js → standalone-DsnFnsqM.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,8 @@ 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 的完整显示顺序(含禁用文件);启用文件的合并顺序与其在该数组中的相对顺序一致 |
207
+ | `readonlyRuleFiles` | 只读(加锁)的规则文件名称。可选,缺省即全部可读写。只能在 Web 界面中修改;锁住的规则文件拒绝 CLI / MCP / API 的一切写入。见 [只读规则集](./docs/RULESET_READONLY.md) |
201
208
  | `mocksFilePath` | 自定义 Mock JSON 路径;空值使用默认位置 |
202
209
  | `pluginMode` | 插件 Pipeline 模式:`off`、`shadow` 或 `on` |
203
210
  | `clientAliases` | 客户端 IP 到设备名称的映射 |
@@ -223,8 +230,8 @@ https://localhost:13001/micro/app?a=1
223
230
 
224
231
  ### 路由规则
225
232
 
226
- 1. 读取 `~/.meddle/settings.json` 中的 `activeRuleFiles`。
227
- 2. 按数组顺序读取 `~/.meddle/route-rules/<名称>.txt`。
233
+ 1. 读取 `~/.meddle/settings.json` 中的 `activeRuleFiles`(及可选的 `ruleFileOrder`)。
234
+ 2. 按 `activeRuleFiles` 数组顺序读取 `~/.meddle/route-rules/<名称>.txt`。启用文件的相对顺序与 Tab 栏(`ruleFileOrder`)中启用文件的相对顺序保持一致。
228
235
  3. 合并规则并按顺序匹配。
229
236
  4. 如果目录为空,首次启动会创建一个默认规则文件并将其设为启用状态。
230
237
  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
@@ -179,6 +181,22 @@ meddle --remote --no-intercept-https
179
181
 
180
182
  所有 `mock_rule_*`、`route_rule_*`、`route_preview` 工具均支持可选 `session` 参数,指定后可操作对应 session 的规则,不传则操作默认 session。
181
183
 
184
+ **路由规则写入**(`route_rule_add` / `route_rule_update` / `route_rule_delete`)只改包含该 pattern 的那一行,规则文件里的注释、空行、`//` 禁用规则和其他规则(连同排除项)都按原样保留:
185
+
186
+ - **不传 `exclusions`**:保留该规则原有的排除规则(`route_rule_add` 覆盖已有规则时也一样;新规则则没有排除规则)。
187
+ - **传 `exclusions: []`**:清空该规则的排除规则。
188
+ - 传非空数组:用它替换原有的排除规则。
189
+ - 如果一行写了多个 pattern,修改或删除其中一个时,若剩下的 pattern 以 `//` 或 `#` 开头(拆开后会被当成注释),写入会被拒绝并提示「该行剩余的 pattern 以 // 或 # 开头,拆开后会被当成注释,请用文本编辑器手动修改」,规则文件不变。
190
+ - 新增规则的 pattern 不能以 `#` 或 `//` 开头(例如 `#foo.com`、`//foo.com`),否则写进文件后会被当成注释或禁用行。写入会被拒绝并提示「规则不能以 // 或 # 开头,否则会被当成禁用行或注释。要写注释或禁用规则,请用文本编辑器。」,规则文件不变。
191
+ - CLI:`meddle route update <file> <pattern> --exclusions=` 清空该规则的排除项(该行不再有 `!path`,其余行逐字节不变)。`--exclusions=/a,/b` 替换。pattern 里写 `!path`(如 `"api.example.com !/health"`)同样是替换。两者都不写则保留原有排除项。单独的 `--exclusions`(没有 `=`)、空的 `!`、未知参数,或 `!path` 与 `--exclusions` 同时出现,都会非 0 退出且不改文件。`#` 只有出现在一行开头(前导空白之后)才是整行注释;行尾的 `#` 不是注释。
192
+
193
+ **只读规则集**([详细说明](./docs/RULESET_READONLY.md)):规则集(规则文件)可以在 Meddle Web 界面中加只读锁,防止 Agent 误改。
194
+
195
+ - `route_rule_list` 的结果带 `readonly: true|false`。
196
+ - 锁住后 `route_rule_add` / `route_rule_update` / `route_rule_delete` / `route_rule_active_set`(会改变锁住规则集的启用状态时)全部被拒绝,规则文件不变,工具返回 `isError: true` 与 `{"error":{"code":"RULESET_READONLY","message":"该规则集已设为只读,只能在 Meddle 界面中解锁","ruleFile":"…"}}`。CLI 同样拒绝,退出码为 `3`。
197
+ - **CLI 和 MCP 都没有加锁 / 解锁的命令或工具**,只能由人在 Meddle 界面中操作。界面使用页面令牌 + 同源校验;这只是防误改,不是安全隔离(可被 `curl` 取页面令牌并伪造 `Origin` 绕过,有 shell 的 Agent 也能直接改磁盘文件)。
198
+ - 锁住的规则集不能重命名,需先在 Meddle 界面中解锁再改名;在 Meddle 之外直接改文件名视为新规则集(可读写),旧锁记录自动清除。
199
+
182
200
  ### 多 Session 隔离(预览功能)
183
201
 
184
202
  多 Session 允许同时运行多个独立的 `meddle` 代理进程,每个进程拥有独立的配置目录(MEDDLE_HOME)、端口、路由规则和 Mock 规则,适用于多个 Agent 或项目需要隔离代理环境的场景。
@@ -3,32 +3,49 @@
3
3
  */
4
4
 
5
5
  const output = require('../../lib/output')
6
- const { isProxyRunning } = require('../../lib/proxy-detect')
7
6
  const { apiPost } = require('../../lib/api-client')
8
7
  const { addMockRule } = require('../../lib/file-access')
8
+ const { withProxyOrFile, failCommand } = require('../../lib/proxy-or-file')
9
9
 
10
10
  // Parse arguments
11
11
  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,
@@ -58,35 +76,33 @@ async function run() {
58
76
  enabled: true
59
77
  }
60
78
 
61
- const running = await isProxyRunning()
79
+ const { value: result, fileOnly } = await withProxyOrFile({
80
+ api: async () => {
81
+ const created = await apiPost('/api/mocks', rule)
82
+ return created.rule || created
83
+ },
84
+ file: () => addMockRule(rule),
85
+ })
62
86
 
63
- let result
64
- if (running) {
65
- try {
66
- result = await apiPost('/api/mocks', rule)
67
- result = result.rule || result
68
- } catch (e) {
69
- // Fallback to file mode
70
- result = addMockRule(rule)
71
- }
72
- } else {
73
- result = addMockRule(rule)
74
- }
87
+ if (fileOnly) output.warnFileOnlyMode()
75
88
 
76
89
  if (jsonFlag) {
77
- output.jsonRaw(result)
90
+ output.jsonRaw(fileOnly
91
+ ? { ...result, fileOnly: true, warning: output.FILE_ONLY_WARNING }
92
+ : result)
78
93
  return
79
94
  }
80
95
 
81
96
  output.header('Mock Rule Added')
82
97
  output.success(`#${result.id}: ${result.name}`)
83
98
  output.kv('Pattern', result.urlPattern)
99
+ if (result.query) output.kv('Query', result.query)
84
100
  output.kv('Method', result.method)
85
101
  output.kv('Status', result.statusCode)
86
102
  output.kv('Delay', `${result.delay}ms`)
103
+ if (result.headers && Object.keys(result.headers).length > 0) {
104
+ output.kv('Headers', JSON.stringify(result.headers))
105
+ }
87
106
  }
88
107
 
89
- run().catch(e => {
90
- output.error(e.message)
91
- process.exit(1)
92
- })
108
+ run().catch((e) => failCommand(e))
@@ -3,9 +3,9 @@
3
3
  */
4
4
 
5
5
  const output = require('../../lib/output')
6
- const { isProxyRunning } = require('../../lib/proxy-detect')
7
6
  const { apiDelete } = require('../../lib/api-client')
8
7
  const { deleteMockRule } = require('../../lib/file-access')
8
+ const { withProxyOrFile, failCommand } = require('../../lib/proxy-or-file')
9
9
 
10
10
  // Parse arguments (skip 'mock' and 'delete' from argv)
11
11
  const args = process.argv.slice(4)
@@ -22,29 +22,24 @@ async function run() {
22
22
  process.exit(1)
23
23
  }
24
24
 
25
- const running = await isProxyRunning()
26
-
27
- if (running) {
28
- try {
25
+ const { fileOnly } = await withProxyOrFile({
26
+ api: async () => {
29
27
  await apiDelete(`/api/mocks/${id}`)
30
- } catch (e) {
31
- // Fallback to file mode
28
+ return true
29
+ },
30
+ file: () => {
32
31
  const deleted = deleteMockRule(id)
33
- if (!deleted) {
34
- output.error(`Mock rule not found: id=${id}`)
35
- process.exit(1)
36
- }
37
- }
38
- } else {
39
- const deleted = deleteMockRule(id)
40
- if (!deleted) {
41
- output.error(`Mock rule not found: id=${id}`)
42
- process.exit(1)
43
- }
44
- }
32
+ if (!deleted) throw new Error(`Mock rule not found: id=${id}`)
33
+ return true
34
+ },
35
+ })
36
+
37
+ if (fileOnly) output.warnFileOnlyMode()
45
38
 
46
39
  if (jsonFlag) {
47
- output.jsonRaw({ success: true, id })
40
+ output.jsonRaw(fileOnly
41
+ ? { success: true, id, fileOnly: true, warning: output.FILE_ONLY_WARNING }
42
+ : { success: true, id })
48
43
  return
49
44
  }
50
45
 
@@ -52,7 +47,4 @@ async function run() {
52
47
  output.success(`id=${id}`)
53
48
  }
54
49
 
55
- run().catch(e => {
56
- output.error(e.message)
57
- process.exit(1)
58
- })
50
+ run().catch((e) => failCommand(e))
@@ -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
+ }
@@ -3,30 +3,49 @@
3
3
  */
4
4
 
5
5
  const output = require('../../lib/output')
6
- const { isProxyRunning } = require('../../lib/proxy-detect')
7
6
  const { apiGet } = require('../../lib/api-client')
8
7
  const { loadMockRules } = require('../../lib/file-access')
8
+ const { withProxyOrFile, failCommand } = require('../../lib/proxy-or-file')
9
9
 
10
10
  // Check for --json flag
11
11
  const args = process.argv.slice(3)
12
12
  const jsonFlag = args.includes('--json')
13
13
  output.setJsonMode(jsonFlag)
14
14
 
15
- async function run() {
16
- const running = await isProxyRunning()
17
-
18
- let rules = []
19
-
20
- if (running) {
21
- try {
22
- rules = await apiGet('/api/mocks')
23
- } catch (e) {
24
- // Fallback to file mode
25
- rules = loadMockRules()
15
+ function listFromFile() {
16
+ try {
17
+ return loadMockRules()
18
+ } catch (err) {
19
+ if (err && err.code === 'MOCKS_FILE_INVALID' && err.listMessage) {
20
+ const listed = new Error(err.listMessage)
21
+ listed.rawStderr = true
22
+ throw listed
26
23
  }
27
- } else {
28
- rules = loadMockRules()
24
+ throw err
29
25
  }
26
+ }
27
+
28
+ async function run() {
29
+ const { value: rules } = await withProxyOrFile({
30
+ api: async () => {
31
+ const listed = await apiGet('/api/mocks')
32
+ // GET /api/mocks stays a bare array and is [] when the file failed to
33
+ // parse. The status endpoint is the signal that the file is corrupt.
34
+ let fileStatus = null
35
+ try {
36
+ fileStatus = await apiGet('/api/mocks-file')
37
+ } catch (err) {
38
+ if (!err || !Number.isInteger(err.status)) throw err
39
+ }
40
+ if (fileStatus && fileStatus.valid === false) {
41
+ const err = new Error(fileStatus.error || 'Mock 文件格式错误')
42
+ err.rawStderr = true
43
+ throw err
44
+ }
45
+ return listed
46
+ },
47
+ file: listFromFile,
48
+ })
30
49
 
31
50
  if (jsonFlag) {
32
51
  output.jsonRaw(rules)
@@ -48,6 +67,7 @@ async function run() {
48
67
  output.section('Active Rules')
49
68
  active.forEach(rule => {
50
69
  output.success(`#${rule.id}: ${rule.name} (${rule.urlPattern})`)
70
+ if (rule.query) output.kv('Query', rule.query, 4)
51
71
  output.kv('Status', rule.statusCode, 4)
52
72
  output.kv('Method', rule.method || '*', 4)
53
73
  output.kv('Delay', `${rule.delay || 0}ms`, 4)
@@ -62,7 +82,4 @@ async function run() {
62
82
  }
63
83
  }
64
84
 
65
- run().catch(e => {
66
- output.error(e.message)
67
- process.exit(1)
68
- })
85
+ run().catch((e) => failCommand(e))
@@ -3,9 +3,9 @@
3
3
  */
4
4
 
5
5
  const output = require('../../lib/output')
6
- const { isProxyRunning } = require('../../lib/proxy-detect')
7
6
  const { apiPut } = require('../../lib/api-client')
8
7
  const { updateMockRule } = require('../../lib/file-access')
8
+ const { withProxyOrFile, failCommand } = require('../../lib/proxy-or-file')
9
9
 
10
10
  // Parse arguments
11
11
  const args = process.argv.slice(2) // Full args: ['mock', 'enable'/'disable', '<id>']
@@ -25,31 +25,24 @@ async function run() {
25
25
 
26
26
  const enabled = subcommand === 'enable'
27
27
 
28
- const running = await isProxyRunning()
28
+ const { value: result, fileOnly } = await withProxyOrFile({
29
+ api: async () => {
30
+ const updated = await apiPut(`/api/mocks/${id}`, { enabled })
31
+ return updated.rule || updated
32
+ },
33
+ file: () => {
34
+ const updated = updateMockRule(id, { enabled })
35
+ if (!updated) throw new Error(`Mock rule not found: id=${id}`)
36
+ return updated
37
+ },
38
+ })
29
39
 
30
- let result
31
- if (running) {
32
- try {
33
- result = await apiPut(`/api/mocks/${id}`, { enabled })
34
- result = result.rule || result
35
- } catch (e) {
36
- // Fallback to file mode
37
- result = updateMockRule(id, { enabled })
38
- if (!result) {
39
- output.error(`Mock rule not found: id=${id}`)
40
- process.exit(1)
41
- }
42
- }
43
- } else {
44
- result = updateMockRule(id, { enabled })
45
- if (!result) {
46
- output.error(`Mock rule not found: id=${id}`)
47
- process.exit(1)
48
- }
49
- }
40
+ if (fileOnly) output.warnFileOnlyMode()
50
41
 
51
42
  if (jsonFlag) {
52
- output.jsonRaw(result)
43
+ output.jsonRaw(fileOnly
44
+ ? { ...result, fileOnly: true, warning: output.FILE_ONLY_WARNING }
45
+ : result)
53
46
  return
54
47
  }
55
48
 
@@ -58,7 +51,4 @@ async function run() {
58
51
  output.kv('Enabled', enabled ? 'Yes' : 'No')
59
52
  }
60
53
 
61
- run().catch(e => {
62
- output.error(e.message)
63
- process.exit(1)
64
- })
54
+ run().catch((e) => failCommand(e))
@@ -3,15 +3,27 @@
3
3
  */
4
4
 
5
5
  const output = require('../../lib/output')
6
- const { isProxyRunning } = require('../../lib/proxy-detect')
7
6
  const { apiPut } = require('../../lib/api-client')
8
- const { updateMockRule, loadMockRules, saveMockRules } = require('../../lib/file-access')
7
+ const { updateMockRule } = require('../../lib/file-access')
8
+ const { withProxyOrFile, failCommand } = require('../../lib/proxy-or-file')
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
 
@@ -52,43 +69,35 @@ async function run() {
52
69
  process.exit(1)
53
70
  }
54
71
 
55
- const running = await isProxyRunning()
72
+ const { value: result, fileOnly } = await withProxyOrFile({
73
+ api: async () => {
74
+ const updated = await apiPut(`/api/mocks/${id}`, options)
75
+ return updated.rule || updated
76
+ },
77
+ file: () => {
78
+ const updated = updateMockRule(id, options)
79
+ if (!updated) throw new Error(`Mock rule not found: id=${id}`)
80
+ return updated
81
+ },
82
+ })
56
83
 
57
- let result
58
- if (running) {
59
- try {
60
- result = await apiPut(`/api/mocks/${id}`, options)
61
- result = result.rule || result
62
- } catch (e) {
63
- // Fallback to file mode
64
- result = updateMockRule(id, options)
65
- if (!result) {
66
- output.error(`Mock rule not found: id=${id}`)
67
- process.exit(1)
68
- }
69
- }
70
- } else {
71
- result = updateMockRule(id, options)
72
- if (!result) {
73
- output.error(`Mock rule not found: id=${id}`)
74
- process.exit(1)
75
- }
76
- }
84
+ if (fileOnly) output.warnFileOnlyMode()
77
85
 
78
86
  if (jsonFlag) {
79
- output.jsonRaw(result)
87
+ output.jsonRaw(fileOnly
88
+ ? { ...result, fileOnly: true, warning: output.FILE_ONLY_WARNING }
89
+ : result)
80
90
  return
81
91
  }
82
92
 
83
93
  output.header('Mock Rule Updated')
84
94
  output.success(`#${result.id}: ${result.name}`)
85
95
  if (options.urlPattern) output.kv('Pattern', result.urlPattern)
96
+ if (options.query !== undefined) output.kv('Query', result.query || '(none)')
86
97
  if (options.method) output.kv('Method', result.method)
87
98
  if (options.statusCode) output.kv('Status', result.statusCode)
88
99
  if (options.delay) output.kv('Delay', `${result.delay}ms`)
100
+ if (options.headers) output.kv('Headers', JSON.stringify(result.headers || {}))
89
101
  }
90
102
 
91
- run().catch(e => {
92
- output.error(e.message)
93
- process.exit(1)
94
- })
103
+ run().catch((e) => failCommand(e))
@@ -5,7 +5,13 @@
5
5
  const output = require('../../lib/output')
6
6
  const { isProxyRunning } = require('../../lib/proxy-detect')
7
7
  const { apiGet, apiPut } = require('../../lib/api-client')
8
- const { listRuleFiles, getActiveRuleFileNames, setActiveRuleFileNames, setRuleFileEnabled } = require('../../lib/file-access')
8
+ const { listRuleFiles, getActiveRuleFileNames, setRuleFileEnabled } = require('../../lib/file-access')
9
+ const { isRulesetReadonlyError, exitRulesetReadonly, assertListedFilesWritable } = require('../../lib/ruleset-readonly')
10
+
11
+ /** Names whose enabled state `active set <file>` would change. */
12
+ function pendingToggles(files, target) {
13
+ return files.filter((f) => !!f.enabled !== (f.name === target)).map((f) => f.name)
14
+ }
9
15
 
10
16
  // Parse arguments (skip 'route' and 'active' from argv)
11
17
  const args = process.argv.slice(4)
@@ -39,6 +45,9 @@ async function run() {
39
45
  process.exit(1)
40
46
  }
41
47
 
48
+ // Issue #137: refuse up front so a locked ruleset never leaves a half-applied switch.
49
+ assertListedFilesWritable(files, pendingToggles(files, targetFile))
50
+
42
51
  for (const f of files) {
43
52
  const nextEnabled = f.name === targetFile
44
53
  if (!!f.enabled === nextEnabled) continue
@@ -53,6 +62,7 @@ async function run() {
53
62
  output.header('Active Route File Set')
54
63
  output.success(targetFile)
55
64
  } catch (e) {
65
+ if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: targetFile })
56
66
  // Fallback to file mode
57
67
  const files = listRuleFiles()
58
68
  const target = files.find(f => f.name === targetFile)
@@ -61,11 +71,14 @@ async function run() {
61
71
  process.exit(1)
62
72
  }
63
73
 
74
+ assertListedFilesWritable(files, pendingToggles(files, targetFile))
64
75
  // Disable all, then enable target
65
76
  files.forEach(f => setRuleFileEnabled(f.name, f.name === targetFile))
66
77
 
78
+ output.warnFileOnlyMode()
79
+
67
80
  if (jsonFlag) {
68
- output.jsonRaw({ success: true, activeFile: targetFile })
81
+ output.jsonRaw({ success: true, activeFile: targetFile, fileOnly: true, warning: output.FILE_ONLY_WARNING })
69
82
  return
70
83
  }
71
84
 
@@ -81,10 +94,13 @@ async function run() {
81
94
  process.exit(1)
82
95
  }
83
96
 
97
+ assertListedFilesWritable(files, pendingToggles(files, targetFile))
84
98
  files.forEach(f => setRuleFileEnabled(f.name, f.name === targetFile))
85
99
 
100
+ output.warnFileOnlyMode()
101
+
86
102
  if (jsonFlag) {
87
- output.jsonRaw({ success: true, activeFile: targetFile })
103
+ output.jsonRaw({ success: true, activeFile: targetFile, fileOnly: true, warning: output.FILE_ONLY_WARNING })
88
104
  return
89
105
  }
90
106
 
@@ -136,6 +152,7 @@ async function run() {
136
152
  }
137
153
 
138
154
  run().catch(e => {
155
+ if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: targetFile })
139
156
  output.error(e.message)
140
157
  process.exit(1)
141
158
  })