pi-shepherd 0.1.2 → 0.2.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/README.en.md +205 -84
- package/README.md +207 -88
- package/index.ts +262 -229
- package/package.json +60 -55
- package/rules.json +196 -33
- package/shepherd/compaction.ts +76 -0
- package/shepherd/conditions.ts +98 -0
- package/shepherd/ephemeral.ts +55 -52
- package/shepherd/git.ts +64 -0
- package/shepherd/index.ts +13 -0
- package/shepherd/line-count.ts +86 -86
- package/shepherd/message-end.ts +120 -0
- package/shepherd/rules-editor.ts +250 -215
- package/shepherd/rules-tool-helpers.ts +119 -120
- package/shepherd/rules-tool-list.ts +124 -126
- package/shepherd/rules-tool.ts +182 -142
- package/shepherd/rules-validate.ts +89 -44
- package/shepherd/rules.ts +366 -295
- package/shepherd/tool-event-types.ts +27 -14
- package/shepherd/tool-hooks.ts +165 -177
- package/shepherd/worktree-check.ts +130 -130
- package/tsconfig.json +21 -14
package/shepherd/rules-editor.ts
CHANGED
|
@@ -1,215 +1,250 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shepherd 规则文件安全编辑器
|
|
3
|
-
*
|
|
4
|
-
* 提供 rules.json 的安全增删改查:
|
|
5
|
-
* - 写入前校验(必填字段、正则合法性、枚举值)
|
|
6
|
-
* - 自动备份(.bak)
|
|
7
|
-
* - 写入后回读验证
|
|
8
|
-
* - 失败自动回滚
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import * as fs from "node:fs";
|
|
12
|
-
import * as path from "node:path";
|
|
13
|
-
import { validateRule } from "./rules-validate";
|
|
14
|
-
|
|
15
|
-
export type { ValidationResult } from "./rules-validate";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
export function
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
rules
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shepherd 规则文件安全编辑器
|
|
3
|
+
*
|
|
4
|
+
* 提供 rules.json 的安全增删改查:
|
|
5
|
+
* - 写入前校验(必填字段、正则合法性、枚举值)
|
|
6
|
+
* - 自动备份(.bak)
|
|
7
|
+
* - 写入后回读验证
|
|
8
|
+
* - 失败自动回滚
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { validateRule } from "./rules-validate";
|
|
14
|
+
|
|
15
|
+
export type { ValidationResult } from "./rules-validate";
|
|
16
|
+
export { validateRule } from "./rules-validate";
|
|
17
|
+
|
|
18
|
+
// ── 类型 ──────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface RuleSummary {
|
|
21
|
+
index: number;
|
|
22
|
+
comment: string;
|
|
23
|
+
action?: string;
|
|
24
|
+
tool?: string;
|
|
25
|
+
hook?: string;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
pattern?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ListResult {
|
|
31
|
+
rules: RuleSummary[];
|
|
32
|
+
count: number;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RuleDetail {
|
|
37
|
+
index: number;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ListDetailResult {
|
|
42
|
+
rules: RuleDetail[];
|
|
43
|
+
count: number;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WriteResult {
|
|
48
|
+
success: boolean;
|
|
49
|
+
error?: string;
|
|
50
|
+
index?: number;
|
|
51
|
+
deleted?: { comment: string; [key: string]: unknown };
|
|
52
|
+
overwritten?: boolean;
|
|
53
|
+
warning?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── 文件操作辅助 ──────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
interface FileData {
|
|
59
|
+
rules: Record<string, unknown>[];
|
|
60
|
+
error?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readFile(filePath: string): FileData {
|
|
64
|
+
if (!fs.existsSync(filePath)) return { rules: [] };
|
|
65
|
+
try {
|
|
66
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
if (!Array.isArray(parsed)) {
|
|
69
|
+
return {
|
|
70
|
+
rules: [],
|
|
71
|
+
error: `顶层必须是 JSON 数组,当前是 ${typeof parsed}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { rules: parsed };
|
|
75
|
+
} catch (e: unknown) {
|
|
76
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
77
|
+
return { rules: [], error: `JSON 解析失败: ${msg}` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function safeWrite(
|
|
82
|
+
filePath: string,
|
|
83
|
+
rules: Record<string, unknown>[],
|
|
84
|
+
): WriteResult {
|
|
85
|
+
const backupPath = `${filePath}.bak`;
|
|
86
|
+
if (fs.existsSync(filePath)) fs.copyFileSync(filePath, backupPath);
|
|
87
|
+
const dir = path.dirname(filePath);
|
|
88
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
fs.writeFileSync(filePath, JSON.stringify(rules, null, "\t"), "utf-8");
|
|
90
|
+
try {
|
|
91
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
if (!Array.isArray(parsed)) throw new Error("not array");
|
|
94
|
+
return { success: true };
|
|
95
|
+
} catch {
|
|
96
|
+
if (fs.existsSync(backupPath)) fs.copyFileSync(backupPath, filePath);
|
|
97
|
+
return { success: false, error: "写入后验证失败,已从备份恢复" };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── 去重签名 ─────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/** 规范化 conditions 数组:按 field+pattern 排序,取关键属性 */
|
|
104
|
+
function normalizeConditions(
|
|
105
|
+
conditions: Array<Record<string, unknown>>,
|
|
106
|
+
): string {
|
|
107
|
+
const normalized = conditions
|
|
108
|
+
.map((c) => ({
|
|
109
|
+
f: (c.field as string) || "",
|
|
110
|
+
p: (c.pattern as string) || "",
|
|
111
|
+
}))
|
|
112
|
+
.sort((a, b) => (a.f + a.p).localeCompare(b.f + b.p));
|
|
113
|
+
return JSON.stringify(normalized);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 生成规则去重签名 key */
|
|
117
|
+
export function dedupKey(rule: Record<string, unknown>): string {
|
|
118
|
+
const tool = (rule.tool as string) || "";
|
|
119
|
+
const hook = (rule.hook as string) || "";
|
|
120
|
+
const action = (rule.action as string) || "";
|
|
121
|
+
let trigger: string;
|
|
122
|
+
if (Array.isArray(rule.conditions) && rule.conditions.length > 0) {
|
|
123
|
+
trigger = normalizeConditions(
|
|
124
|
+
rule.conditions as Array<Record<string, unknown>>,
|
|
125
|
+
);
|
|
126
|
+
} else if (rule.pattern) {
|
|
127
|
+
trigger = rule.pattern as string;
|
|
128
|
+
} else if (rule.check) {
|
|
129
|
+
trigger = rule.check as string;
|
|
130
|
+
} else {
|
|
131
|
+
trigger = "";
|
|
132
|
+
}
|
|
133
|
+
return `${tool}|${hook}|${trigger}|${action}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 在规则数组中查找签名相同的规则,返回 index 或 null */
|
|
137
|
+
export function findDuplicateBySignature(
|
|
138
|
+
rules: Record<string, unknown>[],
|
|
139
|
+
newRule: Record<string, unknown>,
|
|
140
|
+
): number | null {
|
|
141
|
+
const newKey = dedupKey(newRule);
|
|
142
|
+
for (let i = 0; i < rules.length; i++) {
|
|
143
|
+
if (dedupKey(rules[i]) === newKey) return i;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── 公开 API ─────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
export function listRules(filePath: string): ListResult {
|
|
151
|
+
const { rules, error } = readFile(filePath);
|
|
152
|
+
if (error) return { rules: [], count: 0, error };
|
|
153
|
+
return {
|
|
154
|
+
rules: rules.map((r, i) => ({
|
|
155
|
+
index: i,
|
|
156
|
+
comment: (r.comment as string) || "",
|
|
157
|
+
action: r.action as string | undefined,
|
|
158
|
+
tool: r.tool as string | undefined,
|
|
159
|
+
hook: r.hook as string | undefined,
|
|
160
|
+
enabled: r.enabled as boolean | undefined,
|
|
161
|
+
pattern: r.pattern as string | undefined,
|
|
162
|
+
})),
|
|
163
|
+
count: rules.length,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function listRulesDetail(filePath: string): ListDetailResult {
|
|
168
|
+
const { rules, error } = readFile(filePath);
|
|
169
|
+
if (error) return { rules: [], count: 0, error };
|
|
170
|
+
return {
|
|
171
|
+
rules: rules.map((r, i) => ({ index: i, ...r })),
|
|
172
|
+
count: rules.length,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getRuleDetail(
|
|
177
|
+
filePath: string,
|
|
178
|
+
index: number,
|
|
179
|
+
): RuleDetail | { error: string } {
|
|
180
|
+
const { rules, error } = readFile(filePath);
|
|
181
|
+
if (error) return { error };
|
|
182
|
+
if (index < 0 || index >= rules.length)
|
|
183
|
+
return { error: `编号越界: ${index}(共 ${rules.length} 条)` };
|
|
184
|
+
return { index, ...rules[index] };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function addRule(
|
|
188
|
+
filePath: string,
|
|
189
|
+
rule: Record<string, unknown>,
|
|
190
|
+
): WriteResult {
|
|
191
|
+
const { rules, error } = readFile(filePath);
|
|
192
|
+
if (error) return { success: false, error };
|
|
193
|
+
const validation = validateRule(rule);
|
|
194
|
+
if (!validation.valid)
|
|
195
|
+
return { success: false, error: validation.errors.join("; ") };
|
|
196
|
+
// 去重检测:签名相同则覆盖
|
|
197
|
+
const dupIdx = findDuplicateBySignature(rules, rule);
|
|
198
|
+
if (dupIdx !== null) {
|
|
199
|
+
rules[dupIdx] = rule;
|
|
200
|
+
const writeResult = safeWrite(filePath, rules);
|
|
201
|
+
return writeResult.success
|
|
202
|
+
? { success: true, index: dupIdx, overwritten: true }
|
|
203
|
+
: writeResult;
|
|
204
|
+
}
|
|
205
|
+
rules.push(rule);
|
|
206
|
+
const writeResult = safeWrite(filePath, rules);
|
|
207
|
+
return writeResult.success
|
|
208
|
+
? { success: true, index: rules.length - 1 }
|
|
209
|
+
: writeResult;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function updateRule(
|
|
213
|
+
filePath: string,
|
|
214
|
+
index: number,
|
|
215
|
+
changes: Record<string, unknown>,
|
|
216
|
+
): WriteResult {
|
|
217
|
+
if (index < 0) return { success: false, error: `编号越界: ${index}` };
|
|
218
|
+
const { rules, error } = readFile(filePath);
|
|
219
|
+
if (error) return { success: false, error };
|
|
220
|
+
if (index >= rules.length)
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
error: `编号越界: ${index}(共 ${rules.length} 条)`,
|
|
224
|
+
};
|
|
225
|
+
const merged = { ...rules[index], ...changes };
|
|
226
|
+
const validation = validateRule(merged);
|
|
227
|
+
if (!validation.valid)
|
|
228
|
+
return { success: false, error: validation.errors.join("; ") };
|
|
229
|
+
rules[index] = merged;
|
|
230
|
+
return safeWrite(filePath, rules);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function deleteRule(filePath: string, index: number): WriteResult {
|
|
234
|
+
if (index < 0) return { success: false, error: `编号越界: ${index}` };
|
|
235
|
+
const { rules, error } = readFile(filePath);
|
|
236
|
+
if (error) return { success: false, error };
|
|
237
|
+
if (index >= rules.length)
|
|
238
|
+
return {
|
|
239
|
+
success: false,
|
|
240
|
+
error: `编号越界: ${index}(共 ${rules.length} 条)`,
|
|
241
|
+
};
|
|
242
|
+
const deleted = rules.splice(index, 1)[0];
|
|
243
|
+
const writeResult = safeWrite(filePath, rules);
|
|
244
|
+
return writeResult.success
|
|
245
|
+
? {
|
|
246
|
+
success: true,
|
|
247
|
+
deleted: { comment: (deleted.comment as string) || "", ...deleted },
|
|
248
|
+
}
|
|
249
|
+
: writeResult;
|
|
250
|
+
}
|