@jonathanleelx/meddle 0.5.0-beta.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIG_STRUCTURE.md +9 -2
- package/README.md +18 -0
- package/bin/commands/mock/add.js +37 -21
- package/bin/commands/mock/delete.js +16 -24
- package/bin/commands/mock/index.js +8 -4
- 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 +85 -51
- package/bin/commands/route/index.js +20 -2
- package/bin/commands/route/list.js +2 -2
- package/bin/commands/route/show.js +10 -1
- package/bin/commands/route/update.js +135 -92
- package/bin/commands/update.js +4 -0
- package/bin/index.js +7 -3
- package/bin/lib/api-client.js +14 -2
- package/bin/lib/file-access.js +217 -35
- package/bin/lib/output.js +24 -3
- package/bin/lib/proxy-detect.js +99 -20
- package/bin/lib/proxy-or-file.js +90 -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.map +1 -1
- package/dist/cert.js +43 -0
- package/dist/cert.js.map +1 -1
- package/dist/core/browser.d.ts +8 -1
- package/dist/core/browser.d.ts.map +1 -1
- package/dist/core/browser.js +239 -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 +59 -0
- package/dist/core/mock-handler.d.ts.map +1 -1
- package/dist/core/mock-handler.js +627 -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/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 +13 -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 +101 -9
- 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 +15 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -1
- 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 +395 -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 +36 -7
- package/mcp-server.js +90 -88
- 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-BtgWWeu3.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-5cJneeis.js → global-panel-shell-NaUhzX7J.js} +3 -3
- package/web/dist/assets/health-panel-DhXLqcOU.js +1 -0
- package/web/dist/assets/index-B-6nUrqW.js +21 -0
- package/web/dist/assets/index-EWNlAdFB.css +2 -0
- package/web/dist/assets/mobile-proxy-panel-BY8Oi-qC.js +8 -0
- package/web/dist/assets/mock-config-TPmfJwAt.js +1 -0
- package/web/dist/assets/mock-editor-panel-WQwcHSTD.js +20 -0
- package/web/dist/assets/mocks-file-missing-note-BR5oWFLh.js +1 -0
- package/web/dist/assets/monaco-editor-BGLPrb0C.js +11 -0
- package/web/dist/assets/pane-bar-gFRISZLD.js +1 -0
- package/web/dist/assets/pane-split-B6aLmY7G.js +1 -0
- package/web/dist/assets/panel-heading-DT_nXayM.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--Gf4ZwGb.js +2 -0
- package/web/dist/assets/plugin-config-DsX9xCW_.js +2 -0
- package/web/dist/assets/plugin-generator-Bj4f6dxo.js +2 -0
- package/web/dist/assets/plugin-test-dialog-DICuykBK.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-C-LzS1lT.js +1 -0
- package/web/dist/assets/{rule-ai-assistant-panel-DMh0F9dr.js → rule-ai-assistant-panel-BEfPNBbx.js} +2 -2
- package/web/dist/assets/rule-config-Dw9pcuah.js +15 -0
- package/web/dist/assets/rule-overview-panel-C4QQ7U3S.js +1 -0
- package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
- package/web/dist/assets/settings-panel-D8DEH8aH.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/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/mobile-proxy-panel-tU0NyJ3P.js +0 -8
- 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/output.js
CHANGED
|
@@ -51,11 +51,13 @@ 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 this is a no-op; the caller prints the
|
|
56
|
+
* `{ success: false }` object on stdout.
|
|
55
57
|
*/
|
|
56
58
|
function error(message) {
|
|
57
59
|
if (jsonMode) return
|
|
58
|
-
console.
|
|
60
|
+
console.error(chalk.red(' ✗'), message)
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/**
|
|
@@ -128,6 +130,23 @@ function tableRow(columns, widths) {
|
|
|
128
130
|
console.log(' ' + row)
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Shared downgrade warning when CLI mutates config but cannot reach a live proxy
|
|
135
|
+
* (issue #85). Same wording for mock + route families.
|
|
136
|
+
*/
|
|
137
|
+
const FILE_ONLY_WARNING =
|
|
138
|
+
'Config written to file, but could not connect to a running proxy — restart required for changes to take effect.'
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Emit the file-only downgrade warning. Always prints to stderr (including
|
|
142
|
+
* --json mode) so the failure mode is never silent.
|
|
143
|
+
* @returns {string} the warning message
|
|
144
|
+
*/
|
|
145
|
+
function warnFileOnlyMode() {
|
|
146
|
+
console.error(chalk.yellow(' ⚠'), FILE_ONLY_WARNING)
|
|
147
|
+
return FILE_ONLY_WARNING
|
|
148
|
+
}
|
|
149
|
+
|
|
131
150
|
module.exports = {
|
|
132
151
|
setJsonMode,
|
|
133
152
|
isJsonMode,
|
|
@@ -142,5 +161,7 @@ module.exports = {
|
|
|
142
161
|
plain,
|
|
143
162
|
kv,
|
|
144
163
|
bullet,
|
|
145
|
-
tableRow
|
|
164
|
+
tableRow,
|
|
165
|
+
FILE_ONLY_WARNING,
|
|
166
|
+
warnFileOnlyMode,
|
|
146
167
|
}
|
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
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a proxy API failure may fall back to a direct file write.
|
|
3
|
+
*
|
|
4
|
+
* Fall back only when the proxy is not running, or the request fails before
|
|
5
|
+
* any HTTP response (connection refused, timeout, DNS). A 4xx/5xx is the
|
|
6
|
+
* proxy's answer: callers print that body and must not touch the file.
|
|
7
|
+
* Treating a 409 (corrupt mocks.json) as "proxy down" rewrote the file and
|
|
8
|
+
* dropped every existing rule.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const output = require('./output')
|
|
12
|
+
const { isProxyRunning } = require('./proxy-detect')
|
|
13
|
+
|
|
14
|
+
const UNREACHABLE_CODES = new Set([
|
|
15
|
+
'ECONNREFUSED',
|
|
16
|
+
'ECONNRESET',
|
|
17
|
+
'ENOTFOUND',
|
|
18
|
+
'EAI_AGAIN',
|
|
19
|
+
'EHOSTUNREACH',
|
|
20
|
+
'ENETUNREACH',
|
|
21
|
+
'EPIPE',
|
|
22
|
+
'ETIMEDOUT',
|
|
23
|
+
'ECONNABORTED',
|
|
24
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
25
|
+
'UND_ERR_SOCKET',
|
|
26
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
function causeCode(err) {
|
|
30
|
+
if (!err || typeof err !== 'object') return undefined
|
|
31
|
+
if (err.code && err.code !== 'MOCKS_FILE_INVALID') return err.code
|
|
32
|
+
if (err.cause) return causeCode(err.cause)
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when the proxy answered with an HTTP 4xx/5xx (api-client sets err.status). */
|
|
37
|
+
function isApiHttpError(err) {
|
|
38
|
+
const status = err && err.status
|
|
39
|
+
return Number.isInteger(status) && status >= 400 && status <= 599
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* True when we never got an HTTP response: connection refused, DNS, reset,
|
|
44
|
+
* timeout, or undici's `TypeError: fetch failed`.
|
|
45
|
+
*/
|
|
46
|
+
function isProxyUnreachableError(err) {
|
|
47
|
+
if (!err || isApiHttpError(err)) return false
|
|
48
|
+
if (err.name === 'AbortError' || err.name === 'TimeoutError') return true
|
|
49
|
+
const code = causeCode(err)
|
|
50
|
+
if (code && UNREACHABLE_CODES.has(code)) return true
|
|
51
|
+
if (err.name === 'TypeError' && err.message === 'fetch failed') return true
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Call the proxy when it is up. `file` runs only when it is down or the
|
|
57
|
+
* request never received an HTTP response. HTTP errors reject.
|
|
58
|
+
*
|
|
59
|
+
* @param {{ api: () => Promise<any>, file: () => any }} opts
|
|
60
|
+
* @returns {Promise<{ fileOnly: boolean, value: any }>}
|
|
61
|
+
*/
|
|
62
|
+
async function withProxyOrFile({ api, file }) {
|
|
63
|
+
const running = await isProxyRunning()
|
|
64
|
+
if (!running) return { fileOnly: true, value: file() }
|
|
65
|
+
try {
|
|
66
|
+
return { fileOnly: false, value: await api() }
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (!isProxyUnreachableError(err)) throw err
|
|
69
|
+
return { fileOnly: true, value: file() }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Exit 1. An HTTP error's message is the response body and is printed as-is.
|
|
75
|
+
* `err.rawStderr` prints `err.message` with no `✗` prefix (mock list).
|
|
76
|
+
* Other human-mode failures use the CLI `✗` style.
|
|
77
|
+
*/
|
|
78
|
+
function failCommand(err) {
|
|
79
|
+
const message = err && err.message ? String(err.message) : String(err)
|
|
80
|
+
if (isApiHttpError(err) || (err && err.rawStderr) || output.isJsonMode()) console.error(message)
|
|
81
|
+
else output.error(message)
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
isApiHttpError,
|
|
87
|
+
isProxyUnreachableError,
|
|
88
|
+
withProxyOrFile,
|
|
89
|
+
failCommand,
|
|
90
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing for `meddle route add|update|delete` (issue #149).
|
|
3
|
+
*
|
|
4
|
+
* `--exclusions=` clears a rule's exclusions. `--exclusions=/a,/b` replaces
|
|
5
|
+
* them. A bare `--exclusions`, an unknown flag, or an empty `!` token used to
|
|
6
|
+
* be ignored and the command still exited 0 without changing exclusions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { splitPatternArg } = require('./rule-file-edit')
|
|
10
|
+
|
|
11
|
+
class RouteArgError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message)
|
|
14
|
+
this.name = 'RouteArgError'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CLEAR_HINT = 'To clear exclusions, pass --exclusions= (empty). To replace them, pass --exclusions=/a,/b.'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} raw value after `=`
|
|
22
|
+
* @returns {string[]}
|
|
23
|
+
*/
|
|
24
|
+
function parseExclusionList(raw) {
|
|
25
|
+
const value = String(raw).trim()
|
|
26
|
+
if (value === '') return []
|
|
27
|
+
const pieces = value.includes(',') ? value.split(',') : value.split(/\s+/)
|
|
28
|
+
const out = []
|
|
29
|
+
for (const piece of pieces) {
|
|
30
|
+
const token = piece.trim()
|
|
31
|
+
if (!token) {
|
|
32
|
+
throw new RouteArgError(`Invalid --exclusions: empty entry. ${CLEAR_HINT}`)
|
|
33
|
+
}
|
|
34
|
+
const body = token.startsWith('!') ? token.slice(1).trim() : token
|
|
35
|
+
if (!body) {
|
|
36
|
+
throw new RouteArgError(`Invalid --exclusions: "!" needs a path, for example --exclusions=/health. ${CLEAR_HINT}`)
|
|
37
|
+
}
|
|
38
|
+
out.push(body)
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string[]} args argv after the subcommand
|
|
45
|
+
* @param {{ allowExclusions?: boolean }} [options]
|
|
46
|
+
* @returns {{ help: boolean, json: boolean, positional: string[], exclusions: string[] | undefined }}
|
|
47
|
+
* `exclusions` is undefined when the flag was not passed. `[]` means clear.
|
|
48
|
+
*/
|
|
49
|
+
function parseRouteWriteArgs(args, { allowExclusions = true } = {}) {
|
|
50
|
+
let help = false
|
|
51
|
+
let json = false
|
|
52
|
+
/** @type {string[] | undefined} */
|
|
53
|
+
let exclusions
|
|
54
|
+
let sawExclusions = false
|
|
55
|
+
const positional = []
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < args.length; i++) {
|
|
58
|
+
const arg = args[i]
|
|
59
|
+
if (arg === '--help' || arg === '-h') {
|
|
60
|
+
help = true
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (arg === '--json') {
|
|
64
|
+
json = true
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (arg === '--exclusions' || arg.startsWith('--exclusions=')) {
|
|
68
|
+
if (!allowExclusions) {
|
|
69
|
+
throw new RouteArgError(`Unsupported option: ${arg === '--exclusions' ? '--exclusions' : '--exclusions='}. route delete does not change exclusions.`)
|
|
70
|
+
}
|
|
71
|
+
if (arg === '--exclusions') {
|
|
72
|
+
throw new RouteArgError(`Unsupported option: --exclusions. ${CLEAR_HINT}`)
|
|
73
|
+
}
|
|
74
|
+
if (sawExclusions) throw new RouteArgError('--exclusions was given more than once.')
|
|
75
|
+
sawExclusions = true
|
|
76
|
+
exclusions = parseExclusionList(arg.slice('--exclusions='.length))
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (arg.startsWith('-')) {
|
|
80
|
+
throw new RouteArgError(`Unsupported option: ${arg}.`)
|
|
81
|
+
}
|
|
82
|
+
positional.push(arg)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { help, json, positional, exclusions: sawExclusions ? exclusions : undefined }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Split a pattern argument the way the CLI always has (`"host !/api"`), but
|
|
90
|
+
* refuse a bare `!` — that used to be dropped and looked like a successful clear.
|
|
91
|
+
* @returns {{ patterns: string[], exclusions: string[] }}
|
|
92
|
+
*/
|
|
93
|
+
function parsePatternArg(raw) {
|
|
94
|
+
const parts = String(raw).trim().split(/\s+/).filter(Boolean)
|
|
95
|
+
for (const part of parts) {
|
|
96
|
+
if (part.startsWith('!') && part.slice(1).trim() === '') {
|
|
97
|
+
throw new RouteArgError('Invalid exclusion "!": a "!" token needs a path such as "!/api". To clear exclusions, pass --exclusions=.')
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return splitPatternArg(raw)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string[]} patternExclusions exclusions parsed from !path tokens
|
|
105
|
+
* @param {string[] | undefined} flagExclusions undefined when --exclusions was omitted
|
|
106
|
+
* @returns {{ explicit: boolean, exclusions: string[] | undefined }}
|
|
107
|
+
*/
|
|
108
|
+
function resolveExclusions(patternExclusions, flagExclusions) {
|
|
109
|
+
if (flagExclusions !== undefined && patternExclusions.length > 0) {
|
|
110
|
+
throw new RouteArgError('Pass exclusions either as !path tokens in the pattern or with --exclusions, not both.')
|
|
111
|
+
}
|
|
112
|
+
if (flagExclusions !== undefined) return { explicit: true, exclusions: flagExclusions }
|
|
113
|
+
if (patternExclusions.length > 0) return { explicit: true, exclusions: patternExclusions }
|
|
114
|
+
return { explicit: false, exclusions: undefined }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const UPDATE_HELP = `
|
|
118
|
+
meddle route update <file> <pattern> [<target>] [--exclusions=<list>] [--json]
|
|
119
|
+
|
|
120
|
+
Update a rule. Edits only the line(s) that hold the pattern; every other line
|
|
121
|
+
stays byte-for-byte, including comments, blank lines and other rules' !path.
|
|
122
|
+
|
|
123
|
+
--exclusions= Clear this rule's exclusions (the line loses !path).
|
|
124
|
+
--exclusions=/a,/b Replace exclusions. Spaces work inside quotes
|
|
125
|
+
(--exclusions="/a /b"). A leading ! is optional.
|
|
126
|
+
"pattern !/a !/b" Replace exclusions (same as before). Omit both
|
|
127
|
+
--exclusions and !path to keep existing exclusions.
|
|
128
|
+
|
|
129
|
+
The <target> may be omitted when --exclusions is set; the current target stays.
|
|
130
|
+
A bare --exclusions, an empty "!" token, an unknown flag, or both exclusion
|
|
131
|
+
forms at once exits 1 and does not change the file.
|
|
132
|
+
|
|
133
|
+
A "#" comments out a whole line only when it is the first non-space character.
|
|
134
|
+
A "#" later on a rule line is a pattern token, not a trailing comment.
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
meddle route update dev "api.example.com" "localhost:4000"
|
|
138
|
+
meddle route update dev "api.example.com" --exclusions=
|
|
139
|
+
meddle route update dev "api.example.com" "localhost:4000" --exclusions=/health
|
|
140
|
+
`
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
CLEAR_HINT,
|
|
144
|
+
RouteArgError,
|
|
145
|
+
UPDATE_HELP,
|
|
146
|
+
parseExclusionList,
|
|
147
|
+
parsePatternArg,
|
|
148
|
+
parseRouteWriteArgs,
|
|
149
|
+
resolveExclusions,
|
|
150
|
+
}
|