@jonathanleelx/meddle 0.5.0-beta.8 → 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/h2-pool.d.ts +1 -0
- package/dist/core/h2-pool.d.ts.map +1 -1
- package/dist/core/h2-pool.js +17 -1
- package/dist/core/h2-pool.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
|
@@ -1,126 +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
|
-
const { getRuleFileContent, saveRuleFileContent
|
|
11
|
+
const { getRuleFileContent, saveRuleFileContent } = require('../../lib/file-access')
|
|
12
|
+
const { updateRouteRule, patternKey, parseRuleLine } = require('../../lib/rule-file-edit')
|
|
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')
|
|
9
20
|
|
|
10
|
-
// Parse arguments (skip 'route' and 'update' from argv)
|
|
11
21
|
const args = process.argv.slice(4)
|
|
12
|
-
|
|
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
|
|
13
47
|
output.setJsonMode(jsonFlag)
|
|
14
48
|
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const pattern = positionalArgs[1]
|
|
19
|
-
const newTarget = positionalArgs[2]
|
|
49
|
+
const fileName = parsed.positional[0]
|
|
50
|
+
const pattern = parsed.positional[1]
|
|
51
|
+
const newTarget = parsed.positional[2]
|
|
20
52
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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)
|
|
25
61
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Issue #145: rewrite only the line(s) holding this pattern. The rule 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.
|
|
72
|
+
*/
|
|
73
|
+
function applyUpdate(text, plan) {
|
|
74
|
+
const target = newTarget ? newTarget.trim() : undefined
|
|
75
|
+
let next = text
|
|
76
|
+
for (const p of plan.patterns) {
|
|
77
|
+
const result = updateRouteRule(next, { pattern: p, target, exclusions: plan.exclusions })
|
|
78
|
+
if (!result.found) {
|
|
79
|
+
fail(`Pattern not found: ${pattern.trim()}`, jsonFlag)
|
|
80
|
+
}
|
|
81
|
+
next = result.content
|
|
29
82
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
83
|
+
return next
|
|
84
|
+
}
|
|
85
|
+
|
|
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
|
|
33
93
|
}
|
|
94
|
+
return ''
|
|
95
|
+
}
|
|
34
96
|
|
|
35
|
-
|
|
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
|
|
103
|
+
}
|
|
104
|
+
if (jsonFlag) {
|
|
105
|
+
output.jsonRaw(payload)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
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')
|
|
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)
|
|
36
128
|
|
|
37
|
-
const
|
|
38
|
-
const tgt = newTarget.trim()
|
|
129
|
+
const running = await isProxyRunning()
|
|
39
130
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
131
|
+
const finish = (content, fileOnly) => {
|
|
132
|
+
const target = newTarget ? newTarget.trim() : targetOnDisk(content, plan.patterns[0])
|
|
133
|
+
emitUpdated({ fileOnly, target })
|
|
134
|
+
}
|
|
43
135
|
|
|
44
136
|
if (running) {
|
|
45
137
|
try {
|
|
46
138
|
let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
|
|
47
139
|
const text = typeof content === 'string' ? content : String(content)
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
51
|
-
output.error(`Pattern not found: ${pat}`)
|
|
52
|
-
process.exit(1)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
|
|
56
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
140
|
+
const newContent = applyUpdate(text, plan)
|
|
57
141
|
await apiPut(`/api/rule-files/${encodeURIComponent(fileName)}/content`, { content: newContent })
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt })
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
output.header('Route Rule Updated')
|
|
65
|
-
output.success(`${pat} -> ${tgt}`)
|
|
66
|
-
output.kv('File', fileName)
|
|
142
|
+
finish(newContent, false)
|
|
143
|
+
return
|
|
67
144
|
} catch (e) {
|
|
145
|
+
// Issue #137: a locked ruleset is a final answer, never a cue to write the file directly.
|
|
146
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
68
147
|
// Fallback to file mode
|
|
69
148
|
const content = getRuleFileContent(fileName)
|
|
70
|
-
if (!content) {
|
|
71
|
-
|
|
72
|
-
process.exit(1)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const ruleMap = parseEprc(content)
|
|
76
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
77
|
-
output.error(`Pattern not found: ${pat}`)
|
|
78
|
-
process.exit(1)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
|
|
82
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
149
|
+
if (!content) fail(`Route file not found: ${fileName}`, jsonFlag)
|
|
150
|
+
const newContent = applyUpdate(content, plan)
|
|
83
151
|
saveRuleFileContent(fileName, newContent)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt })
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
output.header('Route Rule Updated')
|
|
91
|
-
output.success(`${pat} -> ${tgt}`)
|
|
92
|
-
output.kv('File', fileName)
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
// File mode
|
|
96
|
-
const content = getRuleFileContent(fileName)
|
|
97
|
-
if (!content) {
|
|
98
|
-
output.error(`Route file not found: ${fileName}`)
|
|
99
|
-
process.exit(1)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const ruleMap = parseEprc(content)
|
|
103
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
104
|
-
output.error(`Pattern not found: ${pat}`)
|
|
105
|
-
process.exit(1)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
|
|
109
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
110
|
-
saveRuleFileContent(fileName, newContent)
|
|
111
|
-
|
|
112
|
-
if (jsonFlag) {
|
|
113
|
-
output.jsonRaw({ success: true, file: fileName, pattern: pat, newTarget: tgt })
|
|
152
|
+
output.warnFileOnlyMode()
|
|
153
|
+
finish(newContent, true)
|
|
114
154
|
return
|
|
115
155
|
}
|
|
116
|
-
|
|
117
|
-
output.header('Route Rule Updated')
|
|
118
|
-
output.success(`${pat} -> ${tgt}`)
|
|
119
|
-
output.kv('File', fileName)
|
|
120
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)
|
|
121
164
|
}
|
|
122
165
|
|
|
123
166
|
run().catch(e => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
})
|
|
167
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
168
|
+
fail(e.message, jsonFlag)
|
|
169
|
+
})
|
package/bin/commands/update.js
CHANGED
|
@@ -77,6 +77,10 @@ meddle update - 检查并升级 meddle
|
|
|
77
77
|
meddle update --auto on|off 启用/禁用自动更新 (默认关闭)
|
|
78
78
|
meddle update status 查看安装方式、当前版本和自动更新开关
|
|
79
79
|
meddle update --help, -h 显示帮助
|
|
80
|
+
|
|
81
|
+
私有仓库二进制更新需要设置 GH_TOKEN 或 GITHUB_TOKEN(优先 GH_TOKEN;
|
|
82
|
+
权限:repo,或 fine-grained contents:read)。也可:
|
|
83
|
+
gh release download -R JonathanLee-LX/meddle -p 'meddle-*'
|
|
80
84
|
`)
|
|
81
85
|
}
|
|
82
86
|
|
package/bin/index.js
CHANGED
|
@@ -59,7 +59,7 @@ if (command === 'mock' || command === 'route' || command === 'session') {
|
|
|
59
59
|
// No command or only flags - start proxy (default behavior)
|
|
60
60
|
require('../index.js')
|
|
61
61
|
} else {
|
|
62
|
-
console.
|
|
62
|
+
console.error(`Unknown command: ${command}`)
|
|
63
63
|
showHelp()
|
|
64
64
|
process.exit(1)
|
|
65
65
|
}
|
|
@@ -106,8 +106,10 @@ Route 命令:
|
|
|
106
106
|
meddle route active 查看当前激活的路由文件
|
|
107
107
|
meddle route active set <file> 设置激活的路由文件
|
|
108
108
|
meddle route create <name> 创建路由文件
|
|
109
|
-
meddle route add <file> <pattern> <target>
|
|
110
|
-
|
|
109
|
+
meddle route add <file> <pattern> <target> [--exclusions=]
|
|
110
|
+
添加路由规则;--exclusions= 清空排除项
|
|
111
|
+
meddle route update <file> <pattern> [<target>] [--exclusions=]
|
|
112
|
+
更新路由规则;--exclusions= 清空排除项
|
|
111
113
|
meddle route delete <file> <pattern> 删除路由规则
|
|
112
114
|
|
|
113
115
|
全局选项:
|
|
@@ -137,6 +139,8 @@ Route 命令:
|
|
|
137
139
|
meddle route preview "https://api.example.com/v1/users" # 预览转发目标
|
|
138
140
|
meddle route preview "https://cdn.com/assets/js/app.js" --file dev-rules --json
|
|
139
141
|
meddle route add dev-rules "api.test.com" "localhost:3000"
|
|
142
|
+
meddle route add dev-rules "api.test.com" "localhost:3000" --exclusions=
|
|
143
|
+
meddle route update dev-rules "api.test.com" --exclusions=
|
|
140
144
|
meddle route active set beta-rules
|
|
141
145
|
|
|
142
146
|
更多信息请访问: https://github.com/JonathanLee-LX/meddle
|
package/bin/lib/api-client.js
CHANGED
|
@@ -25,14 +25,26 @@ async function apiRequest(method, pathname, body, timeoutMs = DEFAULT_TIMEOUT) {
|
|
|
25
25
|
|
|
26
26
|
if (!response.ok) {
|
|
27
27
|
const text = await response.text()
|
|
28
|
-
|
|
28
|
+
const err = new Error(text || response.statusText)
|
|
29
|
+
err.status = response.status
|
|
30
|
+
// Structured errors (e.g. issue #137 RULESET_READONLY) keep their code.
|
|
31
|
+
try {
|
|
32
|
+
const data = JSON.parse(text)
|
|
33
|
+
if (data && typeof data === 'object') {
|
|
34
|
+
if (typeof data.code === 'string') err.code = data.code
|
|
35
|
+
if (typeof data.ruleFile === 'string') err.ruleFile = data.ruleFile
|
|
36
|
+
}
|
|
37
|
+
} catch (_) {}
|
|
38
|
+
throw err
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
const contentType = response.headers.get('content-type')
|
|
32
42
|
if (contentType && contentType.includes('application/json')) {
|
|
33
43
|
return response.json()
|
|
34
44
|
}
|
|
35
|
-
|
|
45
|
+
// Issue #145: `Response.text()` strips a leading UTF-8 BOM, so a rule file
|
|
46
|
+
// read and written back through the API would silently lose it.
|
|
47
|
+
return new TextDecoder('utf-8', { ignoreBOM: true }).decode(await response.arrayBuffer())
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
/**
|
package/bin/lib/file-access.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require('fs')
|
|
6
6
|
const path = require('path')
|
|
7
7
|
const { meddleDir } = require('./proxy-detect')
|
|
8
|
+
const { RulesetReadonlyError } = require('./ruleset-readonly')
|
|
8
9
|
|
|
9
10
|
const routeRulesDir = path.join(meddleDir, 'route-rules')
|
|
10
11
|
const settingsPath = path.join(meddleDir, 'settings.json')
|
|
@@ -51,6 +52,33 @@ function setActiveRuleFileNames(names) {
|
|
|
51
52
|
saveSettings(settings)
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Issue #137: locked rule files (settings.json `readonlyRuleFiles`). Entries
|
|
57
|
+
* whose file no longer exists are dropped silently. A missing field = none locked.
|
|
58
|
+
*/
|
|
59
|
+
function getReadonlyRuleFileNames() {
|
|
60
|
+
const settings = loadSettings()
|
|
61
|
+
if (!Object.prototype.hasOwnProperty.call(settings, 'readonlyRuleFiles')) return []
|
|
62
|
+
const raw = settings.readonlyRuleFiles
|
|
63
|
+
const names = Array.isArray(raw) ? raw.filter((n, i, arr) => typeof n === 'string' && n && arr.indexOf(n) === i) : []
|
|
64
|
+
const live = names.filter((name) => fs.existsSync(path.join(routeRulesDir, `${name}.txt`)))
|
|
65
|
+
if (!Array.isArray(raw) || live.length !== raw.length) {
|
|
66
|
+
settings.readonlyRuleFiles = live
|
|
67
|
+
saveSettings(settings)
|
|
68
|
+
}
|
|
69
|
+
return live
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isRuleFileReadonly(name) {
|
|
73
|
+
const fold = (n) => (process.platform === 'darwin' || process.platform === 'win32' ? n.toLowerCase() : n)
|
|
74
|
+
return getReadonlyRuleFileNames().some((item) => fold(item) === fold(name))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Throws RulesetReadonlyError (code RULESET_READONLY) for a locked rule file. */
|
|
78
|
+
function assertRuleFileWritable(name) {
|
|
79
|
+
if (isRuleFileReadonly(name)) throw new RulesetReadonlyError(name)
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
// ========== Mocks ==========
|
|
55
83
|
|
|
56
84
|
/**
|
|
@@ -62,36 +90,163 @@ function getMocksPath() {
|
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
/**
|
|
65
|
-
*
|
|
93
|
+
* Offset of the first JSON syntax error. Kept in sync with
|
|
94
|
+
* core/mock-handler.ts findJsonErrorOffset (V8 often omits a position).
|
|
66
95
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
function findJsonErrorOffset(t) {
|
|
97
|
+
let i = 0
|
|
98
|
+
const ws = () => { while (i < t.length && ' \t\n\r'.includes(t[i])) i++ }
|
|
99
|
+
const fail = () => { throw i }
|
|
100
|
+
const str = () => {
|
|
101
|
+
i++
|
|
102
|
+
while (i < t.length && t[i] !== '"') {
|
|
103
|
+
if (t[i] === '\\') i++
|
|
104
|
+
else if (t.charCodeAt(i) < 0x20) fail()
|
|
105
|
+
i++
|
|
73
106
|
}
|
|
74
|
-
|
|
75
|
-
|
|
107
|
+
if (i >= t.length) fail()
|
|
108
|
+
i++
|
|
109
|
+
}
|
|
110
|
+
const value = () => {
|
|
111
|
+
ws()
|
|
112
|
+
const c = t[i]
|
|
113
|
+
if (c === '{') {
|
|
114
|
+
i++; ws()
|
|
115
|
+
if (t[i] === '}') { i++; return }
|
|
116
|
+
for (;;) {
|
|
117
|
+
ws(); if (t[i] !== '"') fail(); str(); ws()
|
|
118
|
+
if (t[i] !== ':') fail(); i++; value(); ws()
|
|
119
|
+
if (t[i] === ',') { i++; continue }
|
|
120
|
+
if (t[i] === '}') { i++; return }
|
|
121
|
+
fail()
|
|
122
|
+
}
|
|
123
|
+
} else if (c === '[') {
|
|
124
|
+
i++; ws()
|
|
125
|
+
if (t[i] === ']') { i++; return }
|
|
126
|
+
for (;;) {
|
|
127
|
+
value(); ws()
|
|
128
|
+
if (t[i] === ',') { i++; continue }
|
|
129
|
+
if (t[i] === ']') { i++; return }
|
|
130
|
+
fail()
|
|
131
|
+
}
|
|
132
|
+
} else if (c === '"') str()
|
|
133
|
+
else if (/[-0-9]/.test(c || '')) {
|
|
134
|
+
const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(t.slice(i))
|
|
135
|
+
if (!m) fail(); i += m[0].length
|
|
136
|
+
} else {
|
|
137
|
+
for (const w of ['true', 'false', 'null']) if (t.startsWith(w, i)) { i += w.length; return }
|
|
138
|
+
fail()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
value(); ws()
|
|
143
|
+
return i < t.length ? i : t.length
|
|
144
|
+
} catch (at) {
|
|
145
|
+
return typeof at === 'number' ? Math.min(at, t.length) : t.length
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** `第 <line> 行第 <column> 列:<short message>`, same rules as core/mock-handler.ts. */
|
|
150
|
+
function describeMocksParseFailure(text, err) {
|
|
151
|
+
const message = String((err && err.message) || err)
|
|
152
|
+
const body = text.replace(/^\uFEFF/, '')
|
|
153
|
+
let line = null
|
|
154
|
+
let column = null
|
|
155
|
+
const lc = /\(line (\d+) column (\d+)\)/.exec(message)
|
|
156
|
+
const pos = /at position (\d+)/.exec(message)
|
|
157
|
+
const offset = lc ? null : pos ? Number(pos[1]) : findJsonErrorOffset(body)
|
|
158
|
+
if (lc) {
|
|
159
|
+
line = Number(lc[1]); column = Number(lc[2])
|
|
160
|
+
} else {
|
|
161
|
+
const before = body.slice(0, offset == null ? body.length : offset).split('\n')
|
|
162
|
+
line = before.length
|
|
163
|
+
column = before[before.length - 1].length + 1
|
|
164
|
+
}
|
|
165
|
+
const short = message.replace(/\s*\(line \d+ column \d+\)/, '').split(/,\s*(?:\.\.\.)?"/s)[0].replace(/\s+in JSON at position \d+/, '').trim()
|
|
166
|
+
const loc = line != null ? `第 ${line} 行第 ${column} 列:` : ''
|
|
167
|
+
return `${loc}${short}`
|
|
76
168
|
}
|
|
77
169
|
|
|
78
170
|
/**
|
|
79
|
-
*
|
|
171
|
+
* Read mocks.json. Missing file → null. Empty / whitespace → {}.
|
|
172
|
+
* Invalid JSON throws MOCKS_FILE_INVALID and must not be treated as [].
|
|
173
|
+
* `message` refuses a write; `listMessage` is what `mock list` prints.
|
|
80
174
|
*/
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
175
|
+
function readMocksDocument(mocksPath) {
|
|
176
|
+
if (!fs.existsSync(mocksPath)) return null
|
|
177
|
+
const text = fs.readFileSync(mocksPath, 'utf8').replace(/^\uFEFF/, '')
|
|
178
|
+
if (text.trim() === '') return {}
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(text)
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const detail = describeMocksParseFailure(text, err)
|
|
183
|
+
const error = new Error(`Mock 文件格式错误,已禁止写入以免覆盖: ${mocksPath} ${detail}`)
|
|
184
|
+
error.code = 'MOCKS_FILE_INVALID'
|
|
185
|
+
error.filePath = mocksPath
|
|
186
|
+
error.detail = detail
|
|
187
|
+
error.listMessage = `Mock 文件格式错误: ${mocksPath} ${detail}`
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function rulesFromDocument(data) {
|
|
193
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return []
|
|
194
|
+
return Array.isArray(data.rules) ? data.rules : []
|
|
195
|
+
}
|
|
85
196
|
|
|
86
|
-
|
|
197
|
+
function metaFromDocument(data) {
|
|
198
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return { nextId: 0, lastId: 0 }
|
|
199
|
+
return { nextId: Number(data.nextId) || 0, lastId: Number(data.lastId) || 0 }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Load mock rules from file. A corrupt file throws; it is not an empty list.
|
|
204
|
+
*/
|
|
205
|
+
function loadMockRules() {
|
|
206
|
+
const data = readMocksDocument(getMocksPath())
|
|
207
|
+
if (data == null) return []
|
|
208
|
+
return rulesFromDocument(data)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readMocksFileMeta(mocksPath) {
|
|
212
|
+
const data = readMocksDocument(mocksPath)
|
|
213
|
+
if (data == null) return { nextId: 0, lastId: 0 }
|
|
214
|
+
return metaFromDocument(data)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function maxRuleId(rules) {
|
|
87
218
|
let maxId = 0
|
|
88
219
|
rules.forEach(r => {
|
|
89
|
-
if (r.id > maxId) maxId = r.id
|
|
220
|
+
if (r && Number.isInteger(r.id) && r.id > maxId) maxId = r.id
|
|
90
221
|
})
|
|
222
|
+
return maxId
|
|
223
|
+
}
|
|
91
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Next id to hand out. Shares the server's `nextId` field so ids stay
|
|
227
|
+
* monotonic across CLI file-mode and the running proxy (issue #115); also
|
|
228
|
+
* honours the legacy CLI `lastId` field.
|
|
229
|
+
*/
|
|
230
|
+
function nextMockId(rules, meta) {
|
|
231
|
+
return Math.max(1, meta.nextId || 0, (meta.lastId || 0) + 1, maxRuleId(rules) + 1)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Save mock rules to file
|
|
236
|
+
*/
|
|
237
|
+
function saveMockRules(rules, nextId) {
|
|
238
|
+
const mocksPath = getMocksPath()
|
|
239
|
+
const dir = path.dirname(mocksPath)
|
|
240
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
241
|
+
|
|
242
|
+
const meta = readMocksFileMeta(mocksPath)
|
|
243
|
+
const seq = Math.max(nextId || 0, nextMockId(rules, meta))
|
|
92
244
|
const data = {
|
|
245
|
+
// `nextId` is what the proxy reads; keep it so deleting the newest rule
|
|
246
|
+
// does not make its id reusable (issue #115). `lastId` kept for older CLIs.
|
|
247
|
+
nextId: seq,
|
|
93
248
|
rules,
|
|
94
|
-
lastId:
|
|
249
|
+
lastId: seq - 1
|
|
95
250
|
}
|
|
96
251
|
fs.writeFileSync(mocksPath, JSON.stringify(data, null, 2), 'utf8')
|
|
97
252
|
}
|
|
@@ -103,23 +258,14 @@ function addMockRule(rule) {
|
|
|
103
258
|
const rules = loadMockRules()
|
|
104
259
|
const mocksPath = getMocksPath()
|
|
105
260
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (fs.existsSync(mocksPath)) {
|
|
110
|
-
const data = JSON.parse(fs.readFileSync(mocksPath, 'utf8'))
|
|
111
|
-
nextId = (data.lastId || 0) + 1
|
|
112
|
-
}
|
|
113
|
-
} catch (_) {}
|
|
114
|
-
if (rules.length > 0) {
|
|
115
|
-
const maxId = Math.max(...rules.map(r => r.id))
|
|
116
|
-
if (maxId >= nextId) nextId = maxId + 1
|
|
117
|
-
}
|
|
261
|
+
// Issue #115: never reuse an existing (or previously handed-out) id, and
|
|
262
|
+
// ignore any id the caller put on the rule — add never overwrites.
|
|
263
|
+
const nextId = nextMockId(rules, readMocksFileMeta(mocksPath))
|
|
118
264
|
|
|
119
|
-
rule
|
|
120
|
-
rules.push(
|
|
121
|
-
saveMockRules(rules)
|
|
122
|
-
return
|
|
265
|
+
const created = { ...rule, id: nextId }
|
|
266
|
+
rules.push(created)
|
|
267
|
+
saveMockRules(rules, nextId + 1)
|
|
268
|
+
return created
|
|
123
269
|
}
|
|
124
270
|
|
|
125
271
|
/**
|
|
@@ -167,6 +313,30 @@ function getRuleFilePath(name) {
|
|
|
167
313
|
/**
|
|
168
314
|
* List all rule files
|
|
169
315
|
*/
|
|
316
|
+
function getRuleFileOrderNames() {
|
|
317
|
+
const settings = loadSettings()
|
|
318
|
+
const arr = settings.ruleFileOrder
|
|
319
|
+
return Array.isArray(arr) ? arr.filter((name) => typeof name === 'string') : []
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function resolveRuleFileOrder(diskNames, storedOrder, activeNames) {
|
|
323
|
+
const diskSet = new Set(diskNames)
|
|
324
|
+
const ordered = []
|
|
325
|
+
const seen = new Set()
|
|
326
|
+
const pushUnique = (name) => {
|
|
327
|
+
if (!diskSet.has(name) || seen.has(name)) return
|
|
328
|
+
ordered.push(name)
|
|
329
|
+
seen.add(name)
|
|
330
|
+
}
|
|
331
|
+
if (storedOrder.length > 0) {
|
|
332
|
+
for (const name of storedOrder) pushUnique(name)
|
|
333
|
+
} else {
|
|
334
|
+
for (const name of activeNames) pushUnique(name)
|
|
335
|
+
}
|
|
336
|
+
for (const name of diskNames) pushUnique(name)
|
|
337
|
+
return ordered
|
|
338
|
+
}
|
|
339
|
+
|
|
170
340
|
function listRuleFiles() {
|
|
171
341
|
ensureRouteRulesDir()
|
|
172
342
|
const activeNames = getActiveRuleFileNames()
|
|
@@ -175,7 +345,10 @@ function listRuleFiles() {
|
|
|
175
345
|
.filter(f => f.endsWith('.txt'))
|
|
176
346
|
.map(f => f.replace(/\.txt$/, ''))
|
|
177
347
|
|
|
178
|
-
|
|
348
|
+
const ordered = resolveRuleFileOrder(files, getRuleFileOrderNames(), activeNames)
|
|
349
|
+
const readonlyNames = new Set(getReadonlyRuleFileNames())
|
|
350
|
+
|
|
351
|
+
return ordered.map(name => {
|
|
179
352
|
const filePath = getRuleFilePath(name)
|
|
180
353
|
let ruleCount = 0
|
|
181
354
|
let excludeCount = 0
|
|
@@ -191,6 +364,7 @@ function listRuleFiles() {
|
|
|
191
364
|
return {
|
|
192
365
|
name,
|
|
193
366
|
enabled: activeNames.includes(name),
|
|
367
|
+
readonly: readonlyNames.has(name),
|
|
194
368
|
ruleCount,
|
|
195
369
|
excludeCount
|
|
196
370
|
}
|
|
@@ -215,6 +389,7 @@ function getRuleFileContent(name) {
|
|
|
215
389
|
*/
|
|
216
390
|
function saveRuleFileContent(name, content) {
|
|
217
391
|
ensureRouteRulesDir()
|
|
392
|
+
assertRuleFileWritable(name)
|
|
218
393
|
const filePath = getRuleFilePath(name)
|
|
219
394
|
fs.writeFileSync(filePath, content, 'utf8')
|
|
220
395
|
}
|
|
@@ -231,6 +406,8 @@ function createRuleFile(name, content = '', enabled = true) {
|
|
|
231
406
|
throw new Error(`规则文件 "${safeName}" 已存在`)
|
|
232
407
|
}
|
|
233
408
|
|
|
409
|
+
// Drop stale lock entries so a new file never inherits an old lock.
|
|
410
|
+
getReadonlyRuleFileNames()
|
|
234
411
|
fs.writeFileSync(filePath, content, 'utf8')
|
|
235
412
|
|
|
236
413
|
if (enabled) {
|
|
@@ -242,7 +419,7 @@ function createRuleFile(name, content = '', enabled = true) {
|
|
|
242
419
|
}
|
|
243
420
|
|
|
244
421
|
const ruleCount = content.trim() ? Object.keys(parseEprc(content)).length : 0
|
|
245
|
-
return { name: safeName, enabled, ruleCount }
|
|
422
|
+
return { name: safeName, enabled, readonly: false, ruleCount }
|
|
246
423
|
}
|
|
247
424
|
|
|
248
425
|
/**
|
|
@@ -253,6 +430,7 @@ function deleteRuleFile(name) {
|
|
|
253
430
|
if (!fs.existsSync(filePath)) {
|
|
254
431
|
throw new Error('规则文件不存在')
|
|
255
432
|
}
|
|
433
|
+
assertRuleFileWritable(name)
|
|
256
434
|
fs.unlinkSync(filePath)
|
|
257
435
|
|
|
258
436
|
const activeNames = getActiveRuleFileNames()
|
|
@@ -274,6 +452,7 @@ function setRuleFileEnabled(name, enabled) {
|
|
|
274
452
|
|
|
275
453
|
const activeNames = getActiveRuleFileNames()
|
|
276
454
|
const idx = activeNames.indexOf(name)
|
|
455
|
+
if ((enabled && idx === -1) || (!enabled && idx !== -1)) assertRuleFileWritable(name)
|
|
277
456
|
|
|
278
457
|
if (enabled && idx === -1) {
|
|
279
458
|
activeNames.push(name)
|
|
@@ -309,6 +488,9 @@ module.exports = {
|
|
|
309
488
|
saveSettings,
|
|
310
489
|
getActiveRuleFileNames,
|
|
311
490
|
setActiveRuleFileNames,
|
|
491
|
+
getReadonlyRuleFileNames,
|
|
492
|
+
isRuleFileReadonly,
|
|
493
|
+
assertRuleFileWritable,
|
|
312
494
|
|
|
313
495
|
// Mocks
|
|
314
496
|
getMocksPath,
|