@jonathanleelx/meddle 0.5.0-beta.9 → 0.5.1

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 (217) hide show
  1. package/CONFIG_STRUCTURE.md +9 -2
  2. package/README.md +18 -0
  3. package/bin/commands/mock/add.js +59 -21
  4. package/bin/commands/mock/delete.js +16 -24
  5. package/bin/commands/mock/index.js +59 -25
  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 +87 -53
  13. package/bin/commands/route/index.js +74 -26
  14. package/bin/commands/route/list.js +2 -2
  15. package/bin/commands/route/preview.js +2 -2
  16. package/bin/commands/route/show.js +12 -3
  17. package/bin/commands/route/update.js +135 -92
  18. package/bin/commands/status.js +27 -11
  19. package/bin/commands/update.js +4 -0
  20. package/bin/doctor.js +8 -4
  21. package/bin/index.js +11 -7
  22. package/bin/lib/api-client.js +14 -2
  23. package/bin/lib/file-access.js +234 -35
  24. package/bin/lib/meddle-home.js +14 -1
  25. package/bin/lib/output.js +30 -4
  26. package/bin/lib/proxy-detect.js +99 -20
  27. package/bin/lib/proxy-or-file.js +93 -0
  28. package/bin/lib/route-write-args.js +150 -0
  29. package/bin/lib/rule-file-edit.js +263 -0
  30. package/bin/lib/ruleset-readonly.js +61 -0
  31. package/bin/lib/update-check.js +67 -6
  32. package/dist/cert.d.ts +2 -0
  33. package/dist/cert.d.ts.map +1 -1
  34. package/dist/cert.js +53 -1
  35. package/dist/cert.js.map +1 -1
  36. package/dist/core/browser.d.ts +10 -1
  37. package/dist/core/browser.d.ts.map +1 -1
  38. package/dist/core/browser.js +252 -36
  39. package/dist/core/browser.js.map +1 -1
  40. package/dist/core/config-diagnostics.d.ts.map +1 -1
  41. package/dist/core/config-diagnostics.js +6 -1
  42. package/dist/core/config-diagnostics.js.map +1 -1
  43. package/dist/core/custom-plugin-loader.d.ts.map +1 -1
  44. package/dist/core/custom-plugin-loader.js +2 -0
  45. package/dist/core/custom-plugin-loader.js.map +1 -1
  46. package/dist/core/mock-handler.d.ts +63 -0
  47. package/dist/core/mock-handler.d.ts.map +1 -1
  48. package/dist/core/mock-handler.js +641 -29
  49. package/dist/core/mock-handler.js.map +1 -1
  50. package/dist/core/mock-utils.d.ts +58 -0
  51. package/dist/core/mock-utils.d.ts.map +1 -0
  52. package/dist/core/mock-utils.js +167 -0
  53. package/dist/core/mock-utils.js.map +1 -0
  54. package/dist/core/plugin-bootstrap-runner.d.ts +1 -0
  55. package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -1
  56. package/dist/core/plugin-bootstrap-runner.js +37 -1
  57. package/dist/core/plugin-bootstrap-runner.js.map +1 -1
  58. package/dist/core/plugin-source.d.ts +24 -0
  59. package/dist/core/plugin-source.d.ts.map +1 -0
  60. package/dist/core/plugin-source.js +47 -0
  61. package/dist/core/plugin-source.js.map +1 -0
  62. package/dist/core/proxy-record.d.ts +14 -0
  63. package/dist/core/proxy-record.d.ts.map +1 -1
  64. package/dist/core/proxy-record.js +39 -0
  65. package/dist/core/proxy-record.js.map +1 -1
  66. package/dist/core/rule-file-edit.d.ts +81 -0
  67. package/dist/core/rule-file-edit.d.ts.map +1 -0
  68. package/dist/core/rule-file-edit.js +243 -0
  69. package/dist/core/rule-file-edit.js.map +1 -0
  70. package/dist/core/static-server.d.ts.map +1 -1
  71. package/dist/core/static-server.js +15 -13
  72. package/dist/core/static-server.js.map +1 -1
  73. package/dist/core/types.d.ts +15 -1
  74. package/dist/core/types.d.ts.map +1 -1
  75. package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -1
  76. package/dist/plugins/builtin/mock-plugin.js +6 -5
  77. package/dist/plugins/builtin/mock-plugin.js.map +1 -1
  78. package/dist/server/agent/tools.d.ts.map +1 -1
  79. package/dist/server/agent/tools.js +24 -18
  80. package/dist/server/agent/tools.js.map +1 -1
  81. package/dist/server/app-info.d.ts +33 -0
  82. package/dist/server/app-info.d.ts.map +1 -0
  83. package/dist/server/app-info.js +116 -0
  84. package/dist/server/app-info.js.map +1 -0
  85. package/dist/server/config.d.ts +1 -0
  86. package/dist/server/config.d.ts.map +1 -1
  87. package/dist/server/config.js +114 -16
  88. package/dist/server/config.js.map +1 -1
  89. package/dist/server/health.d.ts.map +1 -1
  90. package/dist/server/health.js +8 -0
  91. package/dist/server/health.js.map +1 -1
  92. package/dist/server/index.d.ts +16 -0
  93. package/dist/server/index.d.ts.map +1 -1
  94. package/dist/server/index.js +10 -0
  95. package/dist/server/index.js.map +1 -1
  96. package/dist/server/json-parse-error.d.ts +7 -0
  97. package/dist/server/json-parse-error.d.ts.map +1 -0
  98. package/dist/server/json-parse-error.js +28 -0
  99. package/dist/server/json-parse-error.js.map +1 -0
  100. package/dist/server/mocks.d.ts +26 -0
  101. package/dist/server/mocks.d.ts.map +1 -1
  102. package/dist/server/mocks.js +110 -13
  103. package/dist/server/mocks.js.map +1 -1
  104. package/dist/server/plugins.d.ts.map +1 -1
  105. package/dist/server/plugins.js +56 -11
  106. package/dist/server/plugins.js.map +1 -1
  107. package/dist/server/rule-files.d.ts +36 -0
  108. package/dist/server/rule-files.d.ts.map +1 -1
  109. package/dist/server/rule-files.js +388 -107
  110. package/dist/server/rule-files.js.map +1 -1
  111. package/dist/server/ruleset-lock.d.ts +62 -0
  112. package/dist/server/ruleset-lock.d.ts.map +1 -0
  113. package/dist/server/ruleset-lock.js +234 -0
  114. package/dist/server/ruleset-lock.js.map +1 -0
  115. package/index.js +128 -16
  116. package/mcp-server.js +108 -93
  117. package/package.json +1 -1
  118. package/plugins/builtin/mock-plugin.ts +9 -5
  119. package/web/dist/assets/alert-B514Frte.js +1 -0
  120. package/web/dist/assets/arrow-right-BNum_060.js +1 -0
  121. package/web/dist/assets/{body-diff-view-B-4XWmL6.js → body-diff-view-SfLCLzs9.js} +2 -2
  122. package/web/dist/assets/button-BFNlGZ5p.js +1 -0
  123. package/web/dist/assets/check-BkfHQ6re.js +1 -0
  124. package/web/dist/assets/checkbox-CQyFsZzS.js +1 -0
  125. package/web/dist/assets/chevron-down-DHwUtL4O.js +1 -0
  126. package/web/dist/assets/{circle-alert-Q8cZk9Rv.js → circle-alert-zMagrFuI.js} +1 -1
  127. package/web/dist/assets/{circle-x-C9USji_R.js → circle-x-BEkinhez.js} +1 -1
  128. package/web/dist/assets/code-DD-AYxxu.js +1 -0
  129. package/web/dist/assets/{copy-BYjq1hdN.js → copy-BRnscYy1.js} +1 -1
  130. package/web/dist/assets/detail-panel-DZRKJG2W.js +4 -0
  131. package/web/dist/assets/dist-BQN0rzUS.js +1 -0
  132. package/web/dist/assets/{dist-CEd_wnRC.js → dist-C-E2R2jk.js} +1 -1
  133. package/web/dist/assets/{dist-C6eaYCUo.js → dist-CFZ4xMSF.js} +1 -1
  134. package/web/dist/assets/{dist-DWmDaXVN.js → dist-CRmX_NvH.js} +1 -1
  135. package/web/dist/assets/{dist-C8RHq5At.js → dist-DLQKRjuW.js} +1 -1
  136. package/web/dist/assets/{dist-Blyulj4_.js → dist-LiAcI8CK.js} +1 -1
  137. package/web/dist/assets/{dist-Ce4ujolc.js → dist-fzPL-8F9.js} +1 -1
  138. package/web/dist/assets/{dist-DoE7uybN.js → dist-wv9ebJK_.js} +1 -1
  139. package/web/dist/assets/editor-pane-actions-_ckwcCII.js +1 -0
  140. package/web/dist/assets/{es2015-DG46NGN7.js → es2015-DPJTBYa9.js} +1 -1
  141. package/web/dist/assets/global-panel-shell-Iir-c_pQ.js +6 -0
  142. package/web/dist/assets/health-panel-Dmf-EhJz.js +1 -0
  143. package/web/dist/assets/index-C4dIj1nT.js +21 -0
  144. package/web/dist/assets/index-CdYMl1M7.css +2 -0
  145. package/web/dist/assets/{mobile-proxy-panel-tU0NyJ3P.js → mobile-proxy-panel-CFcwa11A.js} +6 -6
  146. package/web/dist/assets/mock-config-CGl50MIr.js +1 -0
  147. package/web/dist/assets/mock-editor-panel-xcCk23oH.js +20 -0
  148. package/web/dist/assets/mocks-file-missing-note-CW_ONVLA.js +1 -0
  149. package/web/dist/assets/monaco-editor-BkP7envQ.js +11 -0
  150. package/web/dist/assets/pane-bar-5j9Nf7W_.js +1 -0
  151. package/web/dist/assets/pane-split-C0OSe365.js +1 -0
  152. package/web/dist/assets/panel-heading-izT5q0ZN.js +1 -0
  153. package/web/dist/assets/{pencil-line-Cqe41E4h.js → pencil-line-CEZ7D-de.js} +1 -1
  154. package/web/dist/assets/plugin-code-editor-DKE9dFEE.js +2 -0
  155. package/web/dist/assets/plugin-config-Cg09TsuV.js +2 -0
  156. package/web/dist/assets/plugin-generator-CT1f1tu0.js +2 -0
  157. package/web/dist/assets/plugin-test-dialog-BbLIMnkJ.js +7 -0
  158. package/web/dist/assets/plus-ixZQRprm.js +1 -0
  159. package/web/dist/assets/{refresh-cw-CG3HaiQT.js → refresh-cw-BfLoHqg5.js} +1 -1
  160. package/web/dist/assets/rotate-ccw-CRguJya4.js +1 -0
  161. package/web/dist/assets/route-preview-CKO7nF4W.js +1 -0
  162. package/web/dist/assets/{rule-ai-assistant-panel-DMh0F9dr.js → rule-ai-assistant-panel-CtfUqdYG.js} +2 -2
  163. package/web/dist/assets/rule-config-Cb4ejIXI.js +15 -0
  164. package/web/dist/assets/rule-overview-panel-xtHTS27O.js +1 -0
  165. package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
  166. package/web/dist/assets/settings-panel-CpQuE6nr.js +1 -0
  167. package/web/dist/assets/{shield-check-OqUmVLQt.js → shield-check-hQXjdfZ7.js} +1 -1
  168. package/web/dist/assets/smartphone-D4Q7cQfm.js +1 -0
  169. package/web/dist/assets/switch-94vgsMHm.js +1 -0
  170. package/web/dist/assets/{syntax-highlight-Dk0-ewLI.js → syntax-highlight-BFgN_C0Y.js} +1 -1
  171. package/web/dist/assets/{table-2-BR5e3z8m.js → table-2-DvGHhMm4.js} +1 -1
  172. package/web/dist/assets/table-DaXD6jJv.js +1 -0
  173. package/web/dist/assets/textarea-9wB8Hsz-.js +1 -0
  174. package/web/dist/assets/toast-9yeTf7d9.js +1 -0
  175. package/web/dist/assets/use-mocks-file-status-BUIUJ4oQ.js +1 -0
  176. package/web/dist/assets/{wand-sparkles-CJ7fSnom.js → wand-sparkles-Bmt4dDOf.js} +1 -1
  177. package/web/dist/assets/{wifi-CCLKyySQ.js → wifi-Cjj_Br1u.js} +1 -1
  178. package/web/dist/assets/{zap-DR-xiUMe.js → zap-BuOtogAv.js} +1 -1
  179. package/web/dist/index.html +16 -10
  180. package/web/dist/assets/alert-DCvlZAiv.js +0 -1
  181. package/web/dist/assets/arrow-right-B4Mrlj2-.js +0 -1
  182. package/web/dist/assets/button-BTToMaBW.js +0 -1
  183. package/web/dist/assets/checkbox-D1Z9DtjF.js +0 -1
  184. package/web/dist/assets/chevron-down-CPngAx4M.js +0 -1
  185. package/web/dist/assets/chevron-right-V1DGIDbL.js +0 -1
  186. package/web/dist/assets/code-5_eJ_XpJ.js +0 -1
  187. package/web/dist/assets/code-fixer-D48sCDtp.js +0 -20
  188. package/web/dist/assets/detail-panel-BSFz9CUw.js +0 -4
  189. package/web/dist/assets/file-system-access-_og-Tg9o.js +0 -1
  190. package/web/dist/assets/global-panel-shell-5cJneeis.js +0 -6
  191. package/web/dist/assets/health-panel-BS3L2e5d.js +0 -1
  192. package/web/dist/assets/index-CvTR1IVi.js +0 -18
  193. package/web/dist/assets/index-bfV7sxx5.css +0 -2
  194. package/web/dist/assets/mock-config-CULJP6Vt.js +0 -1
  195. package/web/dist/assets/mock-editor-panel-9wlDaySA.js +0 -1
  196. package/web/dist/assets/monaco-editor-BttaUoOz.js +0 -11
  197. package/web/dist/assets/plugin-code-editor-BAIBp4lg.js +0 -2
  198. package/web/dist/assets/plugin-config-ButUV2SZ.js +0 -1
  199. package/web/dist/assets/plugin-generator-uceUPSCv.js +0 -2
  200. package/web/dist/assets/plugin-test-dialog-BrJ4HfRX.js +0 -7
  201. package/web/dist/assets/plus-tVxSydJ8.js +0 -1
  202. package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +0 -1
  203. package/web/dist/assets/route-preview-C4cGrS1-.js +0 -1
  204. package/web/dist/assets/rule-config-BTTGNwMs.js +0 -15
  205. package/web/dist/assets/rule-overview-panel-SFR2OloL.js +0 -1
  206. package/web/dist/assets/select-aBXQ0zNa.js +0 -1
  207. package/web/dist/assets/settings-panel-BMReDG8b.js +0 -1
  208. package/web/dist/assets/smartphone-DKeDuJTU.js +0 -1
  209. package/web/dist/assets/switch-Bes_YiJu.js +0 -1
  210. package/web/dist/assets/table-nW7uQVcl.js +0 -1
  211. package/web/dist/assets/textarea-BSI0rnW6.js +0 -1
  212. /package/web/dist/assets/{babel-DCr4VA1c.js → babel-bwZm0qi3.js} +0 -0
  213. /package/web/dist/assets/{clipboard-VhPXNN5m.js → clipboard-Bt1qY01V.js} +0 -0
  214. /package/web/dist/assets/{estree-qO_m2Qaw.js → estree-MatbZDwq.js} +0 -0
  215. /package/web/dist/assets/{html-VDGahtyk.js → html-CCrsbFYX.js} +0 -0
  216. /package/web/dist/assets/{postcss-BUm8EKMw.js → postcss-BxN7Gr8n.js} +0 -0
  217. /package/web/dist/assets/{standalone-Bj8SSd8Q.js → standalone-DsnFnsqM.js} +0 -0
@@ -57,7 +57,7 @@ async function run() {
57
57
  const content = await fetch(`${baseUrl}/api/rule-files/${encodeURIComponent(ruleFileArg)}/content`)
58
58
  .then(r => r.ok ? r.text() : null)
59
59
  .catch(() => null)
60
- if (!content) {
60
+ if (content == null) {
61
61
  output.error(`Rule file not found: ${ruleFileArg}`)
62
62
  process.exit(1)
63
63
  }
@@ -91,7 +91,7 @@ async function run() {
91
91
  if (ruleFileArg) {
92
92
  // Use specified rule file
93
93
  const content = getRuleFileContent(ruleFileArg)
94
- if (!content) {
94
+ if (content == null) {
95
95
  output.error(`Rule file not found: ${ruleFileArg}`)
96
96
  process.exit(1)
97
97
  }
@@ -5,7 +5,7 @@
5
5
  const output = require('../../lib/output')
6
6
  const { isProxyRunning } = require('../../lib/proxy-detect')
7
7
  const { apiGet } = require('../../lib/api-client')
8
- const { getRuleFileContent, parseEprcWithExclusions } = require('../../lib/file-access')
8
+ const { getRuleFileContent, parseEprcWithExclusions, isRuleFileReadonly } = require('../../lib/file-access')
9
9
 
10
10
  // Parse arguments (skip 'route' and 'show' from argv)
11
11
  const args = process.argv.slice(4)
@@ -26,33 +26,40 @@ async function run() {
26
26
  let content = null
27
27
  let ruleMap = {}
28
28
  let excludeMap = {}
29
+ // Issue #137: report the lock state (read-only for the CLI either way).
30
+ let readonly = false
29
31
 
30
32
  if (running) {
31
33
  try {
32
34
  content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
35
+ const files = await apiGet('/api/rule-files')
36
+ const info = Array.isArray(files) ? files.find((f) => f && f.name === fileName) : null
37
+ readonly = Boolean(info && info.readonly)
33
38
  const parsed = parseEprcWithExclusions(typeof content === 'string' ? content : String(content))
34
39
  ruleMap = parsed.ruleMap
35
40
  excludeMap = parsed.excludeMap
36
41
  } catch (e) {
37
42
  // Fallback to file mode
38
43
  content = getRuleFileContent(fileName)
39
- if (!content) {
44
+ if (content == null) {
40
45
  output.error(`Route file not found: ${fileName}`)
41
46
  process.exit(1)
42
47
  }
43
48
  const parsed = parseEprcWithExclusions(content)
44
49
  ruleMap = parsed.ruleMap
45
50
  excludeMap = parsed.excludeMap
51
+ readonly = isRuleFileReadonly(fileName)
46
52
  }
47
53
  } else {
48
54
  content = getRuleFileContent(fileName)
49
- if (!content) {
55
+ if (content == null) {
50
56
  output.error(`Route file not found: ${fileName}`)
51
57
  process.exit(1)
52
58
  }
53
59
  const parsed = parseEprcWithExclusions(content)
54
60
  ruleMap = parsed.ruleMap
55
61
  excludeMap = parsed.excludeMap
62
+ readonly = isRuleFileReadonly(fileName)
56
63
  }
57
64
 
58
65
  // Build display rules (handle [marker] syntax)
@@ -76,6 +83,7 @@ async function run() {
76
83
  if (jsonFlag) {
77
84
  output.jsonRaw({
78
85
  file: fileName,
86
+ readonly,
79
87
  rules: displayRules,
80
88
  ruleCount: Object.keys(displayRules).length,
81
89
  exclusionCount: totalExclusions
@@ -84,6 +92,7 @@ async function run() {
84
92
  }
85
93
 
86
94
  output.header(`Route File: ${fileName}`)
95
+ output.kv('Read-only', readonly ? 'Yes (unlock in the Meddle UI)' : 'No')
87
96
  output.kv('Rules', Object.keys(displayRules).length)
88
97
  if (totalExclusions > 0) {
89
98
  output.kv('Exclusions', totalExclusions)
@@ -1,126 +1,169 @@
1
1
  /**
2
2
  * meddle route update - Update a rule in a route file
3
+ *
4
+ * Issue #149: `--exclusions=` clears a rule's exclusions. Omitting the flag
5
+ * (and any !path in the pattern) still keeps them.
3
6
  */
4
7
 
5
8
  const output = require('../../lib/output')
6
9
  const { isProxyRunning } = require('../../lib/proxy-detect')
7
10
  const { apiGet, apiPut } = require('../../lib/api-client')
8
- const { getRuleFileContent, saveRuleFileContent, parseEprc, ruleMapToEprcText } = require('../../lib/file-access')
11
+ const { getRuleFileContent, saveRuleFileContent } = require('../../lib/file-access')
12
+ const { updateRouteRule, patternKey, parseRuleLine } = require('../../lib/rule-file-edit')
13
+ const { isRulesetReadonlyError, exitRulesetReadonly } = require('../../lib/ruleset-readonly')
14
+ const {
15
+ parseRouteWriteArgs,
16
+ parsePatternArg,
17
+ resolveExclusions,
18
+ UPDATE_HELP,
19
+ } = require('../../lib/route-write-args')
9
20
 
10
- // Parse arguments (skip 'route' and 'update' from argv)
11
21
  const args = process.argv.slice(4)
12
- const jsonFlag = args.includes('--json')
22
+
23
+ function fail(message, jsonFlag) {
24
+ if (jsonFlag) {
25
+ console.log(JSON.stringify({ success: false, error: { message } }))
26
+ console.error(message)
27
+ } else {
28
+ output.setJsonMode(false)
29
+ output.error(message)
30
+ }
31
+ process.exit(1)
32
+ }
33
+
34
+ let parsed
35
+ try {
36
+ parsed = parseRouteWriteArgs(args)
37
+ } catch (e) {
38
+ fail(e.message, args.includes('--json'))
39
+ }
40
+
41
+ if (parsed.help) {
42
+ console.log(UPDATE_HELP.trim())
43
+ process.exit(0)
44
+ }
45
+
46
+ const jsonFlag = parsed.json
13
47
  output.setJsonMode(jsonFlag)
14
48
 
15
- // Get positional arguments: file, pattern, newTarget
16
- const positionalArgs = args.filter(a => !a.startsWith('--'))
17
- const fileName = positionalArgs[0]
18
- const pattern = positionalArgs[1]
19
- const newTarget = positionalArgs[2]
49
+ const fileName = parsed.positional[0]
50
+ const pattern = parsed.positional[1]
51
+ const newTarget = parsed.positional[2]
20
52
 
21
- async function run() {
22
- if (!fileName) {
23
- output.error('Missing required argument: <file>')
24
- process.exit(1)
53
+ /** Set once arguments are valid. `explicit` means the user asked to set exclusions, including clear. */
54
+ let explicitExclusions = false
55
+ let appliedExclusions = []
56
+
57
+ function planUpdate() {
58
+ const { patterns, exclusions: patternExclusions } = parsePatternArg(pattern)
59
+ if (patterns.length === 0) {
60
+ fail('Missing required argument: <pattern>', jsonFlag)
25
61
  }
26
- if (!pattern) {
27
- output.error('Missing required argument: <pattern>')
28
- process.exit(1)
62
+ const resolved = resolveExclusions(patternExclusions, parsed.exclusions)
63
+ explicitExclusions = resolved.explicit
64
+ appliedExclusions = resolved.exclusions || []
65
+ return { patterns, exclusions: resolved.explicit ? resolved.exclusions : undefined }
66
+ }
67
+
68
+ /**
69
+ * Issue #145: rewrite only the line(s) holding this pattern. The rule keeps its
70
+ * own `!exclusions` unless new ones are given. Issue #149: `--exclusions=` passes
71
+ * `[]`, which removes `!path` from that line. Every other line keeps its bytes.
72
+ */
73
+ function applyUpdate(text, plan) {
74
+ const target = newTarget ? newTarget.trim() : undefined
75
+ let next = text
76
+ for (const p of plan.patterns) {
77
+ const result = updateRouteRule(next, { pattern: p, target, exclusions: plan.exclusions })
78
+ if (!result.found) {
79
+ fail(`Pattern not found: ${pattern.trim()}`, jsonFlag)
80
+ }
81
+ next = result.content
29
82
  }
30
- if (!newTarget) {
31
- output.error('Missing required argument: <target>')
32
- process.exit(1)
83
+ return next
84
+ }
85
+
86
+ /** Target token actually written, for feedback when the user did not pass one. */
87
+ function targetOnDisk(content, patternToken) {
88
+ const key = patternKey(patternToken)
89
+ const lines = content.split(/\r?\n/)
90
+ for (const line of lines) {
91
+ const parsedLine = parseRuleLine(line)
92
+ if (parsedLine && parsedLine.patterns.some((p) => patternKey(p) === key)) return parsedLine.target
33
93
  }
94
+ return ''
95
+ }
34
96
 
35
- const running = await isProxyRunning()
97
+ function emitUpdated({ fileOnly, target }) {
98
+ const payload = { success: true, file: fileName, pattern: pattern.trim(), newTarget: target }
99
+ if (explicitExclusions) payload.exclusions = appliedExclusions
100
+ if (fileOnly) {
101
+ payload.fileOnly = true
102
+ payload.warning = output.FILE_ONLY_WARNING
103
+ }
104
+ if (jsonFlag) {
105
+ output.jsonRaw(payload)
106
+ return
107
+ }
108
+ output.header('Route Rule Updated')
109
+ output.success(`${pattern.trim()} -> ${target}`)
110
+ if (explicitExclusions) {
111
+ output.kv('Exclusions', appliedExclusions.length ? appliedExclusions.map((e) => `!${e}`).join(' ') : 'cleared')
112
+ }
113
+ output.kv('File', fileName)
114
+ }
115
+
116
+ async function run() {
117
+ if (!fileName) fail('Missing required argument: <file>', jsonFlag)
118
+ if (!pattern) fail('Missing required argument: <pattern>', jsonFlag)
119
+ if (parsed.positional.length > 3) fail(`Unexpected argument: ${parsed.positional[3]}`, jsonFlag)
120
+
121
+ let plan
122
+ try {
123
+ plan = planUpdate()
124
+ } catch (e) {
125
+ fail(e.message, jsonFlag)
126
+ }
127
+ if (!newTarget && !explicitExclusions) fail('Missing required argument: <target>', jsonFlag)
36
128
 
37
- const pat = pattern.trim()
38
- const tgt = newTarget.trim()
129
+ const running = await isProxyRunning()
39
130
 
40
- // Handle [marker] syntax for lookup
41
- const bm = pat.match(/\[([^\]]+)\]/)
42
- const internalKey = bm ? pat.replace(bm[0], bm[1]) : pat
131
+ const finish = (content, fileOnly) => {
132
+ const target = newTarget ? newTarget.trim() : targetOnDisk(content, plan.patterns[0])
133
+ emitUpdated({ fileOnly, target })
134
+ }
43
135
 
44
136
  if (running) {
45
137
  try {
46
138
  let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
47
139
  const text = typeof content === 'string' ? content : String(content)
48
- const ruleMap = parseEprc(text)
49
-
50
- if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
51
- output.error(`Pattern not found: ${pat}`)
52
- process.exit(1)
53
- }
54
-
55
- ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
56
- const newContent = ruleMapToEprcText(ruleMap)
140
+ const newContent = applyUpdate(text, plan)
57
141
  await apiPut(`/api/rule-files/${encodeURIComponent(fileName)}/content`, { content: newContent })
58
-
59
- if (jsonFlag) {
60
- output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt })
61
- return
62
- }
63
-
64
- output.header('Route Rule Updated')
65
- output.success(`${pat} -> ${tgt}`)
66
- output.kv('File', fileName)
142
+ finish(newContent, false)
143
+ return
67
144
  } catch (e) {
145
+ // Issue #137: a locked ruleset is a final answer, never a cue to write the file directly.
146
+ if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
68
147
  // Fallback to file mode
69
148
  const content = getRuleFileContent(fileName)
70
- if (!content) {
71
- output.error(`Route file not found: ${fileName}`)
72
- process.exit(1)
73
- }
74
-
75
- const ruleMap = parseEprc(content)
76
- if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
77
- output.error(`Pattern not found: ${pat}`)
78
- process.exit(1)
79
- }
80
-
81
- ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
82
- const newContent = ruleMapToEprcText(ruleMap)
149
+ if (content == null) fail(`Route file not found: ${fileName}`, jsonFlag)
150
+ const newContent = applyUpdate(content, plan)
83
151
  saveRuleFileContent(fileName, newContent)
84
-
85
- if (jsonFlag) {
86
- output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt })
87
- return
88
- }
89
-
90
- output.header('Route Rule Updated')
91
- output.success(`${pat} -> ${tgt}`)
92
- output.kv('File', fileName)
93
- }
94
- } else {
95
- // File mode
96
- const content = getRuleFileContent(fileName)
97
- if (!content) {
98
- output.error(`Route file not found: ${fileName}`)
99
- process.exit(1)
100
- }
101
-
102
- const ruleMap = parseEprc(content)
103
- if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
104
- output.error(`Pattern not found: ${pat}`)
105
- process.exit(1)
106
- }
107
-
108
- ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
109
- const newContent = ruleMapToEprcText(ruleMap)
110
- saveRuleFileContent(fileName, newContent)
111
-
112
- if (jsonFlag) {
113
- output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt })
152
+ output.warnFileOnlyMode()
153
+ finish(newContent, true)
114
154
  return
115
155
  }
116
-
117
- output.header('Route Rule Updated')
118
- output.success(`${pat} -> ${tgt}`)
119
- output.kv('File', fileName)
120
156
  }
157
+
158
+ const content = getRuleFileContent(fileName)
159
+ if (content == null) fail(`Route file not found: ${fileName}`, jsonFlag)
160
+ const newContent = applyUpdate(content, plan)
161
+ saveRuleFileContent(fileName, newContent)
162
+ output.warnFileOnlyMode()
163
+ finish(newContent, true)
121
164
  }
122
165
 
123
166
  run().catch(e => {
124
- output.error(e.message)
125
- process.exit(1)
126
- })
167
+ if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
168
+ fail(e.message, jsonFlag)
169
+ })
@@ -22,6 +22,22 @@ async function run() {
22
22
  let activeRoutes = 0
23
23
  let health = null
24
24
 
25
+ let mocksError = null
26
+
27
+ function readMocksFromFile() {
28
+ try {
29
+ const mocks = loadMockRules()
30
+ mockCount = mocks.length
31
+ activeMocks = mocks.filter(m => m.enabled).length
32
+ } catch (err) {
33
+ mocksError = err && err.code === 'MOCKS_FILE_INVALID'
34
+ ? 'Mock 规则文件格式错误'
35
+ : (err && err.message ? String(err.message) : String(err))
36
+ mockCount = 0
37
+ activeMocks = 0
38
+ }
39
+ }
40
+
25
41
  if (running) {
26
42
  try {
27
43
  // Get mock rules from API
@@ -36,19 +52,13 @@ async function run() {
36
52
  health = await apiGet('/api/health')
37
53
  } catch (_) {
38
54
  // Fallback to file access
39
- const mocks = loadMockRules()
40
- mockCount = mocks.length
41
- activeMocks = mocks.filter(m => m.enabled).length
42
-
55
+ readMocksFromFile()
43
56
  routeFiles = listRuleFiles()
44
57
  activeRoutes = routeFiles.filter(f => f.enabled).length
45
58
  }
46
59
  } else {
47
60
  // File mode
48
- const mocks = loadMockRules()
49
- mockCount = mocks.length
50
- activeMocks = mocks.filter(m => m.enabled).length
51
-
61
+ readMocksFromFile()
52
62
  routeFiles = listRuleFiles()
53
63
  activeRoutes = routeFiles.filter(f => f.enabled).length
54
64
  }
@@ -61,7 +71,9 @@ async function run() {
61
71
  proxyUrl: url,
62
72
  running,
63
73
  health,
64
- mocks: { total: mockCount, active: activeMocks },
74
+ mocks: mocksError
75
+ ? { total: mockCount, active: activeMocks, error: mocksError }
76
+ : { total: mockCount, active: activeMocks },
65
77
  routes: { files: routeFiles.length, activeFiles: activeRoutes, totalRules: totalRouteRules, totalExclusions }
66
78
  })
67
79
  return
@@ -81,8 +93,12 @@ async function run() {
81
93
  }
82
94
 
83
95
  output.section('Mock Rules')
84
- output.kv('Total', mockCount)
85
- output.kv('Active', activeMocks)
96
+ if (mocksError) {
97
+ output.warning(mocksError)
98
+ } else {
99
+ output.kv('Total', mockCount)
100
+ output.kv('Active', activeMocks)
101
+ }
86
102
 
87
103
  output.section('Route Rules')
88
104
  output.kv('Files', routeFiles.length)
@@ -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/doctor.js CHANGED
@@ -7,7 +7,7 @@
7
7
  const fs = require('fs')
8
8
  const path = require('path')
9
9
  const chalk = require('chalk')
10
- const { resolveMeddleHome } = require('./lib/meddle-home')
10
+ const { resolveMeddleHome, resolveConfiguredMocksPath } = require('./lib/meddle-home')
11
11
 
12
12
  const meddleDir = resolveMeddleHome()
13
13
  const settingsPath = path.resolve(meddleDir, 'settings.json')
@@ -183,10 +183,14 @@ function checkMocksFile(settings) {
183
183
 
184
184
  let mocksPath = defaultMocksPath
185
185
 
186
- // 检查自定义路径
186
+ // Same resolution as the proxy: ~ expands, relative paths use MEDDLE_HOME.
187
187
  if (settings && settings.mocksFilePath) {
188
- mocksPath = settings.mocksFilePath
189
- printInfo(`使用自定义路径: ${mocksPath}`)
188
+ const resolved = resolveConfiguredMocksPath(settings.mocksFilePath, meddleDir)
189
+ mocksPath = resolved || defaultMocksPath
190
+ printInfo(`使用自定义路径: ${settings.mocksFilePath}`)
191
+ if (resolved && resolved !== settings.mocksFilePath) {
192
+ printInfo(`解析后路径: ${resolved}`)
193
+ }
190
194
  } else {
191
195
  printInfo(`使用默认路径: ${mocksPath}`)
192
196
  }
package/bin/index.js CHANGED
@@ -59,13 +59,13 @@ if (command === 'mock' || command === 'route' || command === 'session') {
59
59
  // No command or only flags - start proxy (default behavior)
60
60
  require('../index.js')
61
61
  } else {
62
- console.log(`Unknown command: ${command}`)
63
- showHelp()
62
+ console.error(`Unknown command: ${command}`)
63
+ showHelp(process.stderr)
64
64
  process.exit(1)
65
65
  }
66
66
 
67
- function showHelp() {
68
- console.log(`
67
+ function showHelp(stream = process.stdout) {
68
+ stream.write(`
69
69
  Meddle - 开发代理工具
70
70
 
71
71
  用法:
@@ -106,8 +106,10 @@ Route 命令:
106
106
  meddle route active 查看当前激活的路由文件
107
107
  meddle route active set <file> 设置激活的路由文件
108
108
  meddle route create <name> 创建路由文件
109
- meddle route add <file> <pattern> <target> 添加路由规则
110
- meddle route update <file> <pattern> <target> 更新路由规则
109
+ meddle route add <file> <pattern> <target> [--exclusions=]
110
+ 添加路由规则;--exclusions= 清空排除项
111
+ meddle route update <file> <pattern> [<target>] [--exclusions=]
112
+ 更新路由规则;--exclusions= 清空排除项
111
113
  meddle route delete <file> <pattern> 删除路由规则
112
114
 
113
115
  全局选项:
@@ -137,8 +139,10 @@ Route 命令:
137
139
  meddle route preview "https://api.example.com/v1/users" # 预览转发目标
138
140
  meddle route preview "https://cdn.com/assets/js/app.js" --file dev-rules --json
139
141
  meddle route add dev-rules "api.test.com" "localhost:3000"
142
+ meddle route add dev-rules "api.test.com" "localhost:3000" --exclusions=
143
+ meddle route update dev-rules "api.test.com" --exclusions=
140
144
  meddle route active set beta-rules
141
145
 
142
146
  更多信息请访问: https://github.com/JonathanLee-LX/meddle
143
- `)
147
+ `)
144
148
  }
@@ -25,14 +25,26 @@ async function apiRequest(method, pathname, body, timeoutMs = DEFAULT_TIMEOUT) {
25
25
 
26
26
  if (!response.ok) {
27
27
  const text = await response.text()
28
- throw new Error(text || response.statusText)
28
+ const err = new Error(text || response.statusText)
29
+ err.status = response.status
30
+ // Structured errors (e.g. issue #137 RULESET_READONLY) keep their code.
31
+ try {
32
+ const data = JSON.parse(text)
33
+ if (data && typeof data === 'object') {
34
+ if (typeof data.code === 'string') err.code = data.code
35
+ if (typeof data.ruleFile === 'string') err.ruleFile = data.ruleFile
36
+ }
37
+ } catch (_) {}
38
+ throw err
29
39
  }
30
40
 
31
41
  const contentType = response.headers.get('content-type')
32
42
  if (contentType && contentType.includes('application/json')) {
33
43
  return response.json()
34
44
  }
35
- return response.text()
45
+ // Issue #145: `Response.text()` strips a leading UTF-8 BOM, so a rule file
46
+ // read and written back through the API would silently lose it.
47
+ return new TextDecoder('utf-8', { ignoreBOM: true }).decode(await response.arrayBuffer())
36
48
  }
37
49
 
38
50
  /**