@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.
- package/CONFIG_STRUCTURE.md +9 -2
- package/README.md +18 -0
- package/bin/commands/mock/add.js +59 -21
- package/bin/commands/mock/delete.js +16 -24
- package/bin/commands/mock/index.js +59 -25
- package/bin/commands/mock/list.js +35 -18
- package/bin/commands/mock/toggle.js +17 -27
- package/bin/commands/mock/update.js +39 -30
- package/bin/commands/route/active.js +20 -3
- package/bin/commands/route/add.js +113 -94
- package/bin/commands/route/create.js +8 -1
- package/bin/commands/route/delete.js +87 -53
- package/bin/commands/route/index.js +74 -26
- package/bin/commands/route/list.js +2 -2
- package/bin/commands/route/preview.js +2 -2
- package/bin/commands/route/show.js +12 -3
- package/bin/commands/route/update.js +135 -92
- package/bin/commands/status.js +27 -11
- package/bin/commands/update.js +4 -0
- package/bin/doctor.js +8 -4
- package/bin/index.js +11 -7
- package/bin/lib/api-client.js +14 -2
- package/bin/lib/file-access.js +234 -35
- package/bin/lib/meddle-home.js +14 -1
- package/bin/lib/output.js +30 -4
- package/bin/lib/proxy-detect.js +99 -20
- package/bin/lib/proxy-or-file.js +93 -0
- package/bin/lib/route-write-args.js +150 -0
- package/bin/lib/rule-file-edit.js +263 -0
- package/bin/lib/ruleset-readonly.js +61 -0
- package/bin/lib/update-check.js +67 -6
- package/dist/cert.d.ts +2 -0
- package/dist/cert.d.ts.map +1 -1
- package/dist/cert.js +53 -1
- package/dist/cert.js.map +1 -1
- package/dist/core/browser.d.ts +10 -1
- package/dist/core/browser.d.ts.map +1 -1
- package/dist/core/browser.js +252 -36
- package/dist/core/browser.js.map +1 -1
- package/dist/core/config-diagnostics.d.ts.map +1 -1
- package/dist/core/config-diagnostics.js +6 -1
- package/dist/core/config-diagnostics.js.map +1 -1
- package/dist/core/custom-plugin-loader.d.ts.map +1 -1
- package/dist/core/custom-plugin-loader.js +2 -0
- package/dist/core/custom-plugin-loader.js.map +1 -1
- package/dist/core/mock-handler.d.ts +63 -0
- package/dist/core/mock-handler.d.ts.map +1 -1
- package/dist/core/mock-handler.js +641 -29
- package/dist/core/mock-handler.js.map +1 -1
- package/dist/core/mock-utils.d.ts +58 -0
- package/dist/core/mock-utils.d.ts.map +1 -0
- package/dist/core/mock-utils.js +167 -0
- package/dist/core/mock-utils.js.map +1 -0
- package/dist/core/plugin-bootstrap-runner.d.ts +1 -0
- package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -1
- package/dist/core/plugin-bootstrap-runner.js +37 -1
- package/dist/core/plugin-bootstrap-runner.js.map +1 -1
- package/dist/core/plugin-source.d.ts +24 -0
- package/dist/core/plugin-source.d.ts.map +1 -0
- package/dist/core/plugin-source.js +47 -0
- package/dist/core/plugin-source.js.map +1 -0
- package/dist/core/proxy-record.d.ts +14 -0
- package/dist/core/proxy-record.d.ts.map +1 -1
- package/dist/core/proxy-record.js +39 -0
- package/dist/core/proxy-record.js.map +1 -1
- package/dist/core/rule-file-edit.d.ts +81 -0
- package/dist/core/rule-file-edit.d.ts.map +1 -0
- package/dist/core/rule-file-edit.js +243 -0
- package/dist/core/rule-file-edit.js.map +1 -0
- package/dist/core/static-server.d.ts.map +1 -1
- package/dist/core/static-server.js +15 -13
- package/dist/core/static-server.js.map +1 -1
- package/dist/core/types.d.ts +15 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -1
- package/dist/plugins/builtin/mock-plugin.js +6 -5
- package/dist/plugins/builtin/mock-plugin.js.map +1 -1
- package/dist/server/agent/tools.d.ts.map +1 -1
- package/dist/server/agent/tools.js +24 -18
- package/dist/server/agent/tools.js.map +1 -1
- package/dist/server/app-info.d.ts +33 -0
- package/dist/server/app-info.d.ts.map +1 -0
- package/dist/server/app-info.js +116 -0
- package/dist/server/app-info.js.map +1 -0
- package/dist/server/config.d.ts +1 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +114 -16
- package/dist/server/config.js.map +1 -1
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js +8 -0
- package/dist/server/health.js.map +1 -1
- package/dist/server/index.d.ts +16 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +10 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/json-parse-error.d.ts +7 -0
- package/dist/server/json-parse-error.d.ts.map +1 -0
- package/dist/server/json-parse-error.js +28 -0
- package/dist/server/json-parse-error.js.map +1 -0
- package/dist/server/mocks.d.ts +26 -0
- package/dist/server/mocks.d.ts.map +1 -1
- package/dist/server/mocks.js +110 -13
- package/dist/server/mocks.js.map +1 -1
- package/dist/server/plugins.d.ts.map +1 -1
- package/dist/server/plugins.js +56 -11
- package/dist/server/plugins.js.map +1 -1
- package/dist/server/rule-files.d.ts +36 -0
- package/dist/server/rule-files.d.ts.map +1 -1
- package/dist/server/rule-files.js +388 -107
- package/dist/server/rule-files.js.map +1 -1
- package/dist/server/ruleset-lock.d.ts +62 -0
- package/dist/server/ruleset-lock.d.ts.map +1 -0
- package/dist/server/ruleset-lock.js +234 -0
- package/dist/server/ruleset-lock.js.map +1 -0
- package/index.js +128 -16
- package/mcp-server.js +108 -93
- package/package.json +1 -1
- package/plugins/builtin/mock-plugin.ts +9 -5
- package/web/dist/assets/alert-B514Frte.js +1 -0
- package/web/dist/assets/arrow-right-BNum_060.js +1 -0
- package/web/dist/assets/{body-diff-view-B-4XWmL6.js → body-diff-view-SfLCLzs9.js} +2 -2
- package/web/dist/assets/button-BFNlGZ5p.js +1 -0
- package/web/dist/assets/check-BkfHQ6re.js +1 -0
- package/web/dist/assets/checkbox-CQyFsZzS.js +1 -0
- package/web/dist/assets/chevron-down-DHwUtL4O.js +1 -0
- package/web/dist/assets/{circle-alert-Q8cZk9Rv.js → circle-alert-zMagrFuI.js} +1 -1
- package/web/dist/assets/{circle-x-C9USji_R.js → circle-x-BEkinhez.js} +1 -1
- package/web/dist/assets/code-DD-AYxxu.js +1 -0
- package/web/dist/assets/{copy-BYjq1hdN.js → copy-BRnscYy1.js} +1 -1
- package/web/dist/assets/detail-panel-DZRKJG2W.js +4 -0
- package/web/dist/assets/dist-BQN0rzUS.js +1 -0
- package/web/dist/assets/{dist-CEd_wnRC.js → dist-C-E2R2jk.js} +1 -1
- package/web/dist/assets/{dist-C6eaYCUo.js → dist-CFZ4xMSF.js} +1 -1
- package/web/dist/assets/{dist-DWmDaXVN.js → dist-CRmX_NvH.js} +1 -1
- package/web/dist/assets/{dist-C8RHq5At.js → dist-DLQKRjuW.js} +1 -1
- package/web/dist/assets/{dist-Blyulj4_.js → dist-LiAcI8CK.js} +1 -1
- package/web/dist/assets/{dist-Ce4ujolc.js → dist-fzPL-8F9.js} +1 -1
- package/web/dist/assets/{dist-DoE7uybN.js → dist-wv9ebJK_.js} +1 -1
- package/web/dist/assets/editor-pane-actions-_ckwcCII.js +1 -0
- package/web/dist/assets/{es2015-DG46NGN7.js → es2015-DPJTBYa9.js} +1 -1
- package/web/dist/assets/global-panel-shell-Iir-c_pQ.js +6 -0
- package/web/dist/assets/health-panel-Dmf-EhJz.js +1 -0
- package/web/dist/assets/index-C4dIj1nT.js +21 -0
- package/web/dist/assets/index-CdYMl1M7.css +2 -0
- package/web/dist/assets/{mobile-proxy-panel-tU0NyJ3P.js → mobile-proxy-panel-CFcwa11A.js} +6 -6
- package/web/dist/assets/mock-config-CGl50MIr.js +1 -0
- package/web/dist/assets/mock-editor-panel-xcCk23oH.js +20 -0
- package/web/dist/assets/mocks-file-missing-note-CW_ONVLA.js +1 -0
- package/web/dist/assets/monaco-editor-BkP7envQ.js +11 -0
- package/web/dist/assets/pane-bar-5j9Nf7W_.js +1 -0
- package/web/dist/assets/pane-split-C0OSe365.js +1 -0
- package/web/dist/assets/panel-heading-izT5q0ZN.js +1 -0
- package/web/dist/assets/{pencil-line-Cqe41E4h.js → pencil-line-CEZ7D-de.js} +1 -1
- package/web/dist/assets/plugin-code-editor-DKE9dFEE.js +2 -0
- package/web/dist/assets/plugin-config-Cg09TsuV.js +2 -0
- package/web/dist/assets/plugin-generator-CT1f1tu0.js +2 -0
- package/web/dist/assets/plugin-test-dialog-BbLIMnkJ.js +7 -0
- package/web/dist/assets/plus-ixZQRprm.js +1 -0
- package/web/dist/assets/{refresh-cw-CG3HaiQT.js → refresh-cw-BfLoHqg5.js} +1 -1
- package/web/dist/assets/rotate-ccw-CRguJya4.js +1 -0
- package/web/dist/assets/route-preview-CKO7nF4W.js +1 -0
- package/web/dist/assets/{rule-ai-assistant-panel-DMh0F9dr.js → rule-ai-assistant-panel-CtfUqdYG.js} +2 -2
- package/web/dist/assets/rule-config-Cb4ejIXI.js +15 -0
- package/web/dist/assets/rule-overview-panel-xtHTS27O.js +1 -0
- package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
- package/web/dist/assets/settings-panel-CpQuE6nr.js +1 -0
- package/web/dist/assets/{shield-check-OqUmVLQt.js → shield-check-hQXjdfZ7.js} +1 -1
- package/web/dist/assets/smartphone-D4Q7cQfm.js +1 -0
- package/web/dist/assets/switch-94vgsMHm.js +1 -0
- package/web/dist/assets/{syntax-highlight-Dk0-ewLI.js → syntax-highlight-BFgN_C0Y.js} +1 -1
- package/web/dist/assets/{table-2-BR5e3z8m.js → table-2-DvGHhMm4.js} +1 -1
- package/web/dist/assets/table-DaXD6jJv.js +1 -0
- package/web/dist/assets/textarea-9wB8Hsz-.js +1 -0
- package/web/dist/assets/toast-9yeTf7d9.js +1 -0
- package/web/dist/assets/use-mocks-file-status-BUIUJ4oQ.js +1 -0
- package/web/dist/assets/{wand-sparkles-CJ7fSnom.js → wand-sparkles-Bmt4dDOf.js} +1 -1
- package/web/dist/assets/{wifi-CCLKyySQ.js → wifi-Cjj_Br1u.js} +1 -1
- package/web/dist/assets/{zap-DR-xiUMe.js → zap-BuOtogAv.js} +1 -1
- package/web/dist/index.html +16 -10
- package/web/dist/assets/alert-DCvlZAiv.js +0 -1
- package/web/dist/assets/arrow-right-B4Mrlj2-.js +0 -1
- package/web/dist/assets/button-BTToMaBW.js +0 -1
- package/web/dist/assets/checkbox-D1Z9DtjF.js +0 -1
- package/web/dist/assets/chevron-down-CPngAx4M.js +0 -1
- package/web/dist/assets/chevron-right-V1DGIDbL.js +0 -1
- package/web/dist/assets/code-5_eJ_XpJ.js +0 -1
- package/web/dist/assets/code-fixer-D48sCDtp.js +0 -20
- package/web/dist/assets/detail-panel-BSFz9CUw.js +0 -4
- package/web/dist/assets/file-system-access-_og-Tg9o.js +0 -1
- package/web/dist/assets/global-panel-shell-5cJneeis.js +0 -6
- package/web/dist/assets/health-panel-BS3L2e5d.js +0 -1
- package/web/dist/assets/index-CvTR1IVi.js +0 -18
- package/web/dist/assets/index-bfV7sxx5.css +0 -2
- package/web/dist/assets/mock-config-CULJP6Vt.js +0 -1
- package/web/dist/assets/mock-editor-panel-9wlDaySA.js +0 -1
- package/web/dist/assets/monaco-editor-BttaUoOz.js +0 -11
- package/web/dist/assets/plugin-code-editor-BAIBp4lg.js +0 -2
- package/web/dist/assets/plugin-config-ButUV2SZ.js +0 -1
- package/web/dist/assets/plugin-generator-uceUPSCv.js +0 -2
- package/web/dist/assets/plugin-test-dialog-BrJ4HfRX.js +0 -7
- package/web/dist/assets/plus-tVxSydJ8.js +0 -1
- package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +0 -1
- package/web/dist/assets/route-preview-C4cGrS1-.js +0 -1
- package/web/dist/assets/rule-config-BTTGNwMs.js +0 -15
- package/web/dist/assets/rule-overview-panel-SFR2OloL.js +0 -1
- package/web/dist/assets/select-aBXQ0zNa.js +0 -1
- package/web/dist/assets/settings-panel-BMReDG8b.js +0 -1
- package/web/dist/assets/smartphone-DKeDuJTU.js +0 -1
- package/web/dist/assets/switch-Bes_YiJu.js +0 -1
- package/web/dist/assets/table-nW7uQVcl.js +0 -1
- package/web/dist/assets/textarea-BSI0rnW6.js +0 -1
- /package/web/dist/assets/{babel-DCr4VA1c.js → babel-bwZm0qi3.js} +0 -0
- /package/web/dist/assets/{clipboard-VhPXNN5m.js → clipboard-Bt1qY01V.js} +0 -0
- /package/web/dist/assets/{estree-qO_m2Qaw.js → estree-MatbZDwq.js} +0 -0
- /package/web/dist/assets/{html-VDGahtyk.js → html-CCrsbFYX.js} +0 -0
- /package/web/dist/assets/{postcss-BUm8EKMw.js → postcss-BxN7Gr8n.js} +0 -0
- /package/web/dist/assets/{standalone-Bj8SSd8Q.js → standalone-DsnFnsqM.js} +0 -0
package/bin/lib/file-access.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require('fs')
|
|
6
6
|
const path = require('path')
|
|
7
7
|
const { meddleDir } = require('./proxy-detect')
|
|
8
|
+
const { RulesetReadonlyError } = require('./ruleset-readonly')
|
|
8
9
|
|
|
9
10
|
const routeRulesDir = path.join(meddleDir, 'route-rules')
|
|
10
11
|
const settingsPath = path.join(meddleDir, 'settings.json')
|
|
@@ -51,6 +52,33 @@ function setActiveRuleFileNames(names) {
|
|
|
51
52
|
saveSettings(settings)
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Issue #137: locked rule files (settings.json `readonlyRuleFiles`). Entries
|
|
57
|
+
* whose file no longer exists are dropped silently. A missing field = none locked.
|
|
58
|
+
*/
|
|
59
|
+
function getReadonlyRuleFileNames() {
|
|
60
|
+
const settings = loadSettings()
|
|
61
|
+
if (!Object.prototype.hasOwnProperty.call(settings, 'readonlyRuleFiles')) return []
|
|
62
|
+
const raw = settings.readonlyRuleFiles
|
|
63
|
+
const names = Array.isArray(raw) ? raw.filter((n, i, arr) => typeof n === 'string' && n && arr.indexOf(n) === i) : []
|
|
64
|
+
const live = names.filter((name) => fs.existsSync(path.join(routeRulesDir, `${name}.txt`)))
|
|
65
|
+
if (!Array.isArray(raw) || live.length !== raw.length) {
|
|
66
|
+
settings.readonlyRuleFiles = live
|
|
67
|
+
saveSettings(settings)
|
|
68
|
+
}
|
|
69
|
+
return live
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isRuleFileReadonly(name) {
|
|
73
|
+
const fold = (n) => (process.platform === 'darwin' || process.platform === 'win32' ? n.toLowerCase() : n)
|
|
74
|
+
return getReadonlyRuleFileNames().some((item) => fold(item) === fold(name))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Throws RulesetReadonlyError (code RULESET_READONLY) for a locked rule file. */
|
|
78
|
+
function assertRuleFileWritable(name) {
|
|
79
|
+
if (isRuleFileReadonly(name)) throw new RulesetReadonlyError(name)
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
// ========== Mocks ==========
|
|
55
83
|
|
|
56
84
|
/**
|
|
@@ -62,36 +90,180 @@ function getMocksPath() {
|
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
/**
|
|
65
|
-
*
|
|
93
|
+
* Offset of the first JSON syntax error. Kept in sync with
|
|
94
|
+
* core/mock-handler.ts findJsonErrorOffset (V8 often omits a position).
|
|
66
95
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
function findJsonErrorOffset(t) {
|
|
97
|
+
let i = 0
|
|
98
|
+
const ws = () => { while (i < t.length && ' \t\n\r'.includes(t[i])) i++ }
|
|
99
|
+
const fail = () => { throw i }
|
|
100
|
+
const str = () => {
|
|
101
|
+
i++
|
|
102
|
+
while (i < t.length && t[i] !== '"') {
|
|
103
|
+
if (t[i] === '\\') i++
|
|
104
|
+
else if (t.charCodeAt(i) < 0x20) fail()
|
|
105
|
+
i++
|
|
73
106
|
}
|
|
74
|
-
|
|
75
|
-
|
|
107
|
+
if (i >= t.length) fail()
|
|
108
|
+
i++
|
|
109
|
+
}
|
|
110
|
+
const value = () => {
|
|
111
|
+
ws()
|
|
112
|
+
const c = t[i]
|
|
113
|
+
if (c === '{') {
|
|
114
|
+
i++; ws()
|
|
115
|
+
if (t[i] === '}') { i++; return }
|
|
116
|
+
for (;;) {
|
|
117
|
+
ws(); if (t[i] !== '"') fail(); str(); ws()
|
|
118
|
+
if (t[i] !== ':') fail(); i++; value(); ws()
|
|
119
|
+
if (t[i] === ',') { i++; continue }
|
|
120
|
+
if (t[i] === '}') { i++; return }
|
|
121
|
+
fail()
|
|
122
|
+
}
|
|
123
|
+
} else if (c === '[') {
|
|
124
|
+
i++; ws()
|
|
125
|
+
if (t[i] === ']') { i++; return }
|
|
126
|
+
for (;;) {
|
|
127
|
+
value(); ws()
|
|
128
|
+
if (t[i] === ',') { i++; continue }
|
|
129
|
+
if (t[i] === ']') { i++; return }
|
|
130
|
+
fail()
|
|
131
|
+
}
|
|
132
|
+
} else if (c === '"') str()
|
|
133
|
+
else if (/[-0-9]/.test(c || '')) {
|
|
134
|
+
const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(t.slice(i))
|
|
135
|
+
if (!m) fail(); i += m[0].length
|
|
136
|
+
} else {
|
|
137
|
+
for (const w of ['true', 'false', 'null']) if (t.startsWith(w, i)) { i += w.length; return }
|
|
138
|
+
fail()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
value(); ws()
|
|
143
|
+
return i < t.length ? i : t.length
|
|
144
|
+
} catch (at) {
|
|
145
|
+
return typeof at === 'number' ? Math.min(at, t.length) : t.length
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** `第 <line> 行第 <column> 列:<short message>`, same rules as core/mock-handler.ts. */
|
|
150
|
+
function describeMocksParseFailure(text, err) {
|
|
151
|
+
const message = String((err && err.message) || err)
|
|
152
|
+
const body = text.replace(/^\uFEFF/, '')
|
|
153
|
+
let line = null
|
|
154
|
+
let column = null
|
|
155
|
+
const lc = /\(line (\d+) column (\d+)\)/.exec(message)
|
|
156
|
+
const pos = /at position (\d+)/.exec(message)
|
|
157
|
+
const offset = lc ? null : pos ? Number(pos[1]) : findJsonErrorOffset(body)
|
|
158
|
+
if (lc) {
|
|
159
|
+
line = Number(lc[1]); column = Number(lc[2])
|
|
160
|
+
} else {
|
|
161
|
+
const before = body.slice(0, offset == null ? body.length : offset).split('\n')
|
|
162
|
+
line = before.length
|
|
163
|
+
column = before[before.length - 1].length + 1
|
|
164
|
+
}
|
|
165
|
+
const short = message.replace(/\s*\(line \d+ column \d+\)/, '').split(/,\s*(?:\.\.\.)?"/s)[0].replace(/\s+in JSON at position \d+/, '').trim()
|
|
166
|
+
const loc = line != null ? `第 ${line} 行第 ${column} 列:` : ''
|
|
167
|
+
return `${loc}${short}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const MOCKS_MISSING_RULES_MESSAGE = '缺少 rules 数组'
|
|
171
|
+
|
|
172
|
+
function mocksDocumentShapeError(data) {
|
|
173
|
+
if (!data || typeof data !== 'object' || Array.isArray(data) || !Array.isArray(data.rules)) {
|
|
174
|
+
return MOCKS_MISSING_RULES_MESSAGE
|
|
175
|
+
}
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function throwMocksFileInvalid(mocksPath, detail) {
|
|
180
|
+
const error = new Error(`Mock 文件格式错误,已禁止写入以免覆盖: ${mocksPath} ${detail}`)
|
|
181
|
+
error.code = 'MOCKS_FILE_INVALID'
|
|
182
|
+
error.filePath = mocksPath
|
|
183
|
+
error.detail = detail
|
|
184
|
+
error.listMessage = `Mock 文件格式错误: ${mocksPath} ${detail}`
|
|
185
|
+
throw error
|
|
76
186
|
}
|
|
77
187
|
|
|
78
188
|
/**
|
|
79
|
-
*
|
|
189
|
+
* Read mocks.json. Missing file → null. Empty / whitespace → {}.
|
|
190
|
+
* Invalid JSON or a valid document with the wrong shape (issue #160, e.g. a
|
|
191
|
+
* top-level array) throws MOCKS_FILE_INVALID and must not be treated as [].
|
|
192
|
+
* `message` refuses a write; `listMessage` is what `mock list` prints.
|
|
80
193
|
*/
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
194
|
+
function readMocksDocument(mocksPath) {
|
|
195
|
+
if (!fs.existsSync(mocksPath)) return null
|
|
196
|
+
const text = fs.readFileSync(mocksPath, 'utf8').replace(/^\uFEFF/, '')
|
|
197
|
+
if (text.trim() === '') return {}
|
|
198
|
+
let data
|
|
199
|
+
try {
|
|
200
|
+
data = JSON.parse(text)
|
|
201
|
+
} catch (err) {
|
|
202
|
+
throwMocksFileInvalid(mocksPath, describeMocksParseFailure(text, err))
|
|
203
|
+
}
|
|
204
|
+
const shape = mocksDocumentShapeError(data)
|
|
205
|
+
if (shape) throwMocksFileInvalid(mocksPath, shape)
|
|
206
|
+
return data
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function rulesFromDocument(data) {
|
|
210
|
+
if (!data || typeof data !== 'object' || Array.isArray(data) || !Array.isArray(data.rules)) return []
|
|
211
|
+
return data.rules
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function metaFromDocument(data) {
|
|
215
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return { nextId: 0, lastId: 0 }
|
|
216
|
+
return { nextId: Number(data.nextId) || 0, lastId: Number(data.lastId) || 0 }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Load mock rules from file. A corrupt file throws; it is not an empty list.
|
|
221
|
+
*/
|
|
222
|
+
function loadMockRules() {
|
|
223
|
+
const data = readMocksDocument(getMocksPath())
|
|
224
|
+
if (data == null) return []
|
|
225
|
+
return rulesFromDocument(data)
|
|
226
|
+
}
|
|
85
227
|
|
|
86
|
-
|
|
228
|
+
function readMocksFileMeta(mocksPath) {
|
|
229
|
+
const data = readMocksDocument(mocksPath)
|
|
230
|
+
if (data == null) return { nextId: 0, lastId: 0 }
|
|
231
|
+
return metaFromDocument(data)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function maxRuleId(rules) {
|
|
87
235
|
let maxId = 0
|
|
88
236
|
rules.forEach(r => {
|
|
89
|
-
if (r.id > maxId) maxId = r.id
|
|
237
|
+
if (r && Number.isInteger(r.id) && r.id > maxId) maxId = r.id
|
|
90
238
|
})
|
|
239
|
+
return maxId
|
|
240
|
+
}
|
|
91
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Next id to hand out. Shares the server's `nextId` field so ids stay
|
|
244
|
+
* monotonic across CLI file-mode and the running proxy (issue #115); also
|
|
245
|
+
* honours the legacy CLI `lastId` field.
|
|
246
|
+
*/
|
|
247
|
+
function nextMockId(rules, meta) {
|
|
248
|
+
return Math.max(1, meta.nextId || 0, (meta.lastId || 0) + 1, maxRuleId(rules) + 1)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Save mock rules to file
|
|
253
|
+
*/
|
|
254
|
+
function saveMockRules(rules, nextId) {
|
|
255
|
+
const mocksPath = getMocksPath()
|
|
256
|
+
const dir = path.dirname(mocksPath)
|
|
257
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
258
|
+
|
|
259
|
+
const meta = readMocksFileMeta(mocksPath)
|
|
260
|
+
const seq = Math.max(nextId || 0, nextMockId(rules, meta))
|
|
92
261
|
const data = {
|
|
262
|
+
// `nextId` is what the proxy reads; keep it so deleting the newest rule
|
|
263
|
+
// does not make its id reusable (issue #115). `lastId` kept for older CLIs.
|
|
264
|
+
nextId: seq,
|
|
93
265
|
rules,
|
|
94
|
-
lastId:
|
|
266
|
+
lastId: seq - 1
|
|
95
267
|
}
|
|
96
268
|
fs.writeFileSync(mocksPath, JSON.stringify(data, null, 2), 'utf8')
|
|
97
269
|
}
|
|
@@ -103,23 +275,14 @@ function addMockRule(rule) {
|
|
|
103
275
|
const rules = loadMockRules()
|
|
104
276
|
const mocksPath = getMocksPath()
|
|
105
277
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (fs.existsSync(mocksPath)) {
|
|
110
|
-
const data = JSON.parse(fs.readFileSync(mocksPath, 'utf8'))
|
|
111
|
-
nextId = (data.lastId || 0) + 1
|
|
112
|
-
}
|
|
113
|
-
} catch (_) {}
|
|
114
|
-
if (rules.length > 0) {
|
|
115
|
-
const maxId = Math.max(...rules.map(r => r.id))
|
|
116
|
-
if (maxId >= nextId) nextId = maxId + 1
|
|
117
|
-
}
|
|
278
|
+
// Issue #115: never reuse an existing (or previously handed-out) id, and
|
|
279
|
+
// ignore any id the caller put on the rule — add never overwrites.
|
|
280
|
+
const nextId = nextMockId(rules, readMocksFileMeta(mocksPath))
|
|
118
281
|
|
|
119
|
-
rule
|
|
120
|
-
rules.push(
|
|
121
|
-
saveMockRules(rules)
|
|
122
|
-
return
|
|
282
|
+
const created = { ...rule, id: nextId }
|
|
283
|
+
rules.push(created)
|
|
284
|
+
saveMockRules(rules, nextId + 1)
|
|
285
|
+
return created
|
|
123
286
|
}
|
|
124
287
|
|
|
125
288
|
/**
|
|
@@ -167,6 +330,30 @@ function getRuleFilePath(name) {
|
|
|
167
330
|
/**
|
|
168
331
|
* List all rule files
|
|
169
332
|
*/
|
|
333
|
+
function getRuleFileOrderNames() {
|
|
334
|
+
const settings = loadSettings()
|
|
335
|
+
const arr = settings.ruleFileOrder
|
|
336
|
+
return Array.isArray(arr) ? arr.filter((name) => typeof name === 'string') : []
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function resolveRuleFileOrder(diskNames, storedOrder, activeNames) {
|
|
340
|
+
const diskSet = new Set(diskNames)
|
|
341
|
+
const ordered = []
|
|
342
|
+
const seen = new Set()
|
|
343
|
+
const pushUnique = (name) => {
|
|
344
|
+
if (!diskSet.has(name) || seen.has(name)) return
|
|
345
|
+
ordered.push(name)
|
|
346
|
+
seen.add(name)
|
|
347
|
+
}
|
|
348
|
+
if (storedOrder.length > 0) {
|
|
349
|
+
for (const name of storedOrder) pushUnique(name)
|
|
350
|
+
} else {
|
|
351
|
+
for (const name of activeNames) pushUnique(name)
|
|
352
|
+
}
|
|
353
|
+
for (const name of diskNames) pushUnique(name)
|
|
354
|
+
return ordered
|
|
355
|
+
}
|
|
356
|
+
|
|
170
357
|
function listRuleFiles() {
|
|
171
358
|
ensureRouteRulesDir()
|
|
172
359
|
const activeNames = getActiveRuleFileNames()
|
|
@@ -175,7 +362,10 @@ function listRuleFiles() {
|
|
|
175
362
|
.filter(f => f.endsWith('.txt'))
|
|
176
363
|
.map(f => f.replace(/\.txt$/, ''))
|
|
177
364
|
|
|
178
|
-
|
|
365
|
+
const ordered = resolveRuleFileOrder(files, getRuleFileOrderNames(), activeNames)
|
|
366
|
+
const readonlyNames = new Set(getReadonlyRuleFileNames())
|
|
367
|
+
|
|
368
|
+
return ordered.map(name => {
|
|
179
369
|
const filePath = getRuleFilePath(name)
|
|
180
370
|
let ruleCount = 0
|
|
181
371
|
let excludeCount = 0
|
|
@@ -191,6 +381,7 @@ function listRuleFiles() {
|
|
|
191
381
|
return {
|
|
192
382
|
name,
|
|
193
383
|
enabled: activeNames.includes(name),
|
|
384
|
+
readonly: readonlyNames.has(name),
|
|
194
385
|
ruleCount,
|
|
195
386
|
excludeCount
|
|
196
387
|
}
|
|
@@ -215,6 +406,7 @@ function getRuleFileContent(name) {
|
|
|
215
406
|
*/
|
|
216
407
|
function saveRuleFileContent(name, content) {
|
|
217
408
|
ensureRouteRulesDir()
|
|
409
|
+
assertRuleFileWritable(name)
|
|
218
410
|
const filePath = getRuleFilePath(name)
|
|
219
411
|
fs.writeFileSync(filePath, content, 'utf8')
|
|
220
412
|
}
|
|
@@ -231,6 +423,8 @@ function createRuleFile(name, content = '', enabled = true) {
|
|
|
231
423
|
throw new Error(`规则文件 "${safeName}" 已存在`)
|
|
232
424
|
}
|
|
233
425
|
|
|
426
|
+
// Drop stale lock entries so a new file never inherits an old lock.
|
|
427
|
+
getReadonlyRuleFileNames()
|
|
234
428
|
fs.writeFileSync(filePath, content, 'utf8')
|
|
235
429
|
|
|
236
430
|
if (enabled) {
|
|
@@ -242,7 +436,7 @@ function createRuleFile(name, content = '', enabled = true) {
|
|
|
242
436
|
}
|
|
243
437
|
|
|
244
438
|
const ruleCount = content.trim() ? Object.keys(parseEprc(content)).length : 0
|
|
245
|
-
return { name: safeName, enabled, ruleCount }
|
|
439
|
+
return { name: safeName, enabled, readonly: false, ruleCount }
|
|
246
440
|
}
|
|
247
441
|
|
|
248
442
|
/**
|
|
@@ -253,6 +447,7 @@ function deleteRuleFile(name) {
|
|
|
253
447
|
if (!fs.existsSync(filePath)) {
|
|
254
448
|
throw new Error('规则文件不存在')
|
|
255
449
|
}
|
|
450
|
+
assertRuleFileWritable(name)
|
|
256
451
|
fs.unlinkSync(filePath)
|
|
257
452
|
|
|
258
453
|
const activeNames = getActiveRuleFileNames()
|
|
@@ -274,6 +469,7 @@ function setRuleFileEnabled(name, enabled) {
|
|
|
274
469
|
|
|
275
470
|
const activeNames = getActiveRuleFileNames()
|
|
276
471
|
const idx = activeNames.indexOf(name)
|
|
472
|
+
if ((enabled && idx === -1) || (!enabled && idx !== -1)) assertRuleFileWritable(name)
|
|
277
473
|
|
|
278
474
|
if (enabled && idx === -1) {
|
|
279
475
|
activeNames.push(name)
|
|
@@ -309,6 +505,9 @@ module.exports = {
|
|
|
309
505
|
saveSettings,
|
|
310
506
|
getActiveRuleFileNames,
|
|
311
507
|
setActiveRuleFileNames,
|
|
508
|
+
getReadonlyRuleFileNames,
|
|
509
|
+
isRuleFileReadonly,
|
|
510
|
+
assertRuleFileWritable,
|
|
312
511
|
|
|
313
512
|
// Mocks
|
|
314
513
|
getMocksPath,
|
package/bin/lib/meddle-home.js
CHANGED
|
@@ -17,4 +17,17 @@ function resolveMeddleHome() {
|
|
|
17
17
|
return path.resolve(os.homedir(), '.meddle')
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Same rules as core/mock-handler.resolveConfiguredMocksPath: `~` expands to
|
|
22
|
+
* the user home directory; a relative path is resolved against MEDDLE_HOME.
|
|
23
|
+
*/
|
|
24
|
+
function resolveConfiguredMocksPath(raw, meddleDir, homeDir = os.homedir()) {
|
|
25
|
+
const v = typeof raw === 'string' ? raw.trim() : ''
|
|
26
|
+
if (!v) return null
|
|
27
|
+
let p = v
|
|
28
|
+
if (p === '~') p = homeDir
|
|
29
|
+
else if (p.startsWith('~/') || p.startsWith('~\\')) p = path.join(homeDir, p.slice(2))
|
|
30
|
+
return path.resolve(meddleDir, p)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { resolveMeddleHome, resolveConfiguredMocksPath }
|
package/bin/lib/output.js
CHANGED
|
@@ -51,11 +51,18 @@ function success(message) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Print
|
|
54
|
+
* Print a command failure. Human-readable text goes to stderr so stdout stays
|
|
55
|
+
* empty on error. In --json mode stdout is one `{ success: false }` object and
|
|
56
|
+
* the same message is copied to stderr without a `✗` prefix.
|
|
55
57
|
*/
|
|
56
58
|
function error(message) {
|
|
57
|
-
if (jsonMode)
|
|
58
|
-
|
|
59
|
+
if (jsonMode) {
|
|
60
|
+
const text = String(message)
|
|
61
|
+
console.log(JSON.stringify({ success: false, error: { message: text } }))
|
|
62
|
+
console.error(text)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
console.error(chalk.red(' ✗'), message)
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -128,6 +135,23 @@ function tableRow(columns, widths) {
|
|
|
128
135
|
console.log(' ' + row)
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Shared downgrade warning when CLI mutates config but cannot reach a live proxy
|
|
140
|
+
* (issue #85). Same wording for mock + route families.
|
|
141
|
+
*/
|
|
142
|
+
const FILE_ONLY_WARNING =
|
|
143
|
+
'Config written to file, but could not connect to a running proxy — restart required for changes to take effect.'
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Emit the file-only downgrade warning. Always prints to stderr (including
|
|
147
|
+
* --json mode) so the failure mode is never silent.
|
|
148
|
+
* @returns {string} the warning message
|
|
149
|
+
*/
|
|
150
|
+
function warnFileOnlyMode() {
|
|
151
|
+
console.error(chalk.yellow(' ⚠'), FILE_ONLY_WARNING)
|
|
152
|
+
return FILE_ONLY_WARNING
|
|
153
|
+
}
|
|
154
|
+
|
|
131
155
|
module.exports = {
|
|
132
156
|
setJsonMode,
|
|
133
157
|
isJsonMode,
|
|
@@ -142,5 +166,7 @@ module.exports = {
|
|
|
142
166
|
plain,
|
|
143
167
|
kv,
|
|
144
168
|
bullet,
|
|
145
|
-
tableRow
|
|
169
|
+
tableRow,
|
|
170
|
+
FILE_ONLY_WARNING,
|
|
171
|
+
warnFileOnlyMode,
|
|
146
172
|
}
|
package/bin/lib/proxy-detect.js
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* every CLI command session-aware without individual changes: when
|
|
7
7
|
* --session <id> is set, MEDDLE_HOME is pinned to the session's data dir
|
|
8
8
|
* and getProxyUrl() returns that session's port.
|
|
9
|
+
*
|
|
10
|
+
* Probe priority (issue #85):
|
|
11
|
+
* 1. MEDDLE_SESSION_PORT (pinned session)
|
|
12
|
+
* 2. mcp-proxy-url.json (written by proxy on every start)
|
|
13
|
+
* 3. DEFAULT_PROXY_BASE (http://127.0.0.1:8989)
|
|
14
|
+
*
|
|
15
|
+
* Stale ports in mcp-proxy-url.json must not stick forever: isProxyRunning /
|
|
16
|
+
* resolveLiveProxyUrl fall through to later candidates when a probe fails,
|
|
17
|
+
* and cache the first live URL for subsequent getProxyUrl() / API calls.
|
|
9
18
|
*/
|
|
10
19
|
|
|
11
20
|
const fs = require('fs')
|
|
@@ -16,39 +25,61 @@ const { applySessionContext, GLOBAL_DEFAULT_PORT } = require('./session-args')
|
|
|
16
25
|
// Apply --session / MEDDLE_HOME precedence before computing paths.
|
|
17
26
|
applySessionContext()
|
|
18
27
|
|
|
28
|
+
// Resolved once for CLI (MEDDLE_HOME is set before this module loads).
|
|
19
29
|
const meddleDir = resolveMeddleHome()
|
|
20
30
|
const mcpFile = path.join(meddleDir, 'mcp-proxy-url.json')
|
|
21
31
|
const DEFAULT_PROXY_BASE = `http://127.0.0.1:${GLOBAL_DEFAULT_PORT}`
|
|
22
32
|
|
|
33
|
+
/** @type {string | null} */
|
|
34
|
+
let resolvedProxyUrl = null
|
|
35
|
+
|
|
36
|
+
function getMcpFilePath() {
|
|
37
|
+
// Prefer live MEDDLE_HOME so tests can point at a temp home after require.
|
|
38
|
+
const home = (process.env.MEDDLE_HOME || '').trim() || meddleDir
|
|
39
|
+
return path.join(home, 'mcp-proxy-url.json')
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Priority:
|
|
27
|
-
* 1. MEDDLE_SESSION_PORT env (set by --session resolution)
|
|
28
|
-
* 2. mcp-proxy-url.json (written by proxy on startup)
|
|
29
|
-
* 3. DEFAULT_PROXY_BASE (http://127.0.0.1:8989)
|
|
43
|
+
* Sync best-effort URL (no liveness check). Prefer a previously resolved live
|
|
44
|
+
* URL so API calls after isProxyRunning() hit the working port.
|
|
30
45
|
*/
|
|
31
46
|
function getProxyUrl() {
|
|
47
|
+
if (resolvedProxyUrl) return resolvedProxyUrl
|
|
48
|
+
const candidates = getCandidateProxyUrls()
|
|
49
|
+
return candidates[0] || DEFAULT_PROXY_BASE
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Ordered candidate URLs to probe (deduped).
|
|
54
|
+
* Session pin is exclusive — do not fall back to global file/default.
|
|
55
|
+
*/
|
|
56
|
+
function getCandidateProxyUrls() {
|
|
32
57
|
if (process.env.MEDDLE_SESSION_PORT) {
|
|
33
|
-
return `http://127.0.0.1:${process.env.MEDDLE_SESSION_PORT}`
|
|
58
|
+
return [`http://127.0.0.1:${process.env.MEDDLE_SESSION_PORT}`]
|
|
34
59
|
}
|
|
60
|
+
const urls = []
|
|
35
61
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
const file = getMcpFilePath()
|
|
63
|
+
if (fs.existsSync(file)) {
|
|
64
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
65
|
+
if (data.proxyUrl && typeof data.proxyUrl === 'string') {
|
|
66
|
+
urls.push(data.proxyUrl.replace(/\/$/, ''))
|
|
67
|
+
}
|
|
39
68
|
}
|
|
40
69
|
} catch (_) {}
|
|
41
|
-
|
|
70
|
+
if (!urls.includes(DEFAULT_PROXY_BASE)) {
|
|
71
|
+
urls.push(DEFAULT_PROXY_BASE)
|
|
72
|
+
}
|
|
73
|
+
return urls
|
|
42
74
|
}
|
|
43
75
|
|
|
44
76
|
/**
|
|
45
|
-
*
|
|
46
|
-
* @param {number} timeoutMs
|
|
77
|
+
* @param {string} url
|
|
78
|
+
* @param {number} timeoutMs
|
|
47
79
|
*/
|
|
48
|
-
async function
|
|
49
|
-
const url = getProxyUrl()
|
|
80
|
+
async function probeUrl(url, timeoutMs = 2000) {
|
|
50
81
|
try {
|
|
51
|
-
const response = await fetch(url + '/api/mocks', {
|
|
82
|
+
const response = await fetch(url.replace(/\/$/, '') + '/api/mocks', {
|
|
52
83
|
method: 'GET',
|
|
53
84
|
signal: AbortSignal.timeout(timeoutMs)
|
|
54
85
|
})
|
|
@@ -58,6 +89,38 @@ async function isProxyRunning(timeoutMs = 2000) {
|
|
|
58
89
|
}
|
|
59
90
|
}
|
|
60
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Probe candidates in order; cache and return the first live URL, or null.
|
|
94
|
+
* @param {number} timeoutMs
|
|
95
|
+
* @param {(url: string, timeoutMs: number) => Promise<boolean>} [probe]
|
|
96
|
+
*/
|
|
97
|
+
async function resolveLiveProxyUrl(timeoutMs = 2000, probe = probeUrl) {
|
|
98
|
+
if (resolvedProxyUrl) {
|
|
99
|
+
const perTry = Math.min(timeoutMs, 800)
|
|
100
|
+
if (await probe(resolvedProxyUrl, perTry)) return resolvedProxyUrl
|
|
101
|
+
resolvedProxyUrl = null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const candidates = getCandidateProxyUrls()
|
|
105
|
+
const perTry = Math.max(200, Math.floor(timeoutMs / Math.max(1, candidates.length)))
|
|
106
|
+
for (const url of candidates) {
|
|
107
|
+
if (await probe(url, perTry)) {
|
|
108
|
+
resolvedProxyUrl = url
|
|
109
|
+
return url
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if proxy is running by probing candidates (file → default fallback).
|
|
117
|
+
* @param {number} timeoutMs - Timeout in milliseconds (default 2000)
|
|
118
|
+
*/
|
|
119
|
+
async function isProxyRunning(timeoutMs = 2000) {
|
|
120
|
+
const url = await resolveLiveProxyUrl(timeoutMs)
|
|
121
|
+
return !!url
|
|
122
|
+
}
|
|
123
|
+
|
|
61
124
|
/**
|
|
62
125
|
* Wait for proxy to start
|
|
63
126
|
* @param {number} timeoutMs - Timeout in milliseconds (default 5000)
|
|
@@ -69,15 +132,15 @@ function waitForProxyUrl(timeoutMs = 5000) {
|
|
|
69
132
|
const check = () => {
|
|
70
133
|
try {
|
|
71
134
|
if (process.env.MEDDLE_SESSION_PORT) {
|
|
72
|
-
// For --session mode, poll the HTTP endpoint directly.
|
|
73
135
|
const port = process.env.MEDDLE_SESSION_PORT
|
|
74
136
|
fetch(`http://127.0.0.1:${port}/api/mocks`, { method: 'GET', signal: AbortSignal.timeout(500) })
|
|
75
137
|
.then((r) => { if (r.ok) resolve(`http://127.0.0.1:${port}`); else retry() })
|
|
76
138
|
.catch(() => retry())
|
|
77
139
|
return
|
|
78
140
|
}
|
|
79
|
-
|
|
80
|
-
|
|
141
|
+
const file = getMcpFilePath()
|
|
142
|
+
if (fs.existsSync(file)) {
|
|
143
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
81
144
|
if (data.proxyUrl) {
|
|
82
145
|
return resolve(data.proxyUrl)
|
|
83
146
|
}
|
|
@@ -95,11 +158,27 @@ function waitForProxyUrl(timeoutMs = 5000) {
|
|
|
95
158
|
})
|
|
96
159
|
}
|
|
97
160
|
|
|
161
|
+
/** Test-only: clear cached live URL so the next probe re-reads candidates. */
|
|
162
|
+
function __resetResolvedProxyUrlForTest() {
|
|
163
|
+
resolvedProxyUrl = null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Test-only: seed the cached live URL. */
|
|
167
|
+
function __setResolvedProxyUrlForTest(url) {
|
|
168
|
+
resolvedProxyUrl = url || null
|
|
169
|
+
}
|
|
170
|
+
|
|
98
171
|
module.exports = {
|
|
99
172
|
meddleDir,
|
|
100
173
|
mcpFile,
|
|
101
174
|
DEFAULT_PROXY_BASE,
|
|
102
175
|
getProxyUrl,
|
|
176
|
+
getCandidateProxyUrls,
|
|
177
|
+
getMcpFilePath,
|
|
178
|
+
resolveLiveProxyUrl,
|
|
179
|
+
probeUrl,
|
|
103
180
|
isProxyRunning,
|
|
104
|
-
waitForProxyUrl
|
|
181
|
+
waitForProxyUrl,
|
|
182
|
+
__resetResolvedProxyUrlForTest,
|
|
183
|
+
__setResolvedProxyUrlForTest,
|
|
105
184
|
}
|