@jonathanleelx/meddle 0.5.0-beta.24 → 0.5.0-beta.26
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/README.md +1 -1
- package/bin/commands/route/add.js +98 -86
- package/bin/commands/route/delete.js +48 -25
- package/bin/commands/route/index.js +16 -2
- package/bin/commands/route/update.js +118 -87
- package/bin/index.js +7 -3
- package/bin/lib/output.js +4 -2
- package/bin/lib/route-write-args.js +150 -0
- 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/server/agent/tools.d.ts.map +1 -1
- package/dist/server/agent/tools.js +2 -1
- package/dist/server/agent/tools.js.map +1 -1
- package/index.js +3 -2
- package/package.json +1 -1
- package/web/dist/assets/{detail-panel-D56nCma2.js → detail-panel-CyOc7gn8.js} +1 -1
- package/web/dist/assets/{global-panel-shell-SKPshqTf.js → global-panel-shell-jXQTb9nw.js} +1 -1
- package/web/dist/assets/{health-panel-CvI-VsvG.js → health-panel-CSV7OqBv.js} +1 -1
- package/web/dist/assets/{index-Dcm171u1.js → index-CSXxaYkG.js} +2 -2
- package/web/dist/assets/{mobile-proxy-panel-cpkvsdBT.js → mobile-proxy-panel-a09S-R79.js} +1 -1
- package/web/dist/assets/{mock-config-CtZ866iI.js → mock-config-D_E5YOE_.js} +1 -1
- package/web/dist/assets/{mock-editor-panel-CweYBdeM.js → mock-editor-panel-t4TDXeaR.js} +1 -1
- package/web/dist/assets/{mocks-file-missing-note-C-QJ8Bpm.js → mocks-file-missing-note-Ddy1TdMX.js} +1 -1
- package/web/dist/assets/{monaco-editor-Cge8h-Ca.js → monaco-editor-GpPLOKA5.js} +1 -1
- package/web/dist/assets/{pane-bar-Dvi-MHuB.js → pane-bar-zJmE3xI5.js} +1 -1
- package/web/dist/assets/{pane-split-ChCHxR7K.js → pane-split-CJ1VXJDy.js} +1 -1
- package/web/dist/assets/{panel-heading-Crmy9fo0.js → panel-heading-DlFWoYAR.js} +1 -1
- package/web/dist/assets/{plugin-code-editor-VaL2lVyD.js → plugin-code-editor-CtaamKWJ.js} +1 -1
- package/web/dist/assets/{plugin-config-DSiOk77l.js → plugin-config-Dg_FCb-L.js} +2 -2
- package/web/dist/assets/{plugin-generator-iAksMY2Q.js → plugin-generator-CVREuZs7.js} +1 -1
- package/web/dist/assets/{plugin-test-dialog-Cx6ifDnm.js → plugin-test-dialog-B-iV8ocm.js} +1 -1
- package/web/dist/assets/{route-preview-T0s6si4V.js → route-preview-CLJeg_9v.js} +1 -1
- package/web/dist/assets/{rule-ai-assistant-panel-DzfobTWM.js → rule-ai-assistant-panel-BvTqm4zV.js} +1 -1
- package/web/dist/assets/{rule-config-ESpfkJg6.js → rule-config-DmNPaKxc.js} +1 -1
- package/web/dist/assets/{rule-overview-panel-BswSByar.js → rule-overview-panel-CB9b-b0t.js} +1 -1
- package/web/dist/assets/{settings-panel-Cj-1Z8Ow.js → settings-panel-DLdeJ8ox.js} +1 -1
- package/web/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -188,7 +188,7 @@ meddle --remote --no-intercept-https
|
|
|
188
188
|
- 传非空数组:用它替换原有的排除规则。
|
|
189
189
|
- 如果一行写了多个 pattern,修改或删除其中一个时,若剩下的 pattern 以 `//` 或 `#` 开头(拆开后会被当成注释),写入会被拒绝并提示「该行剩余的 pattern 以 // 或 # 开头,拆开后会被当成注释,请用文本编辑器手动修改」,规则文件不变。
|
|
190
190
|
- 新增规则的 pattern 不能以 `#` 或 `//` 开头(例如 `#foo.com`、`//foo.com`),否则写进文件后会被当成注释或禁用行。写入会被拒绝并提示「规则不能以 // 或 # 开头,否则会被当成禁用行或注释。要写注释或禁用规则,请用文本编辑器。」,规则文件不变。
|
|
191
|
-
- CLI
|
|
191
|
+
- CLI:`meddle route update <file> <pattern> --exclusions=` 清空该规则的排除项(该行不再有 `!path`,其余行逐字节不变)。`--exclusions=/a,/b` 替换。pattern 里写 `!path`(如 `"api.example.com !/health"`)同样是替换。两者都不写则保留原有排除项。单独的 `--exclusions`(没有 `=`)、空的 `!`、未知参数,或 `!path` 与 `--exclusions` 同时出现,都会非 0 退出且不改文件。`#` 只有出现在一行开头(前导空白之后)才是整行注释;行尾的 `#` 不是注释。
|
|
192
192
|
|
|
193
193
|
**只读规则集**([详细说明](./docs/RULESET_READONLY.md)):规则集(规则文件)可以在 Meddle Web 界面中加只读锁,防止 Agent 误改。
|
|
194
194
|
|
|
@@ -6,129 +6,141 @@ const output = require('../../lib/output')
|
|
|
6
6
|
const { isProxyRunning } = require('../../lib/proxy-detect')
|
|
7
7
|
const { apiGet, apiPut } = require('../../lib/api-client')
|
|
8
8
|
const { getRuleFileContent, saveRuleFileContent } = require('../../lib/file-access')
|
|
9
|
-
const { addRouteRule
|
|
9
|
+
const { addRouteRule } = require('../../lib/rule-file-edit')
|
|
10
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
|
+
`
|
|
11
21
|
|
|
12
|
-
// Parse arguments (skip 'route' and 'add' from argv)
|
|
13
22
|
const args = process.argv.slice(4)
|
|
14
|
-
|
|
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
|
|
15
48
|
output.setJsonMode(jsonFlag)
|
|
16
49
|
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
50
|
+
const fileName = parsed.positional[0]
|
|
51
|
+
const pattern = parsed.positional[1]
|
|
52
|
+
const target = parsed.positional[2]
|
|
53
|
+
|
|
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
|
+
}
|
|
22
65
|
|
|
23
66
|
/**
|
|
24
67
|
* Issue #145: edit only the line(s) holding this pattern; comments, blank
|
|
25
68
|
* lines and other rules (with their `!exclusions`) keep their exact bytes.
|
|
26
|
-
* Exclusions may be given inside the pattern argument ("app.example.com !/api")
|
|
69
|
+
* Exclusions may be given inside the pattern argument ("app.example.com !/api")
|
|
70
|
+
* or with --exclusions. `--exclusions=` clears them (issue #149).
|
|
27
71
|
*/
|
|
28
|
-
function applyAdd(text) {
|
|
29
|
-
const { patterns, exclusions } = splitPatternArg(pattern)
|
|
30
|
-
if (patterns.length === 0) {
|
|
31
|
-
output.error('Missing required argument: <pattern>')
|
|
32
|
-
process.exit(1)
|
|
33
|
-
}
|
|
72
|
+
function applyAdd(text, plan) {
|
|
34
73
|
let next = text
|
|
35
|
-
for (const p of patterns) {
|
|
36
|
-
next = addRouteRule(next, { pattern: p, target: target.trim(), exclusions:
|
|
74
|
+
for (const p of plan.patterns) {
|
|
75
|
+
next = addRouteRule(next, { pattern: p, target: target.trim(), exclusions: plan.exclusions }).content
|
|
37
76
|
}
|
|
38
77
|
return next
|
|
39
78
|
}
|
|
40
79
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
45
86
|
}
|
|
46
|
-
if (
|
|
47
|
-
output.
|
|
48
|
-
|
|
87
|
+
if (jsonFlag) {
|
|
88
|
+
output.jsonRaw(payload)
|
|
89
|
+
return
|
|
49
90
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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')
|
|
53
95
|
}
|
|
96
|
+
output.kv('File', fileName)
|
|
97
|
+
}
|
|
54
98
|
|
|
55
|
-
|
|
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
|
+
}
|
|
56
111
|
|
|
57
|
-
const
|
|
58
|
-
const tgt = target.trim()
|
|
112
|
+
const running = await isProxyRunning()
|
|
59
113
|
|
|
60
114
|
if (running) {
|
|
61
115
|
try {
|
|
62
|
-
// Get current content
|
|
63
116
|
let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
|
|
64
117
|
const text = typeof content === 'string' ? content : String(content)
|
|
65
|
-
const newContent = applyAdd(text)
|
|
118
|
+
const newContent = applyAdd(text, plan)
|
|
66
119
|
await apiPut(`/api/rule-files/${encodeURIComponent(fileName)}/content`, { content: newContent })
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt })
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
output.header('Route Rule Added')
|
|
74
|
-
output.success(`${pat} -> ${tgt}`)
|
|
75
|
-
output.kv('File', fileName)
|
|
120
|
+
emitAdded({ fileOnly: false })
|
|
121
|
+
return
|
|
76
122
|
} catch (e) {
|
|
77
123
|
// Issue #137: a locked ruleset is a final answer, never a cue to write the file directly.
|
|
78
124
|
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
79
|
-
// Fallback to file mode
|
|
80
125
|
const content = getRuleFileContent(fileName)
|
|
81
|
-
if (!content) {
|
|
82
|
-
|
|
83
|
-
process.exit(1)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const newContent = applyAdd(content)
|
|
126
|
+
if (!content) fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
127
|
+
const newContent = applyAdd(content, plan)
|
|
87
128
|
saveRuleFileContent(fileName, newContent)
|
|
88
129
|
output.warnFileOnlyMode()
|
|
89
|
-
|
|
90
|
-
if (jsonFlag) {
|
|
91
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt,
|
|
92
|
-
fileOnly: true,
|
|
93
|
-
warning: output.FILE_ONLY_WARNING
|
|
94
|
-
})
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
output.header('Route Rule Added')
|
|
100
|
-
output.success(`${pat} -> ${tgt}`)
|
|
101
|
-
output.kv('File', fileName)
|
|
102
|
-
}
|
|
103
|
-
} else {
|
|
104
|
-
// File mode
|
|
105
|
-
const content = getRuleFileContent(fileName)
|
|
106
|
-
if (!content) {
|
|
107
|
-
output.error(`Route file not found: ${fileName}`)
|
|
108
|
-
process.exit(1)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const newContent = applyAdd(content)
|
|
112
|
-
saveRuleFileContent(fileName, newContent)
|
|
113
|
-
output.warnFileOnlyMode()
|
|
114
|
-
|
|
115
|
-
if (jsonFlag) {
|
|
116
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, target: tgt,
|
|
117
|
-
fileOnly: true,
|
|
118
|
-
warning: output.FILE_ONLY_WARNING
|
|
119
|
-
})
|
|
130
|
+
emitAdded({ fileOnly: true })
|
|
120
131
|
return
|
|
121
132
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
output.header('Route Rule Added')
|
|
125
|
-
output.success(`${pat} -> ${tgt}`)
|
|
126
|
-
output.kv('File', fileName)
|
|
127
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 })
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
run().catch(e => {
|
|
131
144
|
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
132
|
-
|
|
133
|
-
process.exit(1)
|
|
145
|
+
fail(e.message, jsonFlag)
|
|
134
146
|
})
|
|
@@ -6,35 +6,61 @@ const output = require('../../lib/output')
|
|
|
6
6
|
const { isProxyRunning } = require('../../lib/proxy-detect')
|
|
7
7
|
const { apiGet, apiPut } = require('../../lib/api-client')
|
|
8
8
|
const { getRuleFileContent, saveRuleFileContent } = require('../../lib/file-access')
|
|
9
|
-
const { deleteRouteRule
|
|
9
|
+
const { deleteRouteRule } = require('../../lib/rule-file-edit')
|
|
10
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
|
+
`
|
|
11
18
|
|
|
12
|
-
// Parse arguments (skip 'route' and 'delete' from argv)
|
|
13
19
|
const args = process.argv.slice(4)
|
|
14
|
-
|
|
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
|
|
15
45
|
output.setJsonMode(jsonFlag)
|
|
16
46
|
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const fileName = positionalArgs[0]
|
|
20
|
-
const pattern = positionalArgs[1]
|
|
47
|
+
const fileName = parsed.positional[0]
|
|
48
|
+
const pattern = parsed.positional[1]
|
|
21
49
|
|
|
22
50
|
/**
|
|
23
51
|
* Issue #145: remove only this pattern; every other line (comments, blank
|
|
24
52
|
* lines, other rules and their `!exclusions`) keeps its exact bytes.
|
|
25
53
|
*/
|
|
26
54
|
function applyDelete(text) {
|
|
27
|
-
const { patterns } =
|
|
55
|
+
const { patterns } = parsePatternArg(pattern)
|
|
28
56
|
if (patterns.length === 0) {
|
|
29
|
-
|
|
30
|
-
process.exit(1)
|
|
57
|
+
fail('Missing required argument: <pattern>', jsonFlag)
|
|
31
58
|
}
|
|
32
59
|
let next = text
|
|
33
60
|
for (const p of patterns) {
|
|
34
61
|
const result = deleteRouteRule(next, { pattern: p })
|
|
35
62
|
if (!result.found) {
|
|
36
|
-
|
|
37
|
-
process.exit(1)
|
|
63
|
+
fail(`Pattern not found: ${pattern.trim()}`, jsonFlag)
|
|
38
64
|
}
|
|
39
65
|
next = result.content
|
|
40
66
|
}
|
|
@@ -42,13 +68,13 @@ function applyDelete(text) {
|
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
async function run() {
|
|
45
|
-
if (!fileName)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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)
|
|
52
78
|
}
|
|
53
79
|
|
|
54
80
|
const running = await isProxyRunning()
|
|
@@ -76,8 +102,7 @@ async function run() {
|
|
|
76
102
|
// Fallback to file mode
|
|
77
103
|
const content = getRuleFileContent(fileName)
|
|
78
104
|
if (!content) {
|
|
79
|
-
|
|
80
|
-
process.exit(1)
|
|
105
|
+
fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
const newContent = applyDelete(content)
|
|
@@ -101,8 +126,7 @@ async function run() {
|
|
|
101
126
|
// File mode
|
|
102
127
|
const content = getRuleFileContent(fileName)
|
|
103
128
|
if (!content) {
|
|
104
|
-
|
|
105
|
-
process.exit(1)
|
|
129
|
+
fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
const newContent = applyDelete(content)
|
|
@@ -126,6 +150,5 @@ async function run() {
|
|
|
126
150
|
|
|
127
151
|
run().catch(e => {
|
|
128
152
|
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
129
|
-
|
|
130
|
-
process.exit(1)
|
|
153
|
+
fail(e.message, jsonFlag)
|
|
131
154
|
})
|
|
@@ -39,8 +39,10 @@ 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
|
|
|
46
48
|
Read-only route files:
|
|
@@ -57,7 +59,19 @@ Examples:
|
|
|
57
59
|
meddle route create staging --content "example.com localhost:3000"
|
|
58
60
|
meddle route add dev-rules "api.test.com" "localhost:8080"
|
|
59
61
|
meddle route update dev-rules "api.test.com" "localhost:9000"
|
|
62
|
+
meddle route update dev-rules "api.test.com" --exclusions=
|
|
60
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.
|
|
61
75
|
`)
|
|
62
76
|
process.exit(1)
|
|
63
77
|
}
|
|
@@ -1,138 +1,169 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* meddle route update - Update a rule in a route file
|
|
3
|
+
*
|
|
4
|
+
* Issue #149: `--exclusions=` clears a rule's exclusions. Omitting the flag
|
|
5
|
+
* (and any !path in the pattern) still keeps them.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
const output = require('../../lib/output')
|
|
6
9
|
const { isProxyRunning } = require('../../lib/proxy-detect')
|
|
7
10
|
const { apiGet, apiPut } = require('../../lib/api-client')
|
|
8
11
|
const { getRuleFileContent, saveRuleFileContent } = require('../../lib/file-access')
|
|
9
|
-
const { updateRouteRule,
|
|
12
|
+
const { updateRouteRule, patternKey, parseRuleLine } = require('../../lib/rule-file-edit')
|
|
10
13
|
const { isRulesetReadonlyError, exitRulesetReadonly } = require('../../lib/ruleset-readonly')
|
|
14
|
+
const {
|
|
15
|
+
parseRouteWriteArgs,
|
|
16
|
+
parsePatternArg,
|
|
17
|
+
resolveExclusions,
|
|
18
|
+
UPDATE_HELP,
|
|
19
|
+
} = require('../../lib/route-write-args')
|
|
11
20
|
|
|
12
|
-
// Parse arguments (skip 'route' and 'update' from argv)
|
|
13
21
|
const args = process.argv.slice(4)
|
|
14
|
-
|
|
22
|
+
|
|
23
|
+
function fail(message, jsonFlag) {
|
|
24
|
+
if (jsonFlag) {
|
|
25
|
+
console.log(JSON.stringify({ success: false, error: { message } }))
|
|
26
|
+
console.error(message)
|
|
27
|
+
} else {
|
|
28
|
+
output.setJsonMode(false)
|
|
29
|
+
output.error(message)
|
|
30
|
+
}
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let parsed
|
|
35
|
+
try {
|
|
36
|
+
parsed = parseRouteWriteArgs(args)
|
|
37
|
+
} catch (e) {
|
|
38
|
+
fail(e.message, args.includes('--json'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (parsed.help) {
|
|
42
|
+
console.log(UPDATE_HELP.trim())
|
|
43
|
+
process.exit(0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const jsonFlag = parsed.json
|
|
15
47
|
output.setJsonMode(jsonFlag)
|
|
16
48
|
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
49
|
+
const fileName = parsed.positional[0]
|
|
50
|
+
const pattern = parsed.positional[1]
|
|
51
|
+
const newTarget = parsed.positional[2]
|
|
52
|
+
|
|
53
|
+
/** Set once arguments are valid. `explicit` means the user asked to set exclusions, including clear. */
|
|
54
|
+
let explicitExclusions = false
|
|
55
|
+
let appliedExclusions = []
|
|
56
|
+
|
|
57
|
+
function planUpdate() {
|
|
58
|
+
const { patterns, exclusions: patternExclusions } = parsePatternArg(pattern)
|
|
59
|
+
if (patterns.length === 0) {
|
|
60
|
+
fail('Missing required argument: <pattern>', jsonFlag)
|
|
61
|
+
}
|
|
62
|
+
const resolved = resolveExclusions(patternExclusions, parsed.exclusions)
|
|
63
|
+
explicitExclusions = resolved.explicit
|
|
64
|
+
appliedExclusions = resolved.exclusions || []
|
|
65
|
+
return { patterns, exclusions: resolved.explicit ? resolved.exclusions : undefined }
|
|
66
|
+
}
|
|
22
67
|
|
|
23
68
|
/**
|
|
24
69
|
* Issue #145: rewrite only the line(s) holding this pattern. The rule keeps its
|
|
25
|
-
* own `!exclusions` unless new ones are given
|
|
26
|
-
* other line keeps its
|
|
70
|
+
* own `!exclusions` unless new ones are given. Issue #149: `--exclusions=` passes
|
|
71
|
+
* `[]`, which removes `!path` from that line. Every other line keeps its bytes.
|
|
27
72
|
*/
|
|
28
|
-
function applyUpdate(text) {
|
|
29
|
-
const
|
|
30
|
-
if (patterns.length === 0) {
|
|
31
|
-
output.error('Missing required argument: <pattern>')
|
|
32
|
-
process.exit(1)
|
|
33
|
-
}
|
|
73
|
+
function applyUpdate(text, plan) {
|
|
74
|
+
const target = newTarget ? newTarget.trim() : undefined
|
|
34
75
|
let next = text
|
|
35
|
-
for (const p of patterns) {
|
|
36
|
-
const result = updateRouteRule(next, { pattern: p, target
|
|
76
|
+
for (const p of plan.patterns) {
|
|
77
|
+
const result = updateRouteRule(next, { pattern: p, target, exclusions: plan.exclusions })
|
|
37
78
|
if (!result.found) {
|
|
38
|
-
|
|
39
|
-
process.exit(1)
|
|
79
|
+
fail(`Pattern not found: ${pattern.trim()}`, jsonFlag)
|
|
40
80
|
}
|
|
41
81
|
next = result.content
|
|
42
82
|
}
|
|
43
83
|
return next
|
|
44
84
|
}
|
|
45
85
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
86
|
+
/** Target token actually written, for feedback when the user did not pass one. */
|
|
87
|
+
function targetOnDisk(content, patternToken) {
|
|
88
|
+
const key = patternKey(patternToken)
|
|
89
|
+
const lines = content.split(/\r?\n/)
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const parsedLine = parseRuleLine(line)
|
|
92
|
+
if (parsedLine && parsedLine.patterns.some((p) => patternKey(p) === key)) return parsedLine.target
|
|
93
|
+
}
|
|
94
|
+
return ''
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function emitUpdated({ fileOnly, target }) {
|
|
98
|
+
const payload = { success: true, file: fileName, pattern: pattern.trim(), newTarget: target }
|
|
99
|
+
if (explicitExclusions) payload.exclusions = appliedExclusions
|
|
100
|
+
if (fileOnly) {
|
|
101
|
+
payload.fileOnly = true
|
|
102
|
+
payload.warning = output.FILE_ONLY_WARNING
|
|
50
103
|
}
|
|
51
|
-
if (
|
|
52
|
-
output.
|
|
53
|
-
|
|
104
|
+
if (jsonFlag) {
|
|
105
|
+
output.jsonRaw(payload)
|
|
106
|
+
return
|
|
54
107
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
108
|
+
output.header('Route Rule Updated')
|
|
109
|
+
output.success(`${pattern.trim()} -> ${target}`)
|
|
110
|
+
if (explicitExclusions) {
|
|
111
|
+
output.kv('Exclusions', appliedExclusions.length ? appliedExclusions.map((e) => `!${e}`).join(' ') : 'cleared')
|
|
58
112
|
}
|
|
113
|
+
output.kv('File', fileName)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function run() {
|
|
117
|
+
if (!fileName) fail('Missing required argument: <file>', jsonFlag)
|
|
118
|
+
if (!pattern) fail('Missing required argument: <pattern>', jsonFlag)
|
|
119
|
+
if (parsed.positional.length > 3) fail(`Unexpected argument: ${parsed.positional[3]}`, jsonFlag)
|
|
120
|
+
|
|
121
|
+
let plan
|
|
122
|
+
try {
|
|
123
|
+
plan = planUpdate()
|
|
124
|
+
} catch (e) {
|
|
125
|
+
fail(e.message, jsonFlag)
|
|
126
|
+
}
|
|
127
|
+
if (!newTarget && !explicitExclusions) fail('Missing required argument: <target>', jsonFlag)
|
|
59
128
|
|
|
60
129
|
const running = await isProxyRunning()
|
|
61
130
|
|
|
62
|
-
const
|
|
63
|
-
|
|
131
|
+
const finish = (content, fileOnly) => {
|
|
132
|
+
const target = newTarget ? newTarget.trim() : targetOnDisk(content, plan.patterns[0])
|
|
133
|
+
emitUpdated({ fileOnly, target })
|
|
134
|
+
}
|
|
64
135
|
|
|
65
136
|
if (running) {
|
|
66
137
|
try {
|
|
67
138
|
let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
|
|
68
139
|
const text = typeof content === 'string' ? content : String(content)
|
|
69
|
-
const newContent = applyUpdate(text)
|
|
140
|
+
const newContent = applyUpdate(text, plan)
|
|
70
141
|
await apiPut(`/api/rule-files/${encodeURIComponent(fileName)}/content`, { content: newContent })
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt })
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
output.header('Route Rule Updated')
|
|
78
|
-
output.success(`${pat} -> ${tgt}`)
|
|
79
|
-
output.kv('File', fileName)
|
|
142
|
+
finish(newContent, false)
|
|
143
|
+
return
|
|
80
144
|
} catch (e) {
|
|
81
145
|
// Issue #137: a locked ruleset is a final answer, never a cue to write the file directly.
|
|
82
146
|
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
83
147
|
// Fallback to file mode
|
|
84
148
|
const content = getRuleFileContent(fileName)
|
|
85
|
-
if (!content) {
|
|
86
|
-
|
|
87
|
-
process.exit(1)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const newContent = applyUpdate(content)
|
|
149
|
+
if (!content) fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
150
|
+
const newContent = applyUpdate(content, plan)
|
|
91
151
|
saveRuleFileContent(fileName, newContent)
|
|
92
152
|
output.warnFileOnlyMode()
|
|
93
|
-
|
|
94
|
-
if (jsonFlag) {
|
|
95
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt,
|
|
96
|
-
fileOnly: true,
|
|
97
|
-
warning: output.FILE_ONLY_WARNING
|
|
98
|
-
})
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
output.header('Route Rule Updated')
|
|
104
|
-
output.success(`${pat} -> ${tgt}`)
|
|
105
|
-
output.kv('File', fileName)
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
// File mode
|
|
109
|
-
const content = getRuleFileContent(fileName)
|
|
110
|
-
if (!content) {
|
|
111
|
-
output.error(`Route file not found: ${fileName}`)
|
|
112
|
-
process.exit(1)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const newContent = applyUpdate(content)
|
|
116
|
-
saveRuleFileContent(fileName, newContent)
|
|
117
|
-
output.warnFileOnlyMode()
|
|
118
|
-
|
|
119
|
-
if (jsonFlag) {
|
|
120
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt,
|
|
121
|
-
fileOnly: true,
|
|
122
|
-
warning: output.FILE_ONLY_WARNING
|
|
123
|
-
})
|
|
153
|
+
finish(newContent, true)
|
|
124
154
|
return
|
|
125
155
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
output.header('Route Rule Updated')
|
|
129
|
-
output.success(`${pat} -> ${tgt}`)
|
|
130
|
-
output.kv('File', fileName)
|
|
131
156
|
}
|
|
157
|
+
|
|
158
|
+
const content = getRuleFileContent(fileName)
|
|
159
|
+
if (!content) fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
160
|
+
const newContent = applyUpdate(content, plan)
|
|
161
|
+
saveRuleFileContent(fileName, newContent)
|
|
162
|
+
output.warnFileOnlyMode()
|
|
163
|
+
finish(newContent, true)
|
|
132
164
|
}
|
|
133
165
|
|
|
134
166
|
run().catch(e => {
|
|
135
167
|
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
})
|
|
168
|
+
fail(e.message, jsonFlag)
|
|
169
|
+
})
|