@jonathanleelx/meddle 0.0.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 (361) hide show
  1. package/CONFIG_STRUCTURE.md +315 -0
  2. package/LICENSE +21 -0
  3. package/README.md +298 -0
  4. package/bin/commands/mock/add.js +92 -0
  5. package/bin/commands/mock/delete.js +58 -0
  6. package/bin/commands/mock/index.js +50 -0
  7. package/bin/commands/mock/list.js +68 -0
  8. package/bin/commands/mock/toggle.js +64 -0
  9. package/bin/commands/mock/update.js +94 -0
  10. package/bin/commands/route/active.js +141 -0
  11. package/bin/commands/route/add.js +127 -0
  12. package/bin/commands/route/create.js +77 -0
  13. package/bin/commands/route/delete.js +120 -0
  14. package/bin/commands/route/index.js +59 -0
  15. package/bin/commands/route/list.js +67 -0
  16. package/bin/commands/route/preview.js +159 -0
  17. package/bin/commands/route/show.js +111 -0
  18. package/bin/commands/route/update.js +126 -0
  19. package/bin/commands/session/create.js +166 -0
  20. package/bin/commands/session/delete.js +69 -0
  21. package/bin/commands/session/index.js +61 -0
  22. package/bin/commands/session/list.js +42 -0
  23. package/bin/commands/session/prune.js +31 -0
  24. package/bin/commands/start.js +43 -0
  25. package/bin/commands/status.js +115 -0
  26. package/bin/commands/supervise.js +106 -0
  27. package/bin/commands/url.js +30 -0
  28. package/bin/doctor.js +356 -0
  29. package/bin/index +130 -0
  30. package/bin/lib/api-client.js +72 -0
  31. package/bin/lib/file-access.js +337 -0
  32. package/bin/lib/meddle-home.js +20 -0
  33. package/bin/lib/output.js +146 -0
  34. package/bin/lib/parsers.js +127 -0
  35. package/bin/lib/proxy-detect.js +105 -0
  36. package/bin/lib/route-preview.js +180 -0
  37. package/bin/lib/session-args.js +54 -0
  38. package/bin/lib/sessions.js +213 -0
  39. package/dist/cert.d.ts +10 -0
  40. package/dist/cert.d.ts.map +1 -0
  41. package/dist/cert.js +184 -0
  42. package/dist/cert.js.map +1 -0
  43. package/dist/core/application-identity.d.ts +55 -0
  44. package/dist/core/application-identity.d.ts.map +1 -0
  45. package/dist/core/application-identity.js +321 -0
  46. package/dist/core/application-identity.js.map +1 -0
  47. package/dist/core/body-utils.d.ts +2 -0
  48. package/dist/core/body-utils.d.ts.map +1 -0
  49. package/dist/core/body-utils.js +21 -0
  50. package/dist/core/body-utils.js.map +1 -0
  51. package/dist/core/browser.d.ts +2 -0
  52. package/dist/core/browser.d.ts.map +1 -0
  53. package/dist/core/browser.js +92 -0
  54. package/dist/core/browser.js.map +1 -0
  55. package/dist/core/client-identity.d.ts +27 -0
  56. package/dist/core/client-identity.d.ts.map +1 -0
  57. package/dist/core/client-identity.js +136 -0
  58. package/dist/core/client-identity.js.map +1 -0
  59. package/dist/core/config-diagnostics.d.ts +14 -0
  60. package/dist/core/config-diagnostics.d.ts.map +1 -0
  61. package/dist/core/config-diagnostics.js +137 -0
  62. package/dist/core/config-diagnostics.js.map +1 -0
  63. package/dist/core/connect-tunnel.d.ts +5 -0
  64. package/dist/core/connect-tunnel.d.ts.map +1 -0
  65. package/dist/core/connect-tunnel.js +45 -0
  66. package/dist/core/connect-tunnel.js.map +1 -0
  67. package/dist/core/content-encoding.d.ts +2 -0
  68. package/dist/core/content-encoding.d.ts.map +1 -0
  69. package/dist/core/content-encoding.js +77 -0
  70. package/dist/core/content-encoding.js.map +1 -0
  71. package/dist/core/custom-plugin-loader.d.ts +18 -0
  72. package/dist/core/custom-plugin-loader.d.ts.map +1 -0
  73. package/dist/core/custom-plugin-loader.js +178 -0
  74. package/dist/core/custom-plugin-loader.js.map +1 -0
  75. package/dist/core/h2-pool.d.ts +4 -0
  76. package/dist/core/h2-pool.d.ts.map +1 -0
  77. package/dist/core/h2-pool.js +180 -0
  78. package/dist/core/h2-pool.js.map +1 -0
  79. package/dist/core/inspection-dispatch.d.ts +5 -0
  80. package/dist/core/inspection-dispatch.d.ts.map +1 -0
  81. package/dist/core/inspection-dispatch.js +146 -0
  82. package/dist/core/inspection-dispatch.js.map +1 -0
  83. package/dist/core/log-rate-limit.d.ts +34 -0
  84. package/dist/core/log-rate-limit.d.ts.map +1 -0
  85. package/dist/core/log-rate-limit.js +52 -0
  86. package/dist/core/log-rate-limit.js.map +1 -0
  87. package/dist/core/map-local.d.ts +4 -0
  88. package/dist/core/map-local.d.ts.map +1 -0
  89. package/dist/core/map-local.js +195 -0
  90. package/dist/core/map-local.js.map +1 -0
  91. package/dist/core/meddle-home.d.ts +13 -0
  92. package/dist/core/meddle-home.d.ts.map +1 -0
  93. package/dist/core/meddle-home.js +56 -0
  94. package/dist/core/meddle-home.js.map +1 -0
  95. package/dist/core/mock-gate.d.ts +3 -0
  96. package/dist/core/mock-gate.d.ts.map +1 -0
  97. package/dist/core/mock-gate.js +20 -0
  98. package/dist/core/mock-gate.js.map +1 -0
  99. package/dist/core/mock-handler.d.ts +5 -0
  100. package/dist/core/mock-handler.d.ts.map +1 -0
  101. package/dist/core/mock-handler.js +339 -0
  102. package/dist/core/mock-handler.js.map +1 -0
  103. package/dist/core/on-mode-gate.d.ts +4 -0
  104. package/dist/core/on-mode-gate.d.ts.map +1 -0
  105. package/dist/core/on-mode-gate.js +64 -0
  106. package/dist/core/on-mode-gate.js.map +1 -0
  107. package/dist/core/pipeline-gate.d.ts +3 -0
  108. package/dist/core/pipeline-gate.d.ts.map +1 -0
  109. package/dist/core/pipeline-gate.js +34 -0
  110. package/dist/core/pipeline-gate.js.map +1 -0
  111. package/dist/core/pipeline.d.ts +6 -0
  112. package/dist/core/pipeline.d.ts.map +1 -0
  113. package/dist/core/pipeline.js +228 -0
  114. package/dist/core/pipeline.js.map +1 -0
  115. package/dist/core/plugin-bootstrap-runner.d.ts +6 -0
  116. package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -0
  117. package/dist/core/plugin-bootstrap-runner.js +148 -0
  118. package/dist/core/plugin-bootstrap-runner.js.map +1 -0
  119. package/dist/core/plugin-bootstrap.d.ts +3 -0
  120. package/dist/core/plugin-bootstrap.d.ts.map +1 -0
  121. package/dist/core/plugin-bootstrap.js +15 -0
  122. package/dist/core/plugin-bootstrap.js.map +1 -0
  123. package/dist/core/plugin-compiler.d.ts +37 -0
  124. package/dist/core/plugin-compiler.d.ts.map +1 -0
  125. package/dist/core/plugin-compiler.js +172 -0
  126. package/dist/core/plugin-compiler.js.map +1 -0
  127. package/dist/core/plugin-context-factory.d.ts +29 -0
  128. package/dist/core/plugin-context-factory.d.ts.map +1 -0
  129. package/dist/core/plugin-context-factory.js +206 -0
  130. package/dist/core/plugin-context-factory.js.map +1 -0
  131. package/dist/core/plugin-generator.d.ts +47 -0
  132. package/dist/core/plugin-generator.d.ts.map +1 -0
  133. package/dist/core/plugin-generator.js +697 -0
  134. package/dist/core/plugin-generator.js.map +1 -0
  135. package/dist/core/plugin-health.d.ts +3 -0
  136. package/dist/core/plugin-health.d.ts.map +1 -0
  137. package/dist/core/plugin-health.js +54 -0
  138. package/dist/core/plugin-health.js.map +1 -0
  139. package/dist/core/plugin-intercept.d.ts +17 -0
  140. package/dist/core/plugin-intercept.d.ts.map +1 -0
  141. package/dist/core/plugin-intercept.js +133 -0
  142. package/dist/core/plugin-intercept.js.map +1 -0
  143. package/dist/core/plugin-runtime.d.ts +33 -0
  144. package/dist/core/plugin-runtime.d.ts.map +1 -0
  145. package/dist/core/plugin-runtime.js +254 -0
  146. package/dist/core/plugin-runtime.js.map +1 -0
  147. package/dist/core/proxy-context.d.ts +3 -0
  148. package/dist/core/proxy-context.d.ts.map +1 -0
  149. package/dist/core/proxy-context.js +129 -0
  150. package/dist/core/proxy-context.js.map +1 -0
  151. package/dist/core/proxy-record.d.ts +3 -0
  152. package/dist/core/proxy-record.d.ts.map +1 -0
  153. package/dist/core/proxy-record.js +32 -0
  154. package/dist/core/proxy-record.js.map +1 -0
  155. package/dist/core/refactor-config.d.ts +6 -0
  156. package/dist/core/refactor-config.d.ts.map +1 -0
  157. package/dist/core/refactor-config.js +45 -0
  158. package/dist/core/refactor-config.js.map +1 -0
  159. package/dist/core/refactor-status.d.ts +3 -0
  160. package/dist/core/refactor-status.d.ts.map +1 -0
  161. package/dist/core/refactor-status.js +19 -0
  162. package/dist/core/refactor-status.js.map +1 -0
  163. package/dist/core/remote-access.d.ts +40 -0
  164. package/dist/core/remote-access.d.ts.map +1 -0
  165. package/dist/core/remote-access.js +281 -0
  166. package/dist/core/remote-access.js.map +1 -0
  167. package/dist/core/route-decision.d.ts +3 -0
  168. package/dist/core/route-decision.d.ts.map +1 -0
  169. package/dist/core/route-decision.js +20 -0
  170. package/dist/core/route-decision.js.map +1 -0
  171. package/dist/core/route-loader.d.ts +14 -0
  172. package/dist/core/route-loader.d.ts.map +1 -0
  173. package/dist/core/route-loader.js +94 -0
  174. package/dist/core/route-loader.js.map +1 -0
  175. package/dist/core/route-preview.d.ts +15 -0
  176. package/dist/core/route-preview.d.ts.map +1 -0
  177. package/dist/core/route-preview.js +91 -0
  178. package/dist/core/route-preview.js.map +1 -0
  179. package/dist/core/runtime-health.d.ts +92 -0
  180. package/dist/core/runtime-health.d.ts.map +1 -0
  181. package/dist/core/runtime-health.js +205 -0
  182. package/dist/core/runtime-health.js.map +1 -0
  183. package/dist/core/sessions.d.ts +19 -0
  184. package/dist/core/sessions.d.ts.map +1 -0
  185. package/dist/core/sessions.js +93 -0
  186. package/dist/core/sessions.js.map +1 -0
  187. package/dist/core/shadow-compare.d.ts +8 -0
  188. package/dist/core/shadow-compare.d.ts.map +1 -0
  189. package/dist/core/shadow-compare.js +83 -0
  190. package/dist/core/shadow-compare.js.map +1 -0
  191. package/dist/core/shadow-readiness.d.ts +4 -0
  192. package/dist/core/shadow-readiness.d.ts.map +1 -0
  193. package/dist/core/shadow-readiness.js +118 -0
  194. package/dist/core/shadow-readiness.js.map +1 -0
  195. package/dist/core/short-response.d.ts +5 -0
  196. package/dist/core/short-response.d.ts.map +1 -0
  197. package/dist/core/short-response.js +18 -0
  198. package/dist/core/short-response.js.map +1 -0
  199. package/dist/core/static-server.d.ts +9 -0
  200. package/dist/core/static-server.d.ts.map +1 -0
  201. package/dist/core/static-server.js +136 -0
  202. package/dist/core/static-server.js.map +1 -0
  203. package/dist/core/types.d.ts +590 -0
  204. package/dist/core/types.d.ts.map +1 -0
  205. package/dist/core/types.js +4 -0
  206. package/dist/core/types.js.map +1 -0
  207. package/dist/helpers.d.ts +36 -0
  208. package/dist/helpers.d.ts.map +1 -0
  209. package/dist/helpers.js +290 -0
  210. package/dist/helpers.js.map +1 -0
  211. package/dist/plugins/builtin/index.d.ts +3 -0
  212. package/dist/plugins/builtin/index.d.ts.map +1 -0
  213. package/dist/plugins/builtin/index.js +23 -0
  214. package/dist/plugins/builtin/index.js.map +1 -0
  215. package/dist/plugins/builtin/logger-plugin.d.ts +8 -0
  216. package/dist/plugins/builtin/logger-plugin.d.ts.map +1 -0
  217. package/dist/plugins/builtin/logger-plugin.js +121 -0
  218. package/dist/plugins/builtin/logger-plugin.js.map +1 -0
  219. package/dist/plugins/builtin/mock-plugin.d.ts +3 -0
  220. package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -0
  221. package/dist/plugins/builtin/mock-plugin.js +99 -0
  222. package/dist/plugins/builtin/mock-plugin.js.map +1 -0
  223. package/dist/plugins/builtin/router-plugin.d.ts +3 -0
  224. package/dist/plugins/builtin/router-plugin.d.ts.map +1 -0
  225. package/dist/plugins/builtin/router-plugin.js +36 -0
  226. package/dist/plugins/builtin/router-plugin.js.map +1 -0
  227. package/dist/server/agent/model.d.ts +30 -0
  228. package/dist/server/agent/model.d.ts.map +1 -0
  229. package/dist/server/agent/model.js +143 -0
  230. package/dist/server/agent/model.js.map +1 -0
  231. package/dist/server/agent/routes.d.ts +4 -0
  232. package/dist/server/agent/routes.d.ts.map +1 -0
  233. package/dist/server/agent/routes.js +188 -0
  234. package/dist/server/agent/routes.js.map +1 -0
  235. package/dist/server/agent/runtime.d.ts +12 -0
  236. package/dist/server/agent/runtime.d.ts.map +1 -0
  237. package/dist/server/agent/runtime.js +208 -0
  238. package/dist/server/agent/runtime.js.map +1 -0
  239. package/dist/server/agent/tools.d.ts +13 -0
  240. package/dist/server/agent/tools.d.ts.map +1 -0
  241. package/dist/server/agent/tools.js +1201 -0
  242. package/dist/server/agent/tools.js.map +1 -0
  243. package/dist/server/agent/types.d.ts +56 -0
  244. package/dist/server/agent/types.d.ts.map +1 -0
  245. package/dist/server/agent/types.js +3 -0
  246. package/dist/server/agent/types.js.map +1 -0
  247. package/dist/server/config.d.ts +9 -0
  248. package/dist/server/config.d.ts.map +1 -0
  249. package/dist/server/config.js +124 -0
  250. package/dist/server/config.js.map +1 -0
  251. package/dist/server/health.d.ts +4 -0
  252. package/dist/server/health.d.ts.map +1 -0
  253. package/dist/server/health.js +22 -0
  254. package/dist/server/health.js.map +1 -0
  255. package/dist/server/index.d.ts +239 -0
  256. package/dist/server/index.d.ts.map +1 -0
  257. package/dist/server/index.js +44 -0
  258. package/dist/server/index.js.map +1 -0
  259. package/dist/server/logs.d.ts +4 -0
  260. package/dist/server/logs.d.ts.map +1 -0
  261. package/dist/server/logs.js +40 -0
  262. package/dist/server/logs.js.map +1 -0
  263. package/dist/server/mocks.d.ts +9 -0
  264. package/dist/server/mocks.d.ts.map +1 -0
  265. package/dist/server/mocks.js +103 -0
  266. package/dist/server/mocks.js.map +1 -0
  267. package/dist/server/pipeline.d.ts +16 -0
  268. package/dist/server/pipeline.d.ts.map +1 -0
  269. package/dist/server/pipeline.js +159 -0
  270. package/dist/server/pipeline.js.map +1 -0
  271. package/dist/server/plugins.d.ts +32 -0
  272. package/dist/server/plugins.d.ts.map +1 -0
  273. package/dist/server/plugins.js +795 -0
  274. package/dist/server/plugins.js.map +1 -0
  275. package/dist/server/refactor.d.ts +4 -0
  276. package/dist/server/refactor.d.ts.map +1 -0
  277. package/dist/server/refactor.js +86 -0
  278. package/dist/server/refactor.js.map +1 -0
  279. package/dist/server/rule-files.d.ts +34 -0
  280. package/dist/server/rule-files.d.ts.map +1 -0
  281. package/dist/server/rule-files.js +389 -0
  282. package/dist/server/rule-files.js.map +1 -0
  283. package/dist/server/rules.d.ts +4 -0
  284. package/dist/server/rules.d.ts.map +1 -0
  285. package/dist/server/rules.js +40 -0
  286. package/dist/server/rules.js.map +1 -0
  287. package/dist/server/sessions.d.ts +4 -0
  288. package/dist/server/sessions.d.ts.map +1 -0
  289. package/dist/server/sessions.js +20 -0
  290. package/dist/server/sessions.js.map +1 -0
  291. package/index.js +950 -0
  292. package/mcp-server.js +794 -0
  293. package/package.json +92 -0
  294. package/plugins/builtin/index.ts +25 -0
  295. package/plugins/builtin/logger-plugin.ts +126 -0
  296. package/plugins/builtin/mock-plugin.ts +112 -0
  297. package/plugins/builtin/router-plugin.ts +35 -0
  298. package/web/README.md +111 -0
  299. package/web/dist/assets/ai-config-store-DQXVJVn_.js +1 -0
  300. package/web/dist/assets/alert-COF-DIAE.js +1 -0
  301. package/web/dist/assets/arrow-right-B4Mrlj2-.js +1 -0
  302. package/web/dist/assets/babel-DsprIpXZ.js +15 -0
  303. package/web/dist/assets/body-diff-view-D7dCWajG.js +6 -0
  304. package/web/dist/assets/button-BTToMaBW.js +1 -0
  305. package/web/dist/assets/checkbox-DIBarrJx.js +1 -0
  306. package/web/dist/assets/chevron-down-CPngAx4M.js +1 -0
  307. package/web/dist/assets/chevron-right-V1DGIDbL.js +1 -0
  308. package/web/dist/assets/circle-alert-Q8cZk9Rv.js +1 -0
  309. package/web/dist/assets/circle-x-C9USji_R.js +1 -0
  310. package/web/dist/assets/code-5_eJ_XpJ.js +1 -0
  311. package/web/dist/assets/code-fixer-fQtWZ5He.js +20 -0
  312. package/web/dist/assets/copy-BYjq1hdN.js +1 -0
  313. package/web/dist/assets/detail-panel-BX_hxeI0.js +3 -0
  314. package/web/dist/assets/dist-B8XUKgXe.js +1 -0
  315. package/web/dist/assets/dist-Bb9TeEub.js +1 -0
  316. package/web/dist/assets/dist-C5nMErq8.js +1 -0
  317. package/web/dist/assets/dist-C6eaYCUo.js +1 -0
  318. package/web/dist/assets/dist-Cn0y0IRn.js +1 -0
  319. package/web/dist/assets/dist-CxdY_A67.js +5 -0
  320. package/web/dist/assets/dist-D5Dj17P8.js +1 -0
  321. package/web/dist/assets/dist-DZJ9SMlJ.js +1 -0
  322. package/web/dist/assets/es2015-CfXu88Y7.js +41 -0
  323. package/web/dist/assets/estree-BP7k0NIt.js +44 -0
  324. package/web/dist/assets/file-system-access-CtcvBfj4.js +1 -0
  325. package/web/dist/assets/global-panel-shell-ChdO0dWd.js +6 -0
  326. package/web/dist/assets/health-panel-CQeCQ7gr.js +1 -0
  327. package/web/dist/assets/html-6FIWbqxX.js +25 -0
  328. package/web/dist/assets/index-CjqaTnE_.css +2 -0
  329. package/web/dist/assets/index-L7uWxTnp.js +18 -0
  330. package/web/dist/assets/mobile-proxy-panel-Dj1iLami.js +8 -0
  331. package/web/dist/assets/mock-config-BTTCw7RK.js +1 -0
  332. package/web/dist/assets/mock-editor-panel-C-aVfFme.js +1 -0
  333. package/web/dist/assets/monaco-editor-B3brLg8j.js +11 -0
  334. package/web/dist/assets/pencil-line-Cqe41E4h.js +1 -0
  335. package/web/dist/assets/plugin-code-editor-CPjCbJ9g.js +2 -0
  336. package/web/dist/assets/plugin-config-BzmCkvz3.js +1 -0
  337. package/web/dist/assets/plugin-generator-BnEafDF9.js +2 -0
  338. package/web/dist/assets/plugin-test-dialog-BWoWsWV4.js +7 -0
  339. package/web/dist/assets/plus-tVxSydJ8.js +1 -0
  340. package/web/dist/assets/postcss-Dnf30fQU.js +61 -0
  341. package/web/dist/assets/refresh-cw-CG3HaiQT.js +1 -0
  342. package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +1 -0
  343. package/web/dist/assets/route-preview-BX7sfY0O.js +1 -0
  344. package/web/dist/assets/rule-ai-assistant-panel-WBiOOUw5.js +47 -0
  345. package/web/dist/assets/rule-config-CHpVij2M.css +1 -0
  346. package/web/dist/assets/rule-config-fTIZVwg1.js +15 -0
  347. package/web/dist/assets/select-CkFOB61l.js +1 -0
  348. package/web/dist/assets/server-Cpj4KNwN.js +1 -0
  349. package/web/dist/assets/settings-panel-COCYiIUg.js +1 -0
  350. package/web/dist/assets/shield-check-OqUmVLQt.js +1 -0
  351. package/web/dist/assets/smartphone-DKeDuJTU.js +1 -0
  352. package/web/dist/assets/standalone-CjxtLKw-.js +29 -0
  353. package/web/dist/assets/switch-C-Xk56TK.js +1 -0
  354. package/web/dist/assets/syntax-highlight-DVhPdkuV.js +5 -0
  355. package/web/dist/assets/table-CT97lAYq.js +1 -0
  356. package/web/dist/assets/textarea-C6NooVK1.js +1 -0
  357. package/web/dist/assets/wand-sparkles-wkul5vje.js +1 -0
  358. package/web/dist/assets/wifi-D9M35Ajj.js +1 -0
  359. package/web/dist/assets/zap-Djo4cgux.js +1 -0
  360. package/web/dist/index.html +24 -0
  361. package/web/dist/vite.svg +1 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * meddle route update - Update a rule in a route file
3
+ */
4
+
5
+ const output = require('../../lib/output')
6
+ const { isProxyRunning } = require('../../lib/proxy-detect')
7
+ const { apiGet, apiPut } = require('../../lib/api-client')
8
+ const { getRuleFileContent, saveRuleFileContent, parseEprc, ruleMapToEprcText } = require('../../lib/file-access')
9
+
10
+ // Parse arguments (skip 'route' and 'update' from argv)
11
+ const args = process.argv.slice(4)
12
+ const jsonFlag = args.includes('--json')
13
+ output.setJsonMode(jsonFlag)
14
+
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]
20
+
21
+ async function run() {
22
+ if (!fileName) {
23
+ output.error('Missing required argument: <file>')
24
+ process.exit(1)
25
+ }
26
+ if (!pattern) {
27
+ output.error('Missing required argument: <pattern>')
28
+ process.exit(1)
29
+ }
30
+ if (!newTarget) {
31
+ output.error('Missing required argument: <target>')
32
+ process.exit(1)
33
+ }
34
+
35
+ const running = await isProxyRunning()
36
+
37
+ const pat = pattern.trim()
38
+ const tgt = newTarget.trim()
39
+
40
+ // Handle [marker] syntax for lookup
41
+ const bm = pat.match(/\[([^\]]+)\]/)
42
+ const internalKey = bm ? pat.replace(bm[0], bm[1]) : pat
43
+
44
+ if (running) {
45
+ try {
46
+ let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
47
+ 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)
57
+ 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)
67
+ } catch (e) {
68
+ // Fallback to file mode
69
+ 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)
83
+ 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 })
114
+ return
115
+ }
116
+
117
+ output.header('Route Rule Updated')
118
+ output.success(`${pat} -> ${tgt}`)
119
+ output.kv('File', fileName)
120
+ }
121
+ }
122
+
123
+ run().catch(e => {
124
+ output.error(e.message)
125
+ process.exit(1)
126
+ })
@@ -0,0 +1,166 @@
1
+ /**
2
+ * meddle session create - Spawn a new isolated proxy session.
3
+ *
4
+ * Flow:
5
+ * 1. Generate session id (label-timestamp) and allocate a free port
6
+ * 2. Ensure the session's MEDDLE_HOME directory exists
7
+ * 3. Spawn `node index.js` with MEDDLE_HOME + PORT env
8
+ * 4. Wait for the child's HTTP API to become reachable
9
+ * 5. Register in sessions.json (pid, port, meddleHome)
10
+ * 6. Print session id + proxy URL
11
+ *
12
+ * The child is spawned detached with stdio inherited so the user sees
13
+ * proxy logs. The child listens for parent disconnect and self-terminates
14
+ * to avoid orphan processes.
15
+ */
16
+
17
+ const fs = require('fs')
18
+ const path = require('path')
19
+ const { spawn } = require('child_process')
20
+ const chalk = require('chalk')
21
+
22
+ const {
23
+ sessionsDir,
24
+ sessionDir,
25
+ allocatePort,
26
+ generateId,
27
+ createSession,
28
+ readRegistry,
29
+ writeRegistry,
30
+ } = require('../../lib/sessions')
31
+ const { resolveMeddleHome } = require('../../lib/meddle-home')
32
+ const { GLOBAL_DEFAULT_PORT } = require('../../lib/session-args')
33
+
34
+ function parseArgs(argv) {
35
+ const args = argv.slice(4) // after 'meddle' 'session' 'create'
36
+ const out = { name: null, port: null, json: false }
37
+ for (let i = 0; i < args.length; i++) {
38
+ const a = args[i]
39
+ if (a === '--name' && args[i + 1]) { out.name = args[i + 1]; i++; continue }
40
+ if (a.startsWith('--name=')) { out.name = a.slice('--name='.length); continue }
41
+ if (a === '--port' && args[i + 1]) { out.port = Number(args[i + 1]); i++; continue }
42
+ if (a.startsWith('--port=')) { out.port = Number(a.slice('--port='.length)); continue }
43
+ if (a === '--json') { out.json = true; continue }
44
+ }
45
+ return out
46
+ }
47
+
48
+ function waitForProxyReady(port, timeoutMs = 8000) {
49
+ const start = Date.now()
50
+ const interval = 100
51
+ return new Promise((resolve, reject) => {
52
+ const check = () => {
53
+ fetch(`http://127.0.0.1:${port}/api/mocks`, { method: 'GET', signal: AbortSignal.timeout(500) })
54
+ .then((r) => { if (r.ok) resolve(); else retry() })
55
+ .catch(() => retry())
56
+ }
57
+ function retry() {
58
+ if (Date.now() - start > timeoutMs) {
59
+ return reject(new Error(`proxy did not become ready on port ${port} within ${timeoutMs}ms`))
60
+ }
61
+ setTimeout(check, interval)
62
+ }
63
+ check()
64
+ })
65
+ }
66
+
67
+ async function main() {
68
+ const opts = parseArgs(process.argv)
69
+
70
+ // 1. id + port
71
+ const id = generateId(opts.name)
72
+ let port = opts.port
73
+ if (port) {
74
+ // user-specified port: validate range and freeness
75
+ if (port < 9000 || port > 9999) {
76
+ throw new Error(`--port must be in [9000, 9999] (got ${port}); default session uses ${GLOBAL_DEFAULT_PORT}`)
77
+ }
78
+ } else {
79
+ const registry = readRegistry()
80
+ port = allocatePort(registry)
81
+ if (!port) throw new Error('no free port in [9000, 9999]; run `meddle session prune` to clean orphaned records')
82
+ }
83
+
84
+ // 2. MEDDLE_HOME dir
85
+ const meddleHome = sessionDir(id)
86
+ fs.mkdirSync(meddleHome, { recursive: true })
87
+ // subdirs that proxy expects to exist or create — let the child create them
88
+ // via its existing startup logic. We only ensure the root exists.
89
+
90
+ // 3. spawn child
91
+ const indexPath = path.join(__dirname, '..', '..', '..', 'index.js')
92
+ const childEnv = {
93
+ ...process.env,
94
+ MEDDLE_HOME: meddleHome,
95
+ PORT: String(port),
96
+ MEDDLE_SESSION_ID: id,
97
+ // Suppress the CA trust prompt — sessions reuse the shared CA
98
+ // from ~/.meddle/ca which the user already trusted when setting up
99
+ // the default session. Also avoid interactive prompts blocking
100
+ // a non-interactive agent spawn.
101
+ MEDDLE_HEADLESS: '1',
102
+ // Disable MCP file discovery for non-default sessions; the MCP
103
+ // proxy-url file is only meaningful for the default session.
104
+ MEDDLE_MCP: '',
105
+ }
106
+ const child = spawn(process.execPath, [indexPath], {
107
+ cwd: process.cwd(),
108
+ env: childEnv,
109
+ stdio: 'inherit',
110
+ detached: false,
111
+ })
112
+
113
+ if (!child.pid) {
114
+ throw new Error('failed to spawn proxy process')
115
+ }
116
+
117
+ // Kill child if parent exits unexpectedly (orphan prevention).
118
+ child.on('exit', (code, signal) => {
119
+ if (code !== 0 && code !== null) {
120
+ console.error(chalk.yellow(`session ${id} exited early (code=${code} signal=${signal || ''})`))
121
+ }
122
+ })
123
+ process.on('exit', () => {
124
+ try { if (child.exitCode === null) child.kill('SIGTERM') } catch (_) {}
125
+ })
126
+
127
+ // 4. wait for HTTP ready
128
+ try {
129
+ await waitForProxyReady(port)
130
+ } catch (err) {
131
+ try { child.kill('SIGTERM') } catch (_) {}
132
+ throw err
133
+ }
134
+
135
+ // 5. register (optimistic: re-read, verify port not taken, write)
136
+ const freshRegistry = readRegistry()
137
+ const portConflict = Object.keys(freshRegistry).some((k) => freshRegistry[k].port === port)
138
+ if (portConflict && !opts.port) {
139
+ try { child.kill('SIGTERM') } catch (_) {}
140
+ throw new Error(`port ${port} was claimed by another session during startup; please retry`)
141
+ }
142
+ createSession({ id, label: opts.name || '', port, pid: child.pid, meddleHome })
143
+
144
+ // 6. report
145
+ if (opts.json) {
146
+ console.log(JSON.stringify({
147
+ id,
148
+ port,
149
+ pid: child.pid,
150
+ meddleHome,
151
+ proxyUrl: `http://127.0.0.1:${port}`,
152
+ }))
153
+ } else {
154
+ console.log(chalk.green('Created session:'), chalk.cyan(id))
155
+ console.log(` ${chalk.gray('Proxy URL:')} http://127.0.0.1:${port}`)
156
+ console.log(` ${chalk.gray('PID:')} ${child.pid}`)
157
+ console.log(` ${chalk.gray('MEDDLE_HOME:')} ${meddleHome}`)
158
+ console.log(chalk.gray('\nUse this session with:'))
159
+ console.log(` meddle --session ${id} route list`)
160
+ }
161
+ }
162
+
163
+ main().catch((err) => {
164
+ console.error(chalk.red('error:'), err.message)
165
+ process.exit(1)
166
+ })
@@ -0,0 +1,69 @@
1
+ /**
2
+ * meddle session delete <id> - Stop a session and remove from registry.
3
+ *
4
+ * --clean also deletes the session's MEDDLE_HOME data directory.
5
+ */
6
+
7
+ const fs = require('fs')
8
+ const path = require('path')
9
+ const chalk = require('chalk')
10
+ const { getSession, deleteSession, isPidAlive } = require('../../lib/sessions')
11
+
12
+ const args = process.argv.slice(4)
13
+ const id = args.find((a) => !a.startsWith('--'))
14
+ const cleanFlag = args.includes('--clean')
15
+ const jsonFlag = args.includes('--json')
16
+
17
+ if (!id) {
18
+ console.error(chalk.red('error:'), 'session id required')
19
+ console.error(chalk.gray('usage: meddle session delete <id> [--clean]'))
20
+ process.exit(1)
21
+ }
22
+
23
+ const record = getSession(id)
24
+ if (!record) {
25
+ console.error(chalk.red('error:'), `session not found: ${id}`)
26
+ process.exit(1)
27
+ }
28
+
29
+ // 1. kill process if alive
30
+ let killed = false
31
+ if (isPidAlive(record.pid)) {
32
+ try {
33
+ process.kill(record.pid, 'SIGTERM')
34
+ killed = true
35
+ } catch (err) {
36
+ console.error(chalk.yellow('warning:'), `could not kill pid ${record.pid}: ${err.message}`)
37
+ }
38
+ }
39
+
40
+ // 2. remove from registry
41
+ deleteSession(id)
42
+
43
+ // 3. optionally clean data dir
44
+ let cleaned = false
45
+ if (cleanFlag) {
46
+ try {
47
+ if (fs.existsSync(record.meddleHome)) {
48
+ // only delete if inside ~/.meddle/sessions/ — safety guard
49
+ const sessionsRoot = path.dirname(record.meddleHome)
50
+ const expectedRoot = path.join(require('../../lib/meddle-home').resolveMeddleHome(), 'sessions')
51
+ if (path.resolve(sessionsRoot) === path.resolve(expectedRoot)) {
52
+ fs.rmSync(record.meddleHome, { recursive: true, force: true })
53
+ cleaned = true
54
+ } else {
55
+ console.error(chalk.yellow('warning:'), `refusing to delete ${record.meddleHome} (outside sessions dir)`)
56
+ }
57
+ }
58
+ } catch (err) {
59
+ console.error(chalk.yellow('warning:'), `could not clean data dir: ${err.message}`)
60
+ }
61
+ }
62
+
63
+ if (jsonFlag) {
64
+ console.log(JSON.stringify({ id, killed, cleaned, meddleHome: record.meddleHome }))
65
+ } else {
66
+ console.log(chalk.green('Deleted session:'), chalk.cyan(id))
67
+ if (killed) console.log(chalk.gray(` stopped pid ${record.pid}`))
68
+ if (cleaned) console.log(chalk.gray(` removed ${record.meddleHome}`))
69
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * meddle session - Multi-instance session management (preview)
3
+ *
4
+ * Each session is an independent `meddle` process with its own MEDDLE_HOME
5
+ * (route-rules, mocks, plugins, settings) and port. The CA store
6
+ * (~/.meddle/ca) is shared across all sessions so HTTPS interception
7
+ * works without re-installing certificates per session.
8
+ *
9
+ * See docs/proposals/multi-session-isolation.md for the full design.
10
+ */
11
+
12
+ const args = process.argv.slice(3)
13
+ const subcommand = args[0]
14
+
15
+ switch (subcommand) {
16
+ case 'create':
17
+ require('./create.js')
18
+ break
19
+ case 'list':
20
+ case 'ls':
21
+ require('./list.js')
22
+ break
23
+ case 'delete':
24
+ case 'rm':
25
+ require('./delete.js')
26
+ break
27
+ case 'prune':
28
+ require('./prune.js')
29
+ break
30
+ default:
31
+ console.log(`
32
+ Session Commands (multi-instance isolation, preview):
33
+
34
+ meddle session create [--name <label>] [--port <port>]
35
+ Create a new proxy session with its own
36
+ MEDDLE_HOME and port. Returns the session id.
37
+ meddle session list List all sessions with liveness check.
38
+ meddle session delete <id> [--clean]
39
+ Stop the session, remove from registry.
40
+ --clean also deletes its data directory.
41
+ meddle session prune Remove registry entries whose process is
42
+ no longer alive (keeps data directories).
43
+
44
+ Operating on a specific session:
45
+ meddle --session <id> route list
46
+ meddle --session <id> mock add --name "API" --pattern "api.test.com"
47
+ meddle --session <id> status
48
+
49
+ Notes:
50
+ - The default session (no --session) uses ~/.meddle and is fully backward
51
+ compatible. Existing commands and tests are unaffected.
52
+ - --session and the MEDDLE_HOME environment variable are mutually exclusive.
53
+ - All sessions share the same CA (~/.meddle/ca); only config is isolated.
54
+
55
+ Examples:
56
+ meddle session create --name my-debug
57
+ meddle --session my-debug-1718283600 route list
58
+ meddle session delete my-debug-1718283600 --clean
59
+ `)
60
+ process.exit(subcommand ? 1 : 0)
61
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * meddle session list - List all sessions with liveness check.
3
+ */
4
+
5
+ const chalk = require('chalk')
6
+ const { listSessions } = require('../../lib/sessions')
7
+
8
+ const args = process.argv.slice(4)
9
+ const jsonFlag = args.includes('--json')
10
+
11
+ const sessions = listSessions()
12
+
13
+ if (jsonFlag) {
14
+ console.log(JSON.stringify(sessions, null, 2))
15
+ process.exit(0)
16
+ }
17
+
18
+ if (sessions.length === 0) {
19
+ console.log(chalk.gray('No sessions registered.'))
20
+ console.log(chalk.gray('Create one with: meddle session create --name <label>'))
21
+ process.exit(0)
22
+ }
23
+
24
+ console.log(chalk.bold.cyan(`Sessions (${sessions.length})`))
25
+ console.log(chalk.gray('─'.repeat(80)))
26
+ for (const s of sessions) {
27
+ const status = s.alive ? chalk.green('● running') : chalk.red('○ orphaned')
28
+ const id = chalk.cyan(s.id)
29
+ console.log(` ${status} ${id}`)
30
+ console.log(` ${chalk.gray('port:')} ${s.port}`)
31
+ console.log(` ${chalk.gray('pid:')} ${s.pid}`)
32
+ console.log(` ${chalk.gray('meddleHome:')} ${s.meddleHome}`)
33
+ console.log(` ${chalk.gray('created:')} ${s.createdAt}`)
34
+ if (s.label) console.log(` ${chalk.gray('label:')} ${s.label}`)
35
+ console.log('')
36
+ }
37
+
38
+ const orphaned = sessions.filter((s) => !s.alive)
39
+ if (orphaned.length > 0) {
40
+ console.log(chalk.yellow(`${orphaned.length} orphaned session(s) detected.`))
41
+ console.log(chalk.gray(`Run 'meddle session prune' to clean up registry entries.`))
42
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * meddle session prune - Remove registry entries whose process is gone.
3
+ *
4
+ * Keeps the data directories intact (use `meddle session delete <id> --clean`
5
+ * for full cleanup of a specific session).
6
+ */
7
+
8
+ const chalk = require('chalk')
9
+ const { pruneOrphaned } = require('../../lib/sessions')
10
+
11
+ const args = process.argv.slice(4)
12
+ const jsonFlag = args.includes('--json')
13
+
14
+ const removed = pruneOrphaned()
15
+
16
+ if (jsonFlag) {
17
+ console.log(JSON.stringify({ pruned: removed }))
18
+ process.exit(0)
19
+ }
20
+
21
+ if (removed.length === 0) {
22
+ console.log(chalk.gray('No orphaned sessions.'))
23
+ process.exit(0)
24
+ }
25
+
26
+ console.log(chalk.green(`Pruned ${removed.length} orphaned session(s):`))
27
+ for (const s of removed) {
28
+ console.log(` ${chalk.cyan(s.id)} ${chalk.gray(`(pid ${s.pid}, port ${s.port})`)}`)
29
+ console.log(chalk.gray(` data dir kept: ${s.meddleHome}`))
30
+ }
31
+ console.log(chalk.gray('\nTo also delete data directories, run: meddle session delete <id> --clean'))
@@ -0,0 +1,43 @@
1
+ /**
2
+ * meddle start - Start proxy server
3
+ */
4
+
5
+ const { spawn } = require('child_process')
6
+ const path = require('path')
7
+
8
+ // Parse arguments
9
+ const args = process.argv.slice(3)
10
+ const openFlag = args.includes('--open')
11
+ const remoteFlag = args.includes('--remote')
12
+ const interceptHttpsFlag = args.includes('--intercept-https')
13
+ const noInterceptHttpsFlag = args.includes('--no-intercept-https')
14
+
15
+ // Start proxy
16
+ const indexPath = path.join(__dirname, '..', 'index.js')
17
+ const spawnEnv = { ...process.env, DEBUG: process.env.DEBUG || '' }
18
+ if (openFlag) spawnEnv.MEDDLE_OPEN = '1'
19
+ if (remoteFlag) spawnEnv.MEDDLE_REMOTE = '1'
20
+ if (interceptHttpsFlag) spawnEnv.MEDDLE_INTERCEPT_HTTPS = '1'
21
+ if (noInterceptHttpsFlag) spawnEnv.MEDDLE_INTERCEPT_HTTPS = '0'
22
+
23
+ const remoteTokenIndex = args.indexOf('--remote-token')
24
+ if (remoteTokenIndex >= 0 && args[remoteTokenIndex + 1]) {
25
+ spawnEnv.MEDDLE_REMOTE_TOKEN = args[remoteTokenIndex + 1]
26
+ }
27
+ const inlineRemoteToken = args.find(arg => arg.startsWith('--remote-token='))
28
+ if (inlineRemoteToken) spawnEnv.MEDDLE_REMOTE_TOKEN = inlineRemoteToken.slice('--remote-token='.length)
29
+
30
+ const child = spawn(process.execPath, [indexPath], {
31
+ env: spawnEnv,
32
+ stdio: 'inherit',
33
+ cwd: process.cwd()
34
+ })
35
+
36
+ child.on('error', (err) => {
37
+ console.error('启动代理失败:', err)
38
+ process.exit(1)
39
+ })
40
+
41
+ child.on('exit', (code) => {
42
+ process.exit(code || 0)
43
+ })
@@ -0,0 +1,115 @@
1
+ /**
2
+ * meddle status - Get proxy status
3
+ */
4
+
5
+ const { getProxyUrl, isProxyRunning } = require('../lib/proxy-detect')
6
+ const { apiGet } = require('../lib/api-client')
7
+ const { loadMockRules, listRuleFiles, getActiveRuleFileNames } = require('../lib/file-access')
8
+ const output = require('../lib/output')
9
+
10
+ // Check for --json flag
11
+ const args = process.argv.slice(2)
12
+ const jsonFlag = args.includes('--json')
13
+ output.setJsonMode(jsonFlag)
14
+
15
+ async function run() {
16
+ const url = getProxyUrl()
17
+ const running = await isProxyRunning()
18
+
19
+ let mockCount = 0
20
+ let activeMocks = 0
21
+ let routeFiles = []
22
+ let activeRoutes = 0
23
+ let health = null
24
+
25
+ if (running) {
26
+ try {
27
+ // Get mock rules from API
28
+ const mocks = await apiGet('/api/mocks')
29
+ mockCount = mocks.length
30
+ activeMocks = mocks.filter(m => m.enabled).length
31
+
32
+ // Get route files from API
33
+ routeFiles = await apiGet('/api/rule-files')
34
+ activeRoutes = routeFiles.filter(f => f.enabled).length
35
+
36
+ health = await apiGet('/api/health')
37
+ } catch (_) {
38
+ // Fallback to file access
39
+ const mocks = loadMockRules()
40
+ mockCount = mocks.length
41
+ activeMocks = mocks.filter(m => m.enabled).length
42
+
43
+ routeFiles = listRuleFiles()
44
+ activeRoutes = routeFiles.filter(f => f.enabled).length
45
+ }
46
+ } else {
47
+ // File mode
48
+ const mocks = loadMockRules()
49
+ mockCount = mocks.length
50
+ activeMocks = mocks.filter(m => m.enabled).length
51
+
52
+ routeFiles = listRuleFiles()
53
+ activeRoutes = routeFiles.filter(f => f.enabled).length
54
+ }
55
+
56
+ const totalRouteRules = routeFiles.reduce((sum, f) => sum + f.ruleCount, 0)
57
+ const totalExclusions = routeFiles.reduce((sum, f) => sum + f.excludeCount, 0)
58
+
59
+ if (output.isJsonMode()) {
60
+ output.jsonRaw({
61
+ proxyUrl: url,
62
+ running,
63
+ health,
64
+ mocks: { total: mockCount, active: activeMocks },
65
+ routes: { files: routeFiles.length, activeFiles: activeRoutes, totalRules: totalRouteRules, totalExclusions }
66
+ })
67
+ return
68
+ }
69
+
70
+ output.header('Proxy Status')
71
+ output.kv('Proxy URL', url)
72
+ output.kv('Status', running ? 'Running' : 'Not Running')
73
+ if (health) {
74
+ output.kv('Health', health.status)
75
+ output.kv('PID', health.pid)
76
+ output.kv('CPU', `${health.cpu.percent}%`)
77
+ output.kv('RSS', formatBytes(health.memory.rss))
78
+ output.kv('Connections', health.connections.total)
79
+ output.kv('MITM Servers', health.mitmServers.count)
80
+ output.kv('Watchdog', health.watchdog.config.enabled ? `${health.watchdog.config.action}, failures=${health.watchdog.consecutiveFailures}` : 'disabled')
81
+ }
82
+
83
+ output.section('Mock Rules')
84
+ output.kv('Total', mockCount)
85
+ output.kv('Active', activeMocks)
86
+
87
+ output.section('Route Rules')
88
+ output.kv('Files', routeFiles.length)
89
+ output.kv('Active Files', activeRoutes)
90
+ output.kv('Total Rules', totalRouteRules)
91
+ if (totalExclusions > 0) {
92
+ output.kv('Total Exclusions', totalExclusions)
93
+ }
94
+
95
+ if (routeFiles.length > 0) {
96
+ output.plain('')
97
+ routeFiles.forEach(f => {
98
+ const excludeInfo = f.excludeCount > 0 ? `, ${f.excludeCount} exclusions` : ''
99
+ output.bullet(`${f.name} (${f.ruleCount} rules${excludeInfo})`, f.enabled)
100
+ })
101
+ }
102
+ }
103
+
104
+ run().catch(e => {
105
+ output.error(e.message)
106
+ process.exit(1)
107
+ })
108
+
109
+ function formatBytes(bytes) {
110
+ if (!Number.isFinite(bytes)) return 'n/a'
111
+ if (bytes >= 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`
112
+ if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
113
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`
114
+ return `${bytes} B`
115
+ }