@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
|
@@ -5,123 +5,142 @@
|
|
|
5
5
|
const output = require('../../lib/output')
|
|
6
6
|
const { isProxyRunning } = require('../../lib/proxy-detect')
|
|
7
7
|
const { apiGet, apiPut } = require('../../lib/api-client')
|
|
8
|
-
const { getRuleFileContent, saveRuleFileContent
|
|
8
|
+
const { getRuleFileContent, saveRuleFileContent } = require('../../lib/file-access')
|
|
9
|
+
const { addRouteRule } = require('../../lib/rule-file-edit')
|
|
10
|
+
const { isRulesetReadonlyError, exitRulesetReadonly } = require('../../lib/ruleset-readonly')
|
|
11
|
+
const { parseRouteWriteArgs, parsePatternArg, resolveExclusions } = require('../../lib/route-write-args')
|
|
12
|
+
|
|
13
|
+
const ADD_HELP = `
|
|
14
|
+
meddle route add <file> <pattern> <target> [--exclusions=<list>] [--json]
|
|
15
|
+
|
|
16
|
+
Add a rule. If the pattern already exists, its target is overwritten in place.
|
|
17
|
+
--exclusions= clears that rule's exclusions; --exclusions=/a,/b replaces them.
|
|
18
|
+
Omit --exclusions and any !path in the pattern to keep existing exclusions.
|
|
19
|
+
See: meddle route update --help
|
|
20
|
+
`
|
|
9
21
|
|
|
10
|
-
// Parse arguments (skip 'route' and 'add' from argv)
|
|
11
22
|
const args = process.argv.slice(4)
|
|
12
|
-
|
|
23
|
+
|
|
24
|
+
function fail(message, jsonFlag) {
|
|
25
|
+
if (jsonFlag) {
|
|
26
|
+
console.log(JSON.stringify({ success: false, error: { message } }))
|
|
27
|
+
console.error(message)
|
|
28
|
+
} else {
|
|
29
|
+
output.setJsonMode(false)
|
|
30
|
+
output.error(message)
|
|
31
|
+
}
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let parsed
|
|
36
|
+
try {
|
|
37
|
+
parsed = parseRouteWriteArgs(args)
|
|
38
|
+
} catch (e) {
|
|
39
|
+
fail(e.message, args.includes('--json'))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (parsed.help) {
|
|
43
|
+
console.log(ADD_HELP.trim())
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const jsonFlag = parsed.json
|
|
13
48
|
output.setJsonMode(jsonFlag)
|
|
14
49
|
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const pattern = positionalArgs[1]
|
|
19
|
-
const target = positionalArgs[2]
|
|
50
|
+
const fileName = parsed.positional[0]
|
|
51
|
+
const pattern = parsed.positional[1]
|
|
52
|
+
const target = parsed.positional[2]
|
|
20
53
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
54
|
+
let explicitExclusions = false
|
|
55
|
+
let appliedExclusions = []
|
|
56
|
+
|
|
57
|
+
function planAdd() {
|
|
58
|
+
const { patterns, exclusions: patternExclusions } = parsePatternArg(pattern)
|
|
59
|
+
if (patterns.length === 0) fail('Missing required argument: <pattern>', jsonFlag)
|
|
60
|
+
const resolved = resolveExclusions(patternExclusions, parsed.exclusions)
|
|
61
|
+
explicitExclusions = resolved.explicit
|
|
62
|
+
appliedExclusions = resolved.exclusions || []
|
|
63
|
+
return { patterns, exclusions: resolved.explicit ? resolved.exclusions : undefined }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Issue #145: edit only the line(s) holding this pattern; comments, blank
|
|
68
|
+
* lines and other rules (with their `!exclusions`) keep their exact bytes.
|
|
69
|
+
* Exclusions may be given inside the pattern argument ("app.example.com !/api")
|
|
70
|
+
* or with --exclusions. `--exclusions=` clears them (issue #149).
|
|
71
|
+
*/
|
|
72
|
+
function applyAdd(text, plan) {
|
|
73
|
+
let next = text
|
|
74
|
+
for (const p of plan.patterns) {
|
|
75
|
+
next = addRouteRule(next, { pattern: p, target: target.trim(), exclusions: plan.exclusions }).content
|
|
25
76
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
77
|
+
return next
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function emitAdded({ fileOnly }) {
|
|
81
|
+
const payload = { success: true, file: fileName, pattern: pattern.trim(), target: target.trim() }
|
|
82
|
+
if (explicitExclusions) payload.exclusions = appliedExclusions
|
|
83
|
+
if (fileOnly) {
|
|
84
|
+
payload.fileOnly = true
|
|
85
|
+
payload.warning = output.FILE_ONLY_WARNING
|
|
29
86
|
}
|
|
30
|
-
if (
|
|
31
|
-
output.
|
|
32
|
-
|
|
87
|
+
if (jsonFlag) {
|
|
88
|
+
output.jsonRaw(payload)
|
|
89
|
+
return
|
|
33
90
|
}
|
|
91
|
+
output.header('Route Rule Added')
|
|
92
|
+
output.success(`${pattern.trim()} -> ${target.trim()}`)
|
|
93
|
+
if (explicitExclusions) {
|
|
94
|
+
output.kv('Exclusions', appliedExclusions.length ? appliedExclusions.map((e) => `!${e}`).join(' ') : 'cleared')
|
|
95
|
+
}
|
|
96
|
+
output.kv('File', fileName)
|
|
97
|
+
}
|
|
34
98
|
|
|
35
|
-
|
|
99
|
+
async function run() {
|
|
100
|
+
if (!fileName) fail('Missing required argument: <file>', jsonFlag)
|
|
101
|
+
if (!pattern) fail('Missing required argument: <pattern>', jsonFlag)
|
|
102
|
+
if (!target) fail('Missing required argument: <target>', jsonFlag)
|
|
103
|
+
if (parsed.positional.length > 3) fail(`Unexpected argument: ${parsed.positional[3]}`, jsonFlag)
|
|
104
|
+
|
|
105
|
+
let plan
|
|
106
|
+
try {
|
|
107
|
+
plan = planAdd()
|
|
108
|
+
} catch (e) {
|
|
109
|
+
fail(e.message, jsonFlag)
|
|
110
|
+
}
|
|
36
111
|
|
|
37
|
-
const
|
|
38
|
-
const tgt = target.trim()
|
|
112
|
+
const running = await isProxyRunning()
|
|
39
113
|
|
|
40
114
|
if (running) {
|
|
41
115
|
try {
|
|
42
|
-
// Get current content
|
|
43
116
|
let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
|
|
44
117
|
const text = typeof content === 'string' ? content : String(content)
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
// Handle [marker] syntax
|
|
48
|
-
const bm = pat.match(/\[([^\]]+)\]/)
|
|
49
|
-
if (bm) {
|
|
50
|
-
ruleMap[pat.replace(bm[0], bm[1])] = tgt + bm[0]
|
|
51
|
-
} else {
|
|
52
|
-
ruleMap[pat] = tgt
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
118
|
+
const newContent = applyAdd(text, plan)
|
|
56
119
|
await apiPut(`/api/rule-files/${encodeURIComponent(fileName)}/content`, { content: newContent })
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt })
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
output.header('Route Rule Added')
|
|
64
|
-
output.success(`${pat} -> ${tgt}`)
|
|
65
|
-
output.kv('File', fileName)
|
|
120
|
+
emitAdded({ fileOnly: false })
|
|
121
|
+
return
|
|
66
122
|
} catch (e) {
|
|
67
|
-
//
|
|
123
|
+
// Issue #137: a locked ruleset is a final answer, never a cue to write the file directly.
|
|
124
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
68
125
|
const content = getRuleFileContent(fileName)
|
|
69
|
-
if (!content) {
|
|
70
|
-
|
|
71
|
-
process.exit(1)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const ruleMap = parseEprc(content)
|
|
75
|
-
const bm = pat.match(/\[([^\]]+)\]/)
|
|
76
|
-
if (bm) {
|
|
77
|
-
ruleMap[pat.replace(bm[0], bm[1])] = tgt + bm[0]
|
|
78
|
-
} else {
|
|
79
|
-
ruleMap[pat] = tgt
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
126
|
+
if (!content) fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
127
|
+
const newContent = applyAdd(content, plan)
|
|
83
128
|
saveRuleFileContent(fileName, newContent)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt })
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
output.header('Route Rule Added')
|
|
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
|
-
const bm = pat.match(/\[([^\]]+)\]/)
|
|
104
|
-
if (bm) {
|
|
105
|
-
ruleMap[pat.replace(bm[0], bm[1])] = tgt + bm[0]
|
|
106
|
-
} else {
|
|
107
|
-
ruleMap[pat] = tgt
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
111
|
-
saveRuleFileContent(fileName, newContent)
|
|
112
|
-
|
|
113
|
-
if (jsonFlag) {
|
|
114
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt })
|
|
129
|
+
output.warnFileOnlyMode()
|
|
130
|
+
emitAdded({ fileOnly: true })
|
|
115
131
|
return
|
|
116
132
|
}
|
|
117
|
-
|
|
118
|
-
output.header('Route Rule Added')
|
|
119
|
-
output.success(`${pat} -> ${tgt}`)
|
|
120
|
-
output.kv('File', fileName)
|
|
121
133
|
}
|
|
134
|
+
|
|
135
|
+
const content = getRuleFileContent(fileName)
|
|
136
|
+
if (!content) fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
137
|
+
const newContent = applyAdd(content, plan)
|
|
138
|
+
saveRuleFileContent(fileName, newContent)
|
|
139
|
+
output.warnFileOnlyMode()
|
|
140
|
+
emitAdded({ fileOnly: true })
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
run().catch(e => {
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
145
|
+
fail(e.message, jsonFlag)
|
|
127
146
|
})
|
|
@@ -34,6 +34,7 @@ async function run() {
|
|
|
34
34
|
const running = await isProxyRunning()
|
|
35
35
|
|
|
36
36
|
let result
|
|
37
|
+
let fileOnly = false
|
|
37
38
|
if (running) {
|
|
38
39
|
try {
|
|
39
40
|
result = await apiPost('/api/rule-files', {
|
|
@@ -46,6 +47,7 @@ async function run() {
|
|
|
46
47
|
// Fallback to file mode
|
|
47
48
|
try {
|
|
48
49
|
result = createRuleFile(fileName, content, true)
|
|
50
|
+
fileOnly = true
|
|
49
51
|
} catch (err) {
|
|
50
52
|
output.error(err.message)
|
|
51
53
|
process.exit(1)
|
|
@@ -54,14 +56,19 @@ async function run() {
|
|
|
54
56
|
} else {
|
|
55
57
|
try {
|
|
56
58
|
result = createRuleFile(fileName, content, true)
|
|
59
|
+
fileOnly = true
|
|
57
60
|
} catch (err) {
|
|
58
61
|
output.error(err.message)
|
|
59
62
|
process.exit(1)
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
if (fileOnly) output.warnFileOnlyMode()
|
|
67
|
+
|
|
63
68
|
if (jsonFlag) {
|
|
64
|
-
output.jsonRaw(
|
|
69
|
+
output.jsonRaw(fileOnly
|
|
70
|
+
? { ...result, fileOnly: true, warning: output.FILE_ONLY_WARNING }
|
|
71
|
+
: result)
|
|
65
72
|
return
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -5,49 +5,87 @@
|
|
|
5
5
|
const output = require('../../lib/output')
|
|
6
6
|
const { isProxyRunning } = require('../../lib/proxy-detect')
|
|
7
7
|
const { apiGet, apiPut } = require('../../lib/api-client')
|
|
8
|
-
const { getRuleFileContent, saveRuleFileContent
|
|
8
|
+
const { getRuleFileContent, saveRuleFileContent } = require('../../lib/file-access')
|
|
9
|
+
const { deleteRouteRule } = require('../../lib/rule-file-edit')
|
|
10
|
+
const { isRulesetReadonlyError, exitRulesetReadonly } = require('../../lib/ruleset-readonly')
|
|
11
|
+
const { parseRouteWriteArgs, parsePatternArg } = require('../../lib/route-write-args')
|
|
12
|
+
|
|
13
|
+
const DELETE_HELP = `
|
|
14
|
+
meddle route delete <file> <pattern> [--json]
|
|
15
|
+
|
|
16
|
+
Delete a rule. This command does not take --exclusions.
|
|
17
|
+
`
|
|
9
18
|
|
|
10
|
-
// Parse arguments (skip 'route' and 'delete' from argv)
|
|
11
19
|
const args = process.argv.slice(4)
|
|
12
|
-
|
|
20
|
+
|
|
21
|
+
function fail(message, jsonFlag) {
|
|
22
|
+
if (jsonFlag) {
|
|
23
|
+
console.log(JSON.stringify({ success: false, error: { message } }))
|
|
24
|
+
console.error(message)
|
|
25
|
+
} else {
|
|
26
|
+
output.setJsonMode(false)
|
|
27
|
+
output.error(message)
|
|
28
|
+
}
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let parsed
|
|
33
|
+
try {
|
|
34
|
+
parsed = parseRouteWriteArgs(args, { allowExclusions: false })
|
|
35
|
+
} catch (e) {
|
|
36
|
+
fail(e.message, args.includes('--json'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (parsed.help) {
|
|
40
|
+
console.log(DELETE_HELP.trim())
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const jsonFlag = parsed.json
|
|
13
45
|
output.setJsonMode(jsonFlag)
|
|
14
46
|
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const fileName = positionalArgs[0]
|
|
18
|
-
const pattern = positionalArgs[1]
|
|
47
|
+
const fileName = parsed.positional[0]
|
|
48
|
+
const pattern = parsed.positional[1]
|
|
19
49
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Issue #145: remove only this pattern; every other line (comments, blank
|
|
52
|
+
* lines, other rules and their `!exclusions`) keeps its exact bytes.
|
|
53
|
+
*/
|
|
54
|
+
function applyDelete(text) {
|
|
55
|
+
const { patterns } = parsePatternArg(pattern)
|
|
56
|
+
if (patterns.length === 0) {
|
|
57
|
+
fail('Missing required argument: <pattern>', jsonFlag)
|
|
58
|
+
}
|
|
59
|
+
let next = text
|
|
60
|
+
for (const p of patterns) {
|
|
61
|
+
const result = deleteRouteRule(next, { pattern: p })
|
|
62
|
+
if (!result.found) {
|
|
63
|
+
fail(`Pattern not found: ${pattern.trim()}`, jsonFlag)
|
|
64
|
+
}
|
|
65
|
+
next = result.content
|
|
24
66
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
67
|
+
return next
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function run() {
|
|
71
|
+
if (!fileName) fail('Missing required argument: <file>', jsonFlag)
|
|
72
|
+
if (!pattern) fail('Missing required argument: <pattern>', jsonFlag)
|
|
73
|
+
if (parsed.positional.length > 2) fail(`Unexpected argument: ${parsed.positional[2]}`, jsonFlag)
|
|
74
|
+
try {
|
|
75
|
+
parsePatternArg(pattern)
|
|
76
|
+
} catch (e) {
|
|
77
|
+
fail(e.message, jsonFlag)
|
|
28
78
|
}
|
|
29
79
|
|
|
30
80
|
const running = await isProxyRunning()
|
|
31
81
|
|
|
32
82
|
const pat = pattern.trim()
|
|
33
83
|
|
|
34
|
-
// Handle [marker] syntax for lookup
|
|
35
|
-
const bm = pat.match(/\[([^\]]+)\]/)
|
|
36
|
-
const internalKey = bm ? pat.replace(bm[0], bm[1]) : pat
|
|
37
|
-
|
|
38
84
|
if (running) {
|
|
39
85
|
try {
|
|
40
86
|
let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
|
|
41
87
|
const text = typeof content === 'string' ? content : String(content)
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
45
|
-
output.error(`Pattern not found: ${pat}`)
|
|
46
|
-
process.exit(1)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
delete ruleMap[internalKey]
|
|
50
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
88
|
+
const newContent = applyDelete(text)
|
|
51
89
|
await apiPut(`/api/rule-files/${encodeURIComponent(fileName)}/content`, { content: newContent })
|
|
52
90
|
|
|
53
91
|
if (jsonFlag) {
|
|
@@ -59,28 +97,27 @@ async function run() {
|
|
|
59
97
|
output.success(pat)
|
|
60
98
|
output.kv('File', fileName)
|
|
61
99
|
} catch (e) {
|
|
100
|
+
// Issue #137: a locked ruleset is a final answer, never a cue to write the file directly.
|
|
101
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
62
102
|
// Fallback to file mode
|
|
63
103
|
const content = getRuleFileContent(fileName)
|
|
64
104
|
if (!content) {
|
|
65
|
-
|
|
66
|
-
process.exit(1)
|
|
105
|
+
fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
67
106
|
}
|
|
68
107
|
|
|
69
|
-
const
|
|
70
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
71
|
-
output.error(`Pattern not found: ${pat}`)
|
|
72
|
-
process.exit(1)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
delete ruleMap[internalKey]
|
|
76
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
108
|
+
const newContent = applyDelete(content)
|
|
77
109
|
saveRuleFileContent(fileName, newContent)
|
|
110
|
+
output.warnFileOnlyMode()
|
|
78
111
|
|
|
79
112
|
if (jsonFlag) {
|
|
80
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat
|
|
113
|
+
output.jsonRaw({ success: true, file: fileName, pattern: pat,
|
|
114
|
+
fileOnly: true,
|
|
115
|
+
warning: output.FILE_ONLY_WARNING
|
|
116
|
+
})
|
|
81
117
|
return
|
|
82
118
|
}
|
|
83
119
|
|
|
120
|
+
|
|
84
121
|
output.header('Route Rule Deleted')
|
|
85
122
|
output.success(pat)
|
|
86
123
|
output.kv('File', fileName)
|
|
@@ -89,25 +126,22 @@ async function run() {
|
|
|
89
126
|
// File mode
|
|
90
127
|
const content = getRuleFileContent(fileName)
|
|
91
128
|
if (!content) {
|
|
92
|
-
|
|
93
|
-
process.exit(1)
|
|
129
|
+
fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
94
130
|
}
|
|
95
131
|
|
|
96
|
-
const
|
|
97
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
98
|
-
output.error(`Pattern not found: ${pat}`)
|
|
99
|
-
process.exit(1)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
delete ruleMap[internalKey]
|
|
103
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
132
|
+
const newContent = applyDelete(content)
|
|
104
133
|
saveRuleFileContent(fileName, newContent)
|
|
134
|
+
output.warnFileOnlyMode()
|
|
105
135
|
|
|
106
136
|
if (jsonFlag) {
|
|
107
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat
|
|
137
|
+
output.jsonRaw({ success: true, file: fileName, pattern: pat,
|
|
138
|
+
fileOnly: true,
|
|
139
|
+
warning: output.FILE_ONLY_WARNING
|
|
140
|
+
})
|
|
108
141
|
return
|
|
109
142
|
}
|
|
110
143
|
|
|
144
|
+
|
|
111
145
|
output.header('Route Rule Deleted')
|
|
112
146
|
output.success(pat)
|
|
113
147
|
output.kv('File', fileName)
|
|
@@ -115,6 +149,6 @@ async function run() {
|
|
|
115
149
|
}
|
|
116
150
|
|
|
117
151
|
run().catch(e => {
|
|
118
|
-
|
|
119
|
-
|
|
152
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
153
|
+
fail(e.message, jsonFlag)
|
|
120
154
|
})
|
|
@@ -39,10 +39,16 @@ Route Commands:
|
|
|
39
39
|
meddle route active Show active route files
|
|
40
40
|
meddle route active set <file> Set active route file
|
|
41
41
|
meddle route create <name> [--content <text>] Create a route file
|
|
42
|
-
meddle route add <file> <pattern> <target>
|
|
43
|
-
|
|
42
|
+
meddle route add <file> <pattern> <target> [--exclusions=<list>]
|
|
43
|
+
Add a rule to file
|
|
44
|
+
meddle route update <file> <pattern> [<target>] [--exclusions=<list>]
|
|
45
|
+
Update a rule in file
|
|
44
46
|
meddle route delete <file> <pattern> Delete a rule from file
|
|
45
47
|
|
|
48
|
+
Read-only route files:
|
|
49
|
+
Files locked in the Meddle web UI reject add/update/delete and active set
|
|
50
|
+
(error RULESET_READONLY, exit code 3). Lock/unlock is only available in the web UI.
|
|
51
|
+
|
|
46
52
|
Examples:
|
|
47
53
|
meddle route list
|
|
48
54
|
meddle route show dev-rules
|
|
@@ -53,7 +59,19 @@ Examples:
|
|
|
53
59
|
meddle route create staging --content "example.com localhost:3000"
|
|
54
60
|
meddle route add dev-rules "api.test.com" "localhost:8080"
|
|
55
61
|
meddle route update dev-rules "api.test.com" "localhost:9000"
|
|
62
|
+
meddle route update dev-rules "api.test.com" --exclusions=
|
|
56
63
|
meddle route delete dev-rules "api.test.com"
|
|
64
|
+
|
|
65
|
+
Exclusions:
|
|
66
|
+
Omit --exclusions and any !path in the pattern to keep the rule's exclusions.
|
|
67
|
+
--exclusions= Clear exclusions (that line loses every !path).
|
|
68
|
+
--exclusions=/a,/b Replace exclusions. A leading ! is optional.
|
|
69
|
+
"pattern !/a" Replace exclusions. A bare "!" is an error.
|
|
70
|
+
A bare --exclusions, an unknown flag, or both forms at once exits 1
|
|
71
|
+
and does not change the file. meddle route update --help has the details.
|
|
72
|
+
|
|
73
|
+
A "#" comments out a whole line only when it is the first non-space
|
|
74
|
+
character. A "#" later on a rule line is a pattern, not a trailing comment.
|
|
57
75
|
`)
|
|
58
76
|
process.exit(1)
|
|
59
77
|
}
|
|
@@ -48,7 +48,7 @@ async function run() {
|
|
|
48
48
|
output.section('Active Files')
|
|
49
49
|
active.forEach(f => {
|
|
50
50
|
const excludeInfo = f.excludeCount > 0 ? `, ${f.excludeCount} exclusions` : ''
|
|
51
|
-
output.success(`${f.name} (${f.ruleCount} rules${excludeInfo})`)
|
|
51
|
+
output.success(`${f.name} (${f.ruleCount} rules${excludeInfo})${f.readonly ? ' [read-only]' : ''}`)
|
|
52
52
|
})
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -56,7 +56,7 @@ async function run() {
|
|
|
56
56
|
output.section('Inactive Files')
|
|
57
57
|
inactive.forEach(f => {
|
|
58
58
|
const excludeInfo = f.excludeCount > 0 ? `, ${f.excludeCount} exclusions` : ''
|
|
59
|
-
output.bullet(`${f.name} (${f.ruleCount} rules${excludeInfo})`, false)
|
|
59
|
+
output.bullet(`${f.name} (${f.ruleCount} rules${excludeInfo})${f.readonly ? ' [read-only]' : ''}`, false)
|
|
60
60
|
})
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const output = require('../../lib/output')
|
|
6
6
|
const { isProxyRunning } = require('../../lib/proxy-detect')
|
|
7
7
|
const { apiGet } = require('../../lib/api-client')
|
|
8
|
-
const { getRuleFileContent, parseEprcWithExclusions } = require('../../lib/file-access')
|
|
8
|
+
const { getRuleFileContent, parseEprcWithExclusions, isRuleFileReadonly } = require('../../lib/file-access')
|
|
9
9
|
|
|
10
10
|
// Parse arguments (skip 'route' and 'show' from argv)
|
|
11
11
|
const args = process.argv.slice(4)
|
|
@@ -26,10 +26,15 @@ async function run() {
|
|
|
26
26
|
let content = null
|
|
27
27
|
let ruleMap = {}
|
|
28
28
|
let excludeMap = {}
|
|
29
|
+
// Issue #137: report the lock state (read-only for the CLI either way).
|
|
30
|
+
let readonly = false
|
|
29
31
|
|
|
30
32
|
if (running) {
|
|
31
33
|
try {
|
|
32
34
|
content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
|
|
35
|
+
const files = await apiGet('/api/rule-files')
|
|
36
|
+
const info = Array.isArray(files) ? files.find((f) => f && f.name === fileName) : null
|
|
37
|
+
readonly = Boolean(info && info.readonly)
|
|
33
38
|
const parsed = parseEprcWithExclusions(typeof content === 'string' ? content : String(content))
|
|
34
39
|
ruleMap = parsed.ruleMap
|
|
35
40
|
excludeMap = parsed.excludeMap
|
|
@@ -43,6 +48,7 @@ async function run() {
|
|
|
43
48
|
const parsed = parseEprcWithExclusions(content)
|
|
44
49
|
ruleMap = parsed.ruleMap
|
|
45
50
|
excludeMap = parsed.excludeMap
|
|
51
|
+
readonly = isRuleFileReadonly(fileName)
|
|
46
52
|
}
|
|
47
53
|
} else {
|
|
48
54
|
content = getRuleFileContent(fileName)
|
|
@@ -53,6 +59,7 @@ async function run() {
|
|
|
53
59
|
const parsed = parseEprcWithExclusions(content)
|
|
54
60
|
ruleMap = parsed.ruleMap
|
|
55
61
|
excludeMap = parsed.excludeMap
|
|
62
|
+
readonly = isRuleFileReadonly(fileName)
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
// Build display rules (handle [marker] syntax)
|
|
@@ -76,6 +83,7 @@ async function run() {
|
|
|
76
83
|
if (jsonFlag) {
|
|
77
84
|
output.jsonRaw({
|
|
78
85
|
file: fileName,
|
|
86
|
+
readonly,
|
|
79
87
|
rules: displayRules,
|
|
80
88
|
ruleCount: Object.keys(displayRules).length,
|
|
81
89
|
exclusionCount: totalExclusions
|
|
@@ -84,6 +92,7 @@ async function run() {
|
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
output.header(`Route File: ${fileName}`)
|
|
95
|
+
output.kv('Read-only', readonly ? 'Yes (unlock in the Meddle UI)' : 'No')
|
|
87
96
|
output.kv('Rules', Object.keys(displayRules).length)
|
|
88
97
|
if (totalExclusions > 0) {
|
|
89
98
|
output.kv('Exclusions', totalExclusions)
|