@jonathanleelx/meddle 0.5.0-beta.23 → 0.5.0-beta.24
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 +1 -0
- package/README.md +16 -0
- package/bin/commands/route/active.js +14 -1
- package/bin/commands/route/add.js +27 -30
- package/bin/commands/route/delete.js +31 -30
- package/bin/commands/route/index.js +4 -0
- package/bin/commands/route/list.js +2 -2
- package/bin/commands/route/show.js +10 -1
- package/bin/commands/route/update.js +32 -30
- package/bin/lib/api-client.js +14 -2
- package/bin/lib/file-access.js +39 -1
- package/bin/lib/rule-file-edit.js +263 -0
- package/bin/lib/ruleset-readonly.js +61 -0
- package/dist/core/mock-handler.d.ts.map +1 -1
- package/dist/core/mock-handler.js +34 -5
- package/dist/core/mock-handler.js.map +1 -1
- package/dist/core/rule-file-edit.d.ts +81 -0
- package/dist/core/rule-file-edit.d.ts.map +1 -0
- package/dist/core/rule-file-edit.js +243 -0
- package/dist/core/rule-file-edit.js.map +1 -0
- package/dist/core/static-server.d.ts.map +1 -1
- package/dist/core/static-server.js +15 -13
- package/dist/core/static-server.js.map +1 -1
- package/dist/server/agent/tools.d.ts.map +1 -1
- package/dist/server/agent/tools.js +18 -17
- package/dist/server/agent/tools.js.map +1 -1
- package/dist/server/app-info.d.ts +14 -0
- package/dist/server/app-info.d.ts.map +1 -1
- package/dist/server/app-info.js +33 -1
- package/dist/server/app-info.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +51 -9
- package/dist/server/config.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/rule-files.d.ts +23 -0
- package/dist/server/rule-files.d.ts.map +1 -1
- package/dist/server/rule-files.js +169 -81
- 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/mcp-server.js +87 -88
- package/package.json +1 -1
- package/web/dist/assets/{detail-panel-D4JSaFH3.js → detail-panel-D56nCma2.js} +3 -3
- package/web/dist/assets/dist-BQN0rzUS.js +1 -0
- package/web/dist/assets/{global-panel-shell-DXvcxmbd.js → global-panel-shell-SKPshqTf.js} +3 -3
- package/web/dist/assets/{health-panel-Bo0eD8_R.js → health-panel-CvI-VsvG.js} +1 -1
- package/web/dist/assets/index-C6dPxxT3.css +2 -0
- package/web/dist/assets/index-Dcm171u1.js +21 -0
- package/web/dist/assets/{mobile-proxy-panel-FQwFQFmS.js → mobile-proxy-panel-cpkvsdBT.js} +2 -2
- package/web/dist/assets/mock-config-CtZ866iI.js +1 -0
- package/web/dist/assets/{mock-editor-panel-kx0bzGv4.js → mock-editor-panel-CweYBdeM.js} +3 -3
- package/web/dist/assets/{mocks-file-missing-note-BvL7-uzS.js → mocks-file-missing-note-C-QJ8Bpm.js} +1 -1
- package/web/dist/assets/{monaco-editor-BLHN8gCU.js → monaco-editor-Cge8h-Ca.js} +2 -2
- package/web/dist/assets/pane-bar-Dvi-MHuB.js +1 -0
- package/web/dist/assets/{pane-split-DfQcgd1v.js → pane-split-ChCHxR7K.js} +1 -1
- package/web/dist/assets/panel-heading-Crmy9fo0.js +1 -0
- package/web/dist/assets/plugin-code-editor-VaL2lVyD.js +2 -0
- package/web/dist/assets/plugin-config-DSiOk77l.js +2 -0
- package/web/dist/assets/{plugin-generator-B5FRaaKc.js → plugin-generator-iAksMY2Q.js} +2 -2
- package/web/dist/assets/plugin-test-dialog-Cx6ifDnm.js +7 -0
- package/web/dist/assets/route-preview-T0s6si4V.js +1 -0
- package/web/dist/assets/{rule-ai-assistant-panel-BEmXPzli.js → rule-ai-assistant-panel-DzfobTWM.js} +2 -2
- package/web/dist/assets/rule-config-ESpfkJg6.js +15 -0
- package/web/dist/assets/{rule-overview-panel-DsNEU8L1.js → rule-overview-panel-BswSByar.js} +1 -1
- package/web/dist/assets/settings-panel-Cj-1Z8Ow.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/index.html +11 -9
- package/web/dist/assets/index-Cbis1y9h.js +0 -18
- package/web/dist/assets/index-dKaIrLee.css +0 -2
- package/web/dist/assets/mock-config-CL9nXIkx.js +0 -1
- package/web/dist/assets/pane-bar-BSzN6D93.js +0 -1
- package/web/dist/assets/panel-heading-Dev_soZA.js +0 -1
- package/web/dist/assets/plugin-code-editor-uI_-iAXY.js +0 -2
- package/web/dist/assets/plugin-config-BpfIX4Uk.js +0 -2
- package/web/dist/assets/plugin-test-dialog-Cptd2bs0.js +0 -7
- package/web/dist/assets/route-preview-DEc50Y_R.js +0 -1
- package/web/dist/assets/rule-config-DQkdF3tr.js +0 -15
- package/web/dist/assets/select-CFTzQ8Uy.js +0 -1
- package/web/dist/assets/settings-panel-C1v0UmZQ.js +0 -1
- /package/web/dist/assets/{alert-DFypnYT1.js → alert-B514Frte.js} +0 -0
- /package/web/dist/assets/{babel-DCr4VA1c.js → babel-bwZm0qi3.js} +0 -0
- /package/web/dist/assets/{body-diff-view-CCwbvDYx.js → body-diff-view-SfLCLzs9.js} +0 -0
- /package/web/dist/assets/{clipboard-VhPXNN5m.js → clipboard-Bt1qY01V.js} +0 -0
- /package/web/dist/assets/{dist-Dk9a2sJl.js → dist-DLQKRjuW.js} +0 -0
- /package/web/dist/assets/{editor-pane-actions-BGttNlkS.js → editor-pane-actions-_ckwcCII.js} +0 -0
- /package/web/dist/assets/{estree-qO_m2Qaw.js → estree-MatbZDwq.js} +0 -0
- /package/web/dist/assets/{html-VDGahtyk.js → html-CCrsbFYX.js} +0 -0
- /package/web/dist/assets/{postcss-BUm8EKMw.js → postcss-BxN7Gr8n.js} +0 -0
- /package/web/dist/assets/{standalone-Bj8SSd8Q.js → standalone-DsnFnsqM.js} +0 -0
- /package/web/dist/assets/{syntax-highlight-Dg_CTbXd.js → syntax-highlight-BFgN_C0Y.js} +0 -0
- /package/web/dist/assets/{table-B10JkPhg.js → table-DaXD6jJv.js} +0 -0
- /package/web/dist/assets/{textarea-BjWF0pk4.js → textarea-9wB8Hsz-.js} +0 -0
- /package/web/dist/assets/{wand-sparkles-DDGjzjnX.js → wand-sparkles-Bmt4dDOf.js} +0 -0
- /package/web/dist/assets/{wifi-QkTcTrED.js → wifi-Cjj_Br1u.js} +0 -0
- /package/web/dist/assets/{zap-CEVoVkAg.js → zap-BuOtogAv.js} +0 -0
package/CONFIG_STRUCTURE.md
CHANGED
|
@@ -204,6 +204,7 @@ https://localhost:13001/micro/app?a=1
|
|
|
204
204
|
| `detailBodySizeKB` | 日志详情保存的请求/响应体截断阈值(KB),默认 `256`;调大可保留更大 body 便于排查,但会增加内存占用(最多 `MAX_DETAIL_SIZE=200` 条同时驻留)。作用范围为单个 session,解析顺序:当前 session 的 `settings.json` → 默认 session(`~/.meddle/settings.json`,仅非默认 session 继承)→ 环境变量 `MEDDLE_MAX_DETAIL_BODY_KB` → 默认 `256`。修改后下次请求即生效(基于 mtime 缓存检测) |
|
|
205
205
|
| `activeRuleFiles` | 当前启用的路由规则文件名称,顺序即合并顺序 |
|
|
206
206
|
| `ruleFileOrder` | Web 规则文件 Tab 的完整显示顺序(含禁用文件);启用文件的合并顺序与其在该数组中的相对顺序一致 |
|
|
207
|
+
| `readonlyRuleFiles` | 只读(加锁)的规则文件名称。可选,缺省即全部可读写。只能在 Web 界面中修改;锁住的规则文件拒绝 CLI / MCP / API 的一切写入。见 [只读规则集](./docs/RULESET_READONLY.md) |
|
|
207
208
|
| `mocksFilePath` | 自定义 Mock JSON 路径;空值使用默认位置 |
|
|
208
209
|
| `pluginMode` | 插件 Pipeline 模式:`off`、`shadow` 或 `on` |
|
|
209
210
|
| `clientAliases` | 客户端 IP 到设备名称的映射 |
|
package/README.md
CHANGED
|
@@ -181,6 +181,22 @@ meddle --remote --no-intercept-https
|
|
|
181
181
|
|
|
182
182
|
所有 `mock_rule_*`、`route_rule_*`、`route_preview` 工具均支持可选 `session` 参数,指定后可操作对应 session 的规则,不传则操作默认 session。
|
|
183
183
|
|
|
184
|
+
**路由规则写入**(`route_rule_add` / `route_rule_update` / `route_rule_delete`)只改包含该 pattern 的那一行,规则文件里的注释、空行、`//` 禁用规则和其他规则(连同排除项)都按原样保留:
|
|
185
|
+
|
|
186
|
+
- **不传 `exclusions`**:保留该规则原有的排除规则(`route_rule_add` 覆盖已有规则时也一样;新规则则没有排除规则)。
|
|
187
|
+
- **传 `exclusions: []`**:清空该规则的排除规则。
|
|
188
|
+
- 传非空数组:用它替换原有的排除规则。
|
|
189
|
+
- 如果一行写了多个 pattern,修改或删除其中一个时,若剩下的 pattern 以 `//` 或 `#` 开头(拆开后会被当成注释),写入会被拒绝并提示「该行剩余的 pattern 以 // 或 # 开头,拆开后会被当成注释,请用文本编辑器手动修改」,规则文件不变。
|
|
190
|
+
- 新增规则的 pattern 不能以 `#` 或 `//` 开头(例如 `#foo.com`、`//foo.com`),否则写进文件后会被当成注释或禁用行。写入会被拒绝并提示「规则不能以 // 或 # 开头,否则会被当成禁用行或注释。要写注释或禁用规则,请用文本编辑器。」,规则文件不变。
|
|
191
|
+
- CLI 没有单独的排除规则参数:`meddle route add/update` 的 pattern 参数里写了 `!path`(如 `"api.example.com !/health"`)就替换,不写就保留原有的;CLI 无法清空排除规则,需要清空时用 MCP 传 `[]`,或在 Web 界面里编辑。
|
|
192
|
+
|
|
193
|
+
**只读规则集**([详细说明](./docs/RULESET_READONLY.md)):规则集(规则文件)可以在 Meddle Web 界面中加只读锁,防止 Agent 误改。
|
|
194
|
+
|
|
195
|
+
- `route_rule_list` 的结果带 `readonly: true|false`。
|
|
196
|
+
- 锁住后 `route_rule_add` / `route_rule_update` / `route_rule_delete` / `route_rule_active_set`(会改变锁住规则集的启用状态时)全部被拒绝,规则文件不变,工具返回 `isError: true` 与 `{"error":{"code":"RULESET_READONLY","message":"该规则集已设为只读,只能在 Meddle 界面中解锁","ruleFile":"…"}}`。CLI 同样拒绝,退出码为 `3`。
|
|
197
|
+
- **CLI 和 MCP 都没有加锁 / 解锁的命令或工具**,只能由人在 Meddle 界面中操作。界面使用页面令牌 + 同源校验;这只是防误改,不是安全隔离(可被 `curl` 取页面令牌并伪造 `Origin` 绕过,有 shell 的 Agent 也能直接改磁盘文件)。
|
|
198
|
+
- 锁住的规则集不能重命名,需先在 Meddle 界面中解锁再改名;在 Meddle 之外直接改文件名视为新规则集(可读写),旧锁记录自动清除。
|
|
199
|
+
|
|
184
200
|
### 多 Session 隔离(预览功能)
|
|
185
201
|
|
|
186
202
|
多 Session 允许同时运行多个独立的 `meddle` 代理进程,每个进程拥有独立的配置目录(MEDDLE_HOME)、端口、路由规则和 Mock 规则,适用于多个 Agent 或项目需要隔离代理环境的场景。
|
|
@@ -5,7 +5,13 @@
|
|
|
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 { listRuleFiles, getActiveRuleFileNames,
|
|
8
|
+
const { listRuleFiles, getActiveRuleFileNames, setRuleFileEnabled } = require('../../lib/file-access')
|
|
9
|
+
const { isRulesetReadonlyError, exitRulesetReadonly, assertListedFilesWritable } = require('../../lib/ruleset-readonly')
|
|
10
|
+
|
|
11
|
+
/** Names whose enabled state `active set <file>` would change. */
|
|
12
|
+
function pendingToggles(files, target) {
|
|
13
|
+
return files.filter((f) => !!f.enabled !== (f.name === target)).map((f) => f.name)
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
// Parse arguments (skip 'route' and 'active' from argv)
|
|
11
17
|
const args = process.argv.slice(4)
|
|
@@ -39,6 +45,9 @@ async function run() {
|
|
|
39
45
|
process.exit(1)
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
// Issue #137: refuse up front so a locked ruleset never leaves a half-applied switch.
|
|
49
|
+
assertListedFilesWritable(files, pendingToggles(files, targetFile))
|
|
50
|
+
|
|
42
51
|
for (const f of files) {
|
|
43
52
|
const nextEnabled = f.name === targetFile
|
|
44
53
|
if (!!f.enabled === nextEnabled) continue
|
|
@@ -53,6 +62,7 @@ async function run() {
|
|
|
53
62
|
output.header('Active Route File Set')
|
|
54
63
|
output.success(targetFile)
|
|
55
64
|
} catch (e) {
|
|
65
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: targetFile })
|
|
56
66
|
// Fallback to file mode
|
|
57
67
|
const files = listRuleFiles()
|
|
58
68
|
const target = files.find(f => f.name === targetFile)
|
|
@@ -61,6 +71,7 @@ async function run() {
|
|
|
61
71
|
process.exit(1)
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
assertListedFilesWritable(files, pendingToggles(files, targetFile))
|
|
64
75
|
// Disable all, then enable target
|
|
65
76
|
files.forEach(f => setRuleFileEnabled(f.name, f.name === targetFile))
|
|
66
77
|
|
|
@@ -83,6 +94,7 @@ async function run() {
|
|
|
83
94
|
process.exit(1)
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
assertListedFilesWritable(files, pendingToggles(files, targetFile))
|
|
86
98
|
files.forEach(f => setRuleFileEnabled(f.name, f.name === targetFile))
|
|
87
99
|
|
|
88
100
|
output.warnFileOnlyMode()
|
|
@@ -140,6 +152,7 @@ async function run() {
|
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
run().catch(e => {
|
|
155
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: targetFile })
|
|
143
156
|
output.error(e.message)
|
|
144
157
|
process.exit(1)
|
|
145
158
|
})
|
|
@@ -5,7 +5,9 @@
|
|
|
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, splitPatternArg } = require('../../lib/rule-file-edit')
|
|
10
|
+
const { isRulesetReadonlyError, exitRulesetReadonly } = require('../../lib/ruleset-readonly')
|
|
9
11
|
|
|
10
12
|
// Parse arguments (skip 'route' and 'add' from argv)
|
|
11
13
|
const args = process.argv.slice(4)
|
|
@@ -18,6 +20,24 @@ const fileName = positionalArgs[0]
|
|
|
18
20
|
const pattern = positionalArgs[1]
|
|
19
21
|
const target = positionalArgs[2]
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Issue #145: edit only the line(s) holding this pattern; comments, blank
|
|
25
|
+
* lines and other rules (with their `!exclusions`) keep their exact bytes.
|
|
26
|
+
* Exclusions may be given inside the pattern argument ("app.example.com !/api").
|
|
27
|
+
*/
|
|
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
|
+
}
|
|
34
|
+
let next = text
|
|
35
|
+
for (const p of patterns) {
|
|
36
|
+
next = addRouteRule(next, { pattern: p, target: target.trim(), exclusions: exclusions.length ? exclusions : undefined }).content
|
|
37
|
+
}
|
|
38
|
+
return next
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
async function run() {
|
|
22
42
|
if (!fileName) {
|
|
23
43
|
output.error('Missing required argument: <file>')
|
|
@@ -42,17 +62,7 @@ async function run() {
|
|
|
42
62
|
// Get current content
|
|
43
63
|
let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
|
|
44
64
|
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)
|
|
65
|
+
const newContent = applyAdd(text)
|
|
56
66
|
await apiPut(`/api/rule-files/${encodeURIComponent(fileName)}/content`, { content: newContent })
|
|
57
67
|
|
|
58
68
|
if (jsonFlag) {
|
|
@@ -64,6 +74,8 @@ async function run() {
|
|
|
64
74
|
output.success(`${pat} -> ${tgt}`)
|
|
65
75
|
output.kv('File', fileName)
|
|
66
76
|
} catch (e) {
|
|
77
|
+
// Issue #137: a locked ruleset is a final answer, never a cue to write the file directly.
|
|
78
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
67
79
|
// Fallback to file mode
|
|
68
80
|
const content = getRuleFileContent(fileName)
|
|
69
81
|
if (!content) {
|
|
@@ -71,15 +83,7 @@ async function run() {
|
|
|
71
83
|
process.exit(1)
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
const
|
|
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)
|
|
86
|
+
const newContent = applyAdd(content)
|
|
83
87
|
saveRuleFileContent(fileName, newContent)
|
|
84
88
|
output.warnFileOnlyMode()
|
|
85
89
|
|
|
@@ -104,15 +108,7 @@ async function run() {
|
|
|
104
108
|
process.exit(1)
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
const
|
|
108
|
-
const bm = pat.match(/\[([^\]]+)\]/)
|
|
109
|
-
if (bm) {
|
|
110
|
-
ruleMap[pat.replace(bm[0], bm[1])] = tgt + bm[0]
|
|
111
|
-
} else {
|
|
112
|
-
ruleMap[pat] = tgt
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
111
|
+
const newContent = applyAdd(content)
|
|
116
112
|
saveRuleFileContent(fileName, newContent)
|
|
117
113
|
output.warnFileOnlyMode()
|
|
118
114
|
|
|
@@ -132,6 +128,7 @@ async function run() {
|
|
|
132
128
|
}
|
|
133
129
|
|
|
134
130
|
run().catch(e => {
|
|
131
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
135
132
|
output.error(e.message)
|
|
136
133
|
process.exit(1)
|
|
137
134
|
})
|
|
@@ -5,7 +5,9 @@
|
|
|
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, splitPatternArg } = require('../../lib/rule-file-edit')
|
|
10
|
+
const { isRulesetReadonlyError, exitRulesetReadonly } = require('../../lib/ruleset-readonly')
|
|
9
11
|
|
|
10
12
|
// Parse arguments (skip 'route' and 'delete' from argv)
|
|
11
13
|
const args = process.argv.slice(4)
|
|
@@ -17,6 +19,28 @@ const positionalArgs = args.filter(a => !a.startsWith('--'))
|
|
|
17
19
|
const fileName = positionalArgs[0]
|
|
18
20
|
const pattern = positionalArgs[1]
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Issue #145: remove only this pattern; every other line (comments, blank
|
|
24
|
+
* lines, other rules and their `!exclusions`) keeps its exact bytes.
|
|
25
|
+
*/
|
|
26
|
+
function applyDelete(text) {
|
|
27
|
+
const { patterns } = splitPatternArg(pattern)
|
|
28
|
+
if (patterns.length === 0) {
|
|
29
|
+
output.error('Missing required argument: <pattern>')
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
let next = text
|
|
33
|
+
for (const p of patterns) {
|
|
34
|
+
const result = deleteRouteRule(next, { pattern: p })
|
|
35
|
+
if (!result.found) {
|
|
36
|
+
output.error(`Pattern not found: ${pattern.trim()}`)
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
next = result.content
|
|
40
|
+
}
|
|
41
|
+
return next
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
async function run() {
|
|
21
45
|
if (!fileName) {
|
|
22
46
|
output.error('Missing required argument: <file>')
|
|
@@ -31,23 +55,11 @@ async function run() {
|
|
|
31
55
|
|
|
32
56
|
const pat = pattern.trim()
|
|
33
57
|
|
|
34
|
-
// Handle [marker] syntax for lookup
|
|
35
|
-
const bm = pat.match(/\[([^\]]+)\]/)
|
|
36
|
-
const internalKey = bm ? pat.replace(bm[0], bm[1]) : pat
|
|
37
|
-
|
|
38
58
|
if (running) {
|
|
39
59
|
try {
|
|
40
60
|
let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
|
|
41
61
|
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)
|
|
62
|
+
const newContent = applyDelete(text)
|
|
51
63
|
await apiPut(`/api/rule-files/${encodeURIComponent(fileName)}/content`, { content: newContent })
|
|
52
64
|
|
|
53
65
|
if (jsonFlag) {
|
|
@@ -59,6 +71,8 @@ async function run() {
|
|
|
59
71
|
output.success(pat)
|
|
60
72
|
output.kv('File', fileName)
|
|
61
73
|
} catch (e) {
|
|
74
|
+
// Issue #137: a locked ruleset is a final answer, never a cue to write the file directly.
|
|
75
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
62
76
|
// Fallback to file mode
|
|
63
77
|
const content = getRuleFileContent(fileName)
|
|
64
78
|
if (!content) {
|
|
@@ -66,14 +80,7 @@ async function run() {
|
|
|
66
80
|
process.exit(1)
|
|
67
81
|
}
|
|
68
82
|
|
|
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)
|
|
83
|
+
const newContent = applyDelete(content)
|
|
77
84
|
saveRuleFileContent(fileName, newContent)
|
|
78
85
|
output.warnFileOnlyMode()
|
|
79
86
|
|
|
@@ -98,14 +105,7 @@ async function run() {
|
|
|
98
105
|
process.exit(1)
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
const
|
|
102
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
103
|
-
output.error(`Pattern not found: ${pat}`)
|
|
104
|
-
process.exit(1)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
delete ruleMap[internalKey]
|
|
108
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
108
|
+
const newContent = applyDelete(content)
|
|
109
109
|
saveRuleFileContent(fileName, newContent)
|
|
110
110
|
output.warnFileOnlyMode()
|
|
111
111
|
|
|
@@ -125,6 +125,7 @@ async function run() {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
run().catch(e => {
|
|
128
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
128
129
|
output.error(e.message)
|
|
129
130
|
process.exit(1)
|
|
130
131
|
})
|
|
@@ -43,6 +43,10 @@ Route Commands:
|
|
|
43
43
|
meddle route update <file> <pattern> <target> Update a rule in file
|
|
44
44
|
meddle route delete <file> <pattern> Delete a rule from file
|
|
45
45
|
|
|
46
|
+
Read-only route files:
|
|
47
|
+
Files locked in the Meddle web UI reject add/update/delete and active set
|
|
48
|
+
(error RULESET_READONLY, exit code 3). Lock/unlock is only available in the web UI.
|
|
49
|
+
|
|
46
50
|
Examples:
|
|
47
51
|
meddle route list
|
|
48
52
|
meddle route show dev-rules
|
|
@@ -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)
|
|
@@ -5,7 +5,9 @@
|
|
|
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 { updateRouteRule, splitPatternArg } = require('../../lib/rule-file-edit')
|
|
10
|
+
const { isRulesetReadonlyError, exitRulesetReadonly } = require('../../lib/ruleset-readonly')
|
|
9
11
|
|
|
10
12
|
// Parse arguments (skip 'route' and 'update' from argv)
|
|
11
13
|
const args = process.argv.slice(4)
|
|
@@ -18,6 +20,29 @@ const fileName = positionalArgs[0]
|
|
|
18
20
|
const pattern = positionalArgs[1]
|
|
19
21
|
const newTarget = positionalArgs[2]
|
|
20
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Issue #145: rewrite only the line(s) holding this pattern. The rule keeps its
|
|
25
|
+
* own `!exclusions` unless new ones are given in the pattern argument; every
|
|
26
|
+
* other line keeps its exact bytes.
|
|
27
|
+
*/
|
|
28
|
+
function applyUpdate(text) {
|
|
29
|
+
const { patterns, exclusions } = splitPatternArg(pattern)
|
|
30
|
+
if (patterns.length === 0) {
|
|
31
|
+
output.error('Missing required argument: <pattern>')
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
let next = text
|
|
35
|
+
for (const p of patterns) {
|
|
36
|
+
const result = updateRouteRule(next, { pattern: p, target: newTarget.trim(), exclusions: exclusions.length ? exclusions : undefined })
|
|
37
|
+
if (!result.found) {
|
|
38
|
+
output.error(`Pattern not found: ${pattern.trim()}`)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
next = result.content
|
|
42
|
+
}
|
|
43
|
+
return next
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
async function run() {
|
|
22
47
|
if (!fileName) {
|
|
23
48
|
output.error('Missing required argument: <file>')
|
|
@@ -37,23 +62,11 @@ async function run() {
|
|
|
37
62
|
const pat = pattern.trim()
|
|
38
63
|
const tgt = newTarget.trim()
|
|
39
64
|
|
|
40
|
-
// Handle [marker] syntax for lookup
|
|
41
|
-
const bm = pat.match(/\[([^\]]+)\]/)
|
|
42
|
-
const internalKey = bm ? pat.replace(bm[0], bm[1]) : pat
|
|
43
|
-
|
|
44
65
|
if (running) {
|
|
45
66
|
try {
|
|
46
67
|
let content = await apiGet(`/api/rule-files/${encodeURIComponent(fileName)}/content`)
|
|
47
68
|
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)
|
|
69
|
+
const newContent = applyUpdate(text)
|
|
57
70
|
await apiPut(`/api/rule-files/${encodeURIComponent(fileName)}/content`, { content: newContent })
|
|
58
71
|
|
|
59
72
|
if (jsonFlag) {
|
|
@@ -65,6 +78,8 @@ async function run() {
|
|
|
65
78
|
output.success(`${pat} -> ${tgt}`)
|
|
66
79
|
output.kv('File', fileName)
|
|
67
80
|
} catch (e) {
|
|
81
|
+
// Issue #137: a locked ruleset is a final answer, never a cue to write the file directly.
|
|
82
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
68
83
|
// Fallback to file mode
|
|
69
84
|
const content = getRuleFileContent(fileName)
|
|
70
85
|
if (!content) {
|
|
@@ -72,14 +87,7 @@ async function run() {
|
|
|
72
87
|
process.exit(1)
|
|
73
88
|
}
|
|
74
89
|
|
|
75
|
-
const
|
|
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)
|
|
90
|
+
const newContent = applyUpdate(content)
|
|
83
91
|
saveRuleFileContent(fileName, newContent)
|
|
84
92
|
output.warnFileOnlyMode()
|
|
85
93
|
|
|
@@ -104,14 +112,7 @@ async function run() {
|
|
|
104
112
|
process.exit(1)
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
const
|
|
108
|
-
if (!Object.prototype.hasOwnProperty.call(ruleMap, internalKey)) {
|
|
109
|
-
output.error(`Pattern not found: ${pat}`)
|
|
110
|
-
process.exit(1)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
ruleMap[internalKey] = bm ? tgt + bm[0] : tgt
|
|
114
|
-
const newContent = ruleMapToEprcText(ruleMap)
|
|
115
|
+
const newContent = applyUpdate(content)
|
|
115
116
|
saveRuleFileContent(fileName, newContent)
|
|
116
117
|
output.warnFileOnlyMode()
|
|
117
118
|
|
|
@@ -131,6 +132,7 @@ async function run() {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
run().catch(e => {
|
|
135
|
+
if (isRulesetReadonlyError(e)) exitRulesetReadonly(e, { json: jsonFlag, ruleFile: fileName })
|
|
134
136
|
output.error(e.message)
|
|
135
137
|
process.exit(1)
|
|
136
138
|
})
|
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
|
/**
|
|
@@ -217,6 +245,7 @@ function listRuleFiles() {
|
|
|
217
245
|
.map(f => f.replace(/\.txt$/, ''))
|
|
218
246
|
|
|
219
247
|
const ordered = resolveRuleFileOrder(files, getRuleFileOrderNames(), activeNames)
|
|
248
|
+
const readonlyNames = new Set(getReadonlyRuleFileNames())
|
|
220
249
|
|
|
221
250
|
return ordered.map(name => {
|
|
222
251
|
const filePath = getRuleFilePath(name)
|
|
@@ -234,6 +263,7 @@ function listRuleFiles() {
|
|
|
234
263
|
return {
|
|
235
264
|
name,
|
|
236
265
|
enabled: activeNames.includes(name),
|
|
266
|
+
readonly: readonlyNames.has(name),
|
|
237
267
|
ruleCount,
|
|
238
268
|
excludeCount
|
|
239
269
|
}
|
|
@@ -258,6 +288,7 @@ function getRuleFileContent(name) {
|
|
|
258
288
|
*/
|
|
259
289
|
function saveRuleFileContent(name, content) {
|
|
260
290
|
ensureRouteRulesDir()
|
|
291
|
+
assertRuleFileWritable(name)
|
|
261
292
|
const filePath = getRuleFilePath(name)
|
|
262
293
|
fs.writeFileSync(filePath, content, 'utf8')
|
|
263
294
|
}
|
|
@@ -274,6 +305,8 @@ function createRuleFile(name, content = '', enabled = true) {
|
|
|
274
305
|
throw new Error(`规则文件 "${safeName}" 已存在`)
|
|
275
306
|
}
|
|
276
307
|
|
|
308
|
+
// Drop stale lock entries so a new file never inherits an old lock.
|
|
309
|
+
getReadonlyRuleFileNames()
|
|
277
310
|
fs.writeFileSync(filePath, content, 'utf8')
|
|
278
311
|
|
|
279
312
|
if (enabled) {
|
|
@@ -285,7 +318,7 @@ function createRuleFile(name, content = '', enabled = true) {
|
|
|
285
318
|
}
|
|
286
319
|
|
|
287
320
|
const ruleCount = content.trim() ? Object.keys(parseEprc(content)).length : 0
|
|
288
|
-
return { name: safeName, enabled, ruleCount }
|
|
321
|
+
return { name: safeName, enabled, readonly: false, ruleCount }
|
|
289
322
|
}
|
|
290
323
|
|
|
291
324
|
/**
|
|
@@ -296,6 +329,7 @@ function deleteRuleFile(name) {
|
|
|
296
329
|
if (!fs.existsSync(filePath)) {
|
|
297
330
|
throw new Error('规则文件不存在')
|
|
298
331
|
}
|
|
332
|
+
assertRuleFileWritable(name)
|
|
299
333
|
fs.unlinkSync(filePath)
|
|
300
334
|
|
|
301
335
|
const activeNames = getActiveRuleFileNames()
|
|
@@ -317,6 +351,7 @@ function setRuleFileEnabled(name, enabled) {
|
|
|
317
351
|
|
|
318
352
|
const activeNames = getActiveRuleFileNames()
|
|
319
353
|
const idx = activeNames.indexOf(name)
|
|
354
|
+
if ((enabled && idx === -1) || (!enabled && idx !== -1)) assertRuleFileWritable(name)
|
|
320
355
|
|
|
321
356
|
if (enabled && idx === -1) {
|
|
322
357
|
activeNames.push(name)
|
|
@@ -352,6 +387,9 @@ module.exports = {
|
|
|
352
387
|
saveSettings,
|
|
353
388
|
getActiveRuleFileNames,
|
|
354
389
|
setActiveRuleFileNames,
|
|
390
|
+
getReadonlyRuleFileNames,
|
|
391
|
+
isRuleFileReadonly,
|
|
392
|
+
assertRuleFileWritable,
|
|
355
393
|
|
|
356
394
|
// Mocks
|
|
357
395
|
getMocksPath,
|