@zhushanwen/pi-permission 1.3.3 → 1.3.4
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/package.json +28 -10
- package/src/__tests__/startup-config-declaration.test.ts +34 -0
- package/src/ast/analyzer.ts +5 -2
- package/src/commands.ts +4 -1
- package/src/config.ts +4 -1
- package/src/index.ts +14 -6
- package/src/model-picker.ts +4 -4
- package/src/rule-editor-component.ts +23 -13
- package/src/rules/builtins.ts +4 -1
package/package.json
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-permission",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "Pi permission extension — four permission modes (yolo/auto/approve/strict) with three-layer pipeline (AST + rules + AI classifier).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"xyz-agent": {
|
|
8
|
-
"role": "universal"
|
|
8
|
+
"role": "universal",
|
|
9
|
+
"startupConfig": [
|
|
10
|
+
{
|
|
11
|
+
"path": "config/permission-ext-config.json",
|
|
12
|
+
"content": {
|
|
13
|
+
"mode": "yolo",
|
|
14
|
+
"enabled": true,
|
|
15
|
+
"classifier": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"model": "auto",
|
|
18
|
+
"timeout": 90,
|
|
19
|
+
"autoApproveLowRisk": true,
|
|
20
|
+
"autoDenyHighRisk": true,
|
|
21
|
+
"thinkingLevel": "off"
|
|
22
|
+
},
|
|
23
|
+
"userRules": []
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
9
27
|
},
|
|
10
28
|
"pi": {
|
|
11
29
|
"extensions": [
|
|
@@ -32,13 +50,13 @@
|
|
|
32
50
|
"dependencies": {
|
|
33
51
|
"tree-sitter-bash": "^0.25.1",
|
|
34
52
|
"web-tree-sitter": "^0.26.11",
|
|
35
|
-
"@zhushanwen/pi-extension-logger": "0.3.
|
|
36
|
-
"@zhushanwen/pi-llm-shared": "0.4.
|
|
53
|
+
"@zhushanwen/pi-extension-logger": "0.3.1",
|
|
54
|
+
"@zhushanwen/pi-llm-shared": "0.4.2"
|
|
37
55
|
},
|
|
38
56
|
"peerDependencies": {
|
|
39
|
-
"@earendil-works/pi-ai": "^0.84.
|
|
40
|
-
"@earendil-works/pi-coding-agent": "^0.84.
|
|
41
|
-
"@earendil-works/pi-tui": "^0.84.
|
|
57
|
+
"@earendil-works/pi-ai": "^0.84.4",
|
|
58
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
59
|
+
"@earendil-works/pi-tui": "^0.84.4"
|
|
42
60
|
},
|
|
43
61
|
"peerDependenciesMeta": {
|
|
44
62
|
"@earendil-works/pi-ai": {
|
|
@@ -52,9 +70,9 @@
|
|
|
52
70
|
}
|
|
53
71
|
},
|
|
54
72
|
"devDependencies": {
|
|
55
|
-
"@earendil-works/pi-ai": "^0.84.
|
|
56
|
-
"@earendil-works/pi-coding-agent": "^0.84.
|
|
57
|
-
"@earendil-works/pi-tui": "^0.84.
|
|
73
|
+
"@earendil-works/pi-ai": "^0.84.4",
|
|
74
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
75
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
58
76
|
"@vitest/coverage-v8": "^4.1.9",
|
|
59
77
|
"vitest": "^4.1.8"
|
|
60
78
|
},
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* startupConfig 声明守护测试:断言 package.json `xyz-agent.startupConfig` 声明的
|
|
3
|
+
* content 与代码 DEFAULT_CONFIG 常量深相等,防止两侧任一改动未同步(漂移)。
|
|
4
|
+
* 机制:runtime 启动序列统一 ensure(extension-startup-config.ts),声明即首建内容。
|
|
5
|
+
*
|
|
6
|
+
* 运行:cd extensions/universal/permission && npx vitest run src/__tests__/startup-config-declaration.test.ts
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
|
|
14
|
+
import { DEFAULT_CONFIG } from "../types.js";
|
|
15
|
+
|
|
16
|
+
/** package.json `xyz-agent.startupConfig` 声明条目的最小形状(守护断言用)。 */
|
|
17
|
+
interface StartupConfigEntry {
|
|
18
|
+
path: string;
|
|
19
|
+
content: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pkg = JSON.parse(
|
|
23
|
+
readFileSync(join(fileURLToPath(import.meta.url), "../../../package.json"), "utf-8"),
|
|
24
|
+
) as { "xyz-agent"?: { startupConfig?: StartupConfigEntry[] } };
|
|
25
|
+
|
|
26
|
+
describe("startupConfig 声明守护", () => {
|
|
27
|
+
it("声明 content 与代码 DEFAULT_CONFIG 深相等", () => {
|
|
28
|
+
const entry = pkg["xyz-agent"]?.startupConfig?.find(
|
|
29
|
+
(e: StartupConfigEntry) => e.path === "config/permission-ext-config.json",
|
|
30
|
+
);
|
|
31
|
+
expect(entry).toBeDefined();
|
|
32
|
+
expect(entry?.content).toEqual(DEFAULT_CONFIG);
|
|
33
|
+
});
|
|
34
|
+
});
|
package/src/ast/analyzer.ts
CHANGED
|
@@ -29,6 +29,9 @@ const logger = getLogger("pi-permission/ast");
|
|
|
29
29
|
/** 超过此长度的输入直接拒(防止 wasm 解析巨型 payload DoS)。 */
|
|
30
30
|
const MAX_COMMAND_LENGTH = 65536;
|
|
31
31
|
|
|
32
|
+
/** 带引号 token 的最小长度(首尾引号各占 1 字符);满足才能安全剥引号。 */
|
|
33
|
+
const MIN_QUOTED_TOKEN_LENGTH = 2;
|
|
34
|
+
|
|
32
35
|
/**
|
|
33
36
|
* Codex ALLOWED_KINDS(11 个 named node)。
|
|
34
37
|
* 遇到不在此集合的 named node 即判定为危险结构。
|
|
@@ -76,7 +79,7 @@ function parseDoubleQuotedString(node: Node): string | null {
|
|
|
76
79
|
}
|
|
77
80
|
const raw = node.text;
|
|
78
81
|
// 去掉首尾的 `"`(raw 形如 `"hello world"`)
|
|
79
|
-
if (raw.startsWith('"') && raw.endsWith('"') && raw.length >=
|
|
82
|
+
if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= MIN_QUOTED_TOKEN_LENGTH) {
|
|
80
83
|
return raw.slice(1, -1);
|
|
81
84
|
}
|
|
82
85
|
// 异常 grammar(理论上不会到这)—— 原样返回
|
|
@@ -92,7 +95,7 @@ function parseRawString(node: Node): string | null {
|
|
|
92
95
|
return null;
|
|
93
96
|
}
|
|
94
97
|
const raw = node.text;
|
|
95
|
-
if (raw.startsWith("'") && raw.endsWith("'") && raw.length >=
|
|
98
|
+
if (raw.startsWith("'") && raw.endsWith("'") && raw.length >= MIN_QUOTED_TOKEN_LENGTH) {
|
|
96
99
|
return raw.slice(1, -1);
|
|
97
100
|
}
|
|
98
101
|
return raw;
|
package/src/commands.ts
CHANGED
|
@@ -55,6 +55,9 @@ export function handlePermissionCommand(
|
|
|
55
55
|
|
|
56
56
|
// ──────────────────────── 消息格式化 ────────────────────────
|
|
57
57
|
|
|
58
|
+
/** 模式列表 mode 列的 padEnd 宽度(最长 'approve' 7 字符 + 1 间隔,保证描述列对齐)。 */
|
|
59
|
+
const MODE_COLUMN_WIDTH = 8;
|
|
60
|
+
|
|
58
61
|
function formatStatusMessage(config: PermissionConfig): string {
|
|
59
62
|
const lines: string[] = [];
|
|
60
63
|
lines.push("[pi-permission] Current mode: " + modeHighlight(config.mode));
|
|
@@ -62,7 +65,7 @@ function formatStatusMessage(config: PermissionConfig): string {
|
|
|
62
65
|
lines.push("Available modes (by strictness, low → high):");
|
|
63
66
|
for (const mode of PERMISSION_MODES) {
|
|
64
67
|
const marker = mode === config.mode ? "►" : " ";
|
|
65
|
-
lines.push(` ${marker} ${mode.padEnd(
|
|
68
|
+
lines.push(` ${marker} ${mode.padEnd(MODE_COLUMN_WIDTH)} — ${MODE_DESCRIPTIONS[mode]}`);
|
|
66
69
|
}
|
|
67
70
|
lines.push("");
|
|
68
71
|
lines.push("Switch with: /permission <mode>");
|
package/src/config.ts
CHANGED
|
@@ -116,8 +116,11 @@ function normalizeConfig(raw: unknown): PermissionConfig {
|
|
|
116
116
|
|
|
117
117
|
// ──────────────────────── 默认配置文件创建 ────────────────────────
|
|
118
118
|
|
|
119
|
+
/** 默认配置文件 JSON 序列化的缩进空格数。 */
|
|
120
|
+
const CONFIG_JSON_INDENT_SPACES = 2;
|
|
121
|
+
|
|
119
122
|
function createDefaultConfigContent(): string {
|
|
120
|
-
return `${JSON.stringify(DEFAULT_CONFIG, null,
|
|
123
|
+
return `${JSON.stringify(DEFAULT_CONFIG, null, CONFIG_JSON_INDENT_SPACES)}\n`;
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
function ensureConfigFile(configPath: string, onWarning?: (msg: string) => void): void {
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,14 @@ interface ToolCallResult {
|
|
|
70
70
|
reason?: string;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* ctx.ui.select / ctx.ui.input 的 opts 参数索引(第 3 个参数,0-based)。
|
|
75
|
+
* 类型位置提取 opts 时用 `Parameters<typeof ...>[typeof _UI_OPTIONS_PARAM_INDEX]`
|
|
76
|
+
* (字面量索引会触发 no-magic-numbers,typeof 常量索引语义等价)。
|
|
77
|
+
* 仅类型位置使用、运行时无值引用,按项目约定加 `_` 前缀豁免 no-unused-vars。
|
|
78
|
+
*/
|
|
79
|
+
const _UI_OPTIONS_PARAM_INDEX = 2;
|
|
80
|
+
|
|
73
81
|
// ──────────────────────── 扩展工厂 ────────────────────────
|
|
74
82
|
|
|
75
83
|
/**
|
|
@@ -135,7 +143,7 @@ export default function permissionExtension(pi: ExtensionAPI): void {
|
|
|
135
143
|
mode: ctx.mode,
|
|
136
144
|
ui: {
|
|
137
145
|
notify: (msg: string, type?: "info" | "warning" | "error") => ctx.ui.notify(msg, type),
|
|
138
|
-
select: (title: string, options: string[], opts?: Parameters<typeof ctx.ui.select>[
|
|
146
|
+
select: (title: string, options: string[], opts?: Parameters<typeof ctx.ui.select>[typeof _UI_OPTIONS_PARAM_INDEX]) =>
|
|
139
147
|
ctx.ui.select(title, options, opts),
|
|
140
148
|
custom: <T,>(
|
|
141
149
|
factory: (tui: unknown, theme: unknown, kb: unknown, done: (result: T) => void) => unknown,
|
|
@@ -145,7 +153,7 @@ export default function permissionExtension(pi: ExtensionAPI): void {
|
|
|
145
153
|
// 连接 ctx.ui.input(rule-editor custom 模板文本输入用)。
|
|
146
154
|
// approval.ts 已声明可选 input(SDK 提供,mock 可能缺失)。
|
|
147
155
|
...(typeof ctx.ui.input === "function"
|
|
148
|
-
? { input: (title: string, placeholder?: string, opts?: Parameters<typeof ctx.ui.input>[
|
|
156
|
+
? { input: (title: string, placeholder?: string, opts?: Parameters<typeof ctx.ui.input>[typeof _UI_OPTIONS_PARAM_INDEX]) => ctx.ui.input(title, placeholder, opts) }
|
|
149
157
|
: {}),
|
|
150
158
|
},
|
|
151
159
|
},
|
|
@@ -172,7 +180,7 @@ export default function permissionExtension(pi: ExtensionAPI): void {
|
|
|
172
180
|
modelRegistry: ctx.modelRegistry,
|
|
173
181
|
ui: {
|
|
174
182
|
notify: (msg: string, type?: "info" | "warning" | "error") => ctx.ui.notify(msg, type),
|
|
175
|
-
select: (title: string, options: string[], opts?: Parameters<typeof ctx.ui.select>[
|
|
183
|
+
select: (title: string, options: string[], opts?: Parameters<typeof ctx.ui.select>[typeof _UI_OPTIONS_PARAM_INDEX]) =>
|
|
176
184
|
ctx.ui.select(title, options, opts),
|
|
177
185
|
custom: <T,>(
|
|
178
186
|
factory: (tui: unknown, theme: unknown, kb: unknown, done: (result: T) => void) => unknown,
|
|
@@ -181,7 +189,7 @@ export default function permissionExtension(pi: ExtensionAPI): void {
|
|
|
181
189
|
ctx.ui.custom<T>(factory as Parameters<typeof ctx.ui.custom<T>>[0], options),
|
|
182
190
|
// 连接 ctx.ui.input(与 rule handler 一致;model picker 当前不用,但保持 ctx 对称)。
|
|
183
191
|
...(typeof ctx.ui.input === "function"
|
|
184
|
-
? { input: (title: string, placeholder?: string, opts?: Parameters<typeof ctx.ui.input>[
|
|
192
|
+
? { input: (title: string, placeholder?: string, opts?: Parameters<typeof ctx.ui.input>[typeof _UI_OPTIONS_PARAM_INDEX]) => ctx.ui.input(title, placeholder, opts) }
|
|
185
193
|
: {}),
|
|
186
194
|
},
|
|
187
195
|
},
|
|
@@ -315,7 +323,7 @@ async function processToolCall(
|
|
|
315
323
|
mode: ctx.mode,
|
|
316
324
|
ui: {
|
|
317
325
|
notify: (msg: string, type?: "info" | "warning" | "error") => ctx.ui.notify(msg, type),
|
|
318
|
-
select: (title: string, options: string[], opts?: Parameters<typeof ctx.ui.select>[
|
|
326
|
+
select: (title: string, options: string[], opts?: Parameters<typeof ctx.ui.select>[typeof _UI_OPTIONS_PARAM_INDEX]) => ctx.ui.select(title, options, opts),
|
|
319
327
|
custom: <T,>(
|
|
320
328
|
factory: (tui: unknown, theme: unknown, kb: unknown, done: (result: T) => void) => unknown,
|
|
321
329
|
options?: { overlay?: boolean },
|
|
@@ -324,7 +332,7 @@ async function processToolCall(
|
|
|
324
332
|
// W6 T9 G3:Reject-with-Reason。ctx.ui.input 存在则透传(采集真实拒绝理由)。
|
|
325
333
|
// approval.ts 的 collectRejectReason 会用 typeof 判断是否可用,不可用则 fallback。
|
|
326
334
|
...(typeof ctx.ui.input === "function"
|
|
327
|
-
? { input: (title: string, placeholder?: string, opts?: Parameters<typeof ctx.ui.input>[
|
|
335
|
+
? { input: (title: string, placeholder?: string, opts?: Parameters<typeof ctx.ui.input>[typeof _UI_OPTIONS_PARAM_INDEX]) => ctx.ui.input(title, placeholder, opts) }
|
|
328
336
|
: {}),
|
|
329
337
|
},
|
|
330
338
|
};
|
package/src/model-picker.ts
CHANGED
|
@@ -53,6 +53,9 @@ const AUTO_VALUE = "__auto__";
|
|
|
53
53
|
/** SelectList 最大可见行数(picker overlay 不超过终端高度)。 */
|
|
54
54
|
const MAX_VISIBLE_ITEMS = 10;
|
|
55
55
|
|
|
56
|
+
/** box 边框左右各占用 1 列(│ × 2)。 */
|
|
57
|
+
const BORDER_OVERHEAD = 2;
|
|
58
|
+
|
|
56
59
|
// ──────────────────────── 辅助组件(inline Component stub,避免依赖 Box 的 padding/bg) ────────────────────────
|
|
57
60
|
|
|
58
61
|
/**
|
|
@@ -127,14 +130,11 @@ export class ProviderModelSelectorComponent extends Container {
|
|
|
127
130
|
this.currentList?.handleInput(data);
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
/** box 边框左右各占用 1 列(│ × 2) */
|
|
131
|
-
private static readonly BORDER_OVERHEAD = 2;
|
|
132
|
-
|
|
133
133
|
/**
|
|
134
134
|
* 重写 render:用 box 边框包裹 Container 子组件的输出。
|
|
135
135
|
*/
|
|
136
136
|
override render(width: number): string[] {
|
|
137
|
-
const innerWidth = Math.max(0, width -
|
|
137
|
+
const innerWidth = Math.max(0, width - BORDER_OVERHEAD);
|
|
138
138
|
const inner = super.render(innerWidth);
|
|
139
139
|
const lines: string[] = [];
|
|
140
140
|
lines.push(`\u250C${"\u2500".repeat(innerWidth)}\u2510`);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* WR8:focusIndex 模 5(Custom form 5 项 Tab 循环)。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { type Component, Container, Input, type SelectItem, SelectList, type SelectListTheme, truncateToWidth } from "@earendil-works/pi-tui";
|
|
14
|
+
import { type Component, Container, Input, isFocusable, type SelectItem, SelectList, type SelectListTheme, truncateToWidth } from "@earendil-works/pi-tui";
|
|
15
15
|
import { matchesKey } from "@earendil-works/pi-tui";
|
|
16
16
|
|
|
17
17
|
import {
|
|
@@ -36,6 +36,19 @@ const THEME: SelectListTheme = {
|
|
|
36
36
|
|
|
37
37
|
const MAX_VISIBLE = 12;
|
|
38
38
|
|
|
39
|
+
/** box 边框左右各占用 1 列(│ × 2)。 */
|
|
40
|
+
const BORDER_OVERHEAD = 2;
|
|
41
|
+
|
|
42
|
+
/** custom form 焦点指示前缀(▶ + 空格,1 显示列 + 1 空格)。 */
|
|
43
|
+
const FOCUS_INDICATOR_PREFIX = "\u25B6 ";
|
|
44
|
+
|
|
45
|
+
/** scope 选择:all/subcmd 二选一全可见。 */
|
|
46
|
+
const SCOPE_LIST_MAX_VISIBLE = 2;
|
|
47
|
+
/** action 选择:allow/deny/ask 三选一全可见。 */
|
|
48
|
+
const ACTION_LIST_MAX_VISIBLE = 3;
|
|
49
|
+
/** submit 选择:Submit/Cancel 二选一全可见。 */
|
|
50
|
+
const SUBMIT_LIST_MAX_VISIBLE = 2;
|
|
51
|
+
|
|
39
52
|
// ──────────────────────── TextLines component ────────────────────────
|
|
40
53
|
|
|
41
54
|
class TextLines implements Component {
|
|
@@ -123,11 +136,8 @@ export class RuleEditorComponent extends Container {
|
|
|
123
136
|
|
|
124
137
|
// ──────────────────────── 边框渲染 ────────────────────────
|
|
125
138
|
|
|
126
|
-
/** box 边框左右各占用 1 列(│ × 2) */
|
|
127
|
-
private static readonly BORDER_OVERHEAD = 2;
|
|
128
|
-
|
|
129
139
|
override render(width: number): string[] {
|
|
130
|
-
const innerWidth = Math.max(0, width -
|
|
140
|
+
const innerWidth = Math.max(0, width - BORDER_OVERHEAD);
|
|
131
141
|
let inner = super.render(innerWidth);
|
|
132
142
|
|
|
133
143
|
// custom form 焦点指示(M4):当前焦点字段的标签加 ▶ 前缀
|
|
@@ -169,11 +179,11 @@ export class RuleEditorComponent extends Container {
|
|
|
169
179
|
return lines.map((line) => {
|
|
170
180
|
// 精确匹配当前焦点标签 → 加 ▶ 前缀
|
|
171
181
|
if (line === focusedLabel) {
|
|
172
|
-
return
|
|
182
|
+
return FOCUS_INDICATOR_PREFIX + line;
|
|
173
183
|
}
|
|
174
184
|
// 焦点已转移:去掉其他标签行残留的 ▶ 前缀(避免重复渲染累积)
|
|
175
|
-
if (line.startsWith(
|
|
176
|
-
return line.slice(
|
|
185
|
+
if (line.startsWith(FOCUS_INDICATOR_PREFIX) && fieldLabels.includes(line.slice(FOCUS_INDICATOR_PREFIX.length))) {
|
|
186
|
+
return line.slice(FOCUS_INDICATOR_PREFIX.length);
|
|
177
187
|
}
|
|
178
188
|
return line;
|
|
179
189
|
});
|
|
@@ -468,7 +478,7 @@ export class RuleEditorComponent extends Container {
|
|
|
468
478
|
{ value: "__all__", label: `${cmd} * (all subcommands)`, description: `Allow/deny all invocations of ${cmd}` },
|
|
469
479
|
{ value: "__subcmd__", label: `${cmd} <subcommand> *`, description: `Allow/deny a specific subcommand` },
|
|
470
480
|
];
|
|
471
|
-
const list = new SelectList(scopeItems,
|
|
481
|
+
const list = new SelectList(scopeItems, SCOPE_LIST_MAX_VISIBLE, this.theme);
|
|
472
482
|
list.onSelect = (item: SelectItem): void => {
|
|
473
483
|
if (item.value === "__all__") {
|
|
474
484
|
this.fillSelections.cmd = cmd;
|
|
@@ -674,7 +684,7 @@ export class RuleEditorComponent extends Container {
|
|
|
674
684
|
{ value: "deny", label: "deny" },
|
|
675
685
|
{ value: "ask", label: "ask" },
|
|
676
686
|
];
|
|
677
|
-
const actionList = new SelectList(actionItems,
|
|
687
|
+
const actionList = new SelectList(actionItems, ACTION_LIST_MAX_VISIBLE, this.theme);
|
|
678
688
|
actionList.onSelect = (item: SelectItem): void => {
|
|
679
689
|
this.fillSelections.action = item.value as PermissionAction;
|
|
680
690
|
};
|
|
@@ -709,7 +719,7 @@ export class RuleEditorComponent extends Container {
|
|
|
709
719
|
{ value: "__submit__", label: "[Submit]" },
|
|
710
720
|
{ value: "__cancel__", label: "[Cancel]" },
|
|
711
721
|
];
|
|
712
|
-
const submitList = new SelectList(submitItems,
|
|
722
|
+
const submitList = new SelectList(submitItems, SUBMIT_LIST_MAX_VISIBLE, this.theme);
|
|
713
723
|
submitList.onSelect = (item: SelectItem): void => {
|
|
714
724
|
if (item.value === "__submit__") {
|
|
715
725
|
this.commitFill();
|
|
@@ -875,8 +885,8 @@ export class RuleEditorComponent extends Container {
|
|
|
875
885
|
private syncCustomFocus(): void {
|
|
876
886
|
for (let i = 0; i < this.customChildren.length; i++) {
|
|
877
887
|
const child = this.customChildren[i];
|
|
878
|
-
if (child !== undefined &&
|
|
879
|
-
|
|
888
|
+
if (child !== undefined && isFocusable(child)) {
|
|
889
|
+
child.focused = i === this.customFocusIndex;
|
|
880
890
|
}
|
|
881
891
|
}
|
|
882
892
|
}
|
package/src/rules/builtins.ts
CHANGED
|
@@ -117,6 +117,9 @@ function executableNameLookupKey(raw: string): string | undefined {
|
|
|
117
117
|
|
|
118
118
|
// ── 合并短 flag 检查工具(G4: 合并 flag 如 -fo 需检测任意位置的 -o)──
|
|
119
119
|
|
|
120
|
+
/** 短 flag 簇(`-x`)的最小长度:`-` + 至少 1 个 flag 字符。 */
|
|
121
|
+
const MIN_SHORT_FLAG_CLUSTER_LENGTH = 2;
|
|
122
|
+
|
|
120
123
|
/**
|
|
121
124
|
* 检查短 flag 簇中是否含某个字符(如 `-fo` 含 `o`)。
|
|
122
125
|
*
|
|
@@ -129,7 +132,7 @@ function executableNameLookupKey(raw: string): string | undefined {
|
|
|
129
132
|
*/
|
|
130
133
|
function shortFlagClusterHasChar(arg: string, flagChar: string): boolean {
|
|
131
134
|
// 只处理短 flag 簇:单个 - 开头,非 --,长度 ≥ 2
|
|
132
|
-
if (!arg.startsWith("-") || arg.startsWith("--") || arg.length <
|
|
135
|
+
if (!arg.startsWith("-") || arg.startsWith("--") || arg.length < MIN_SHORT_FLAG_CLUSTER_LENGTH) return false;
|
|
133
136
|
// arg 去掉开头的 -,检查剩余字符
|
|
134
137
|
return arg.slice(1).includes(flagChar);
|
|
135
138
|
}
|