bmall-mcp 1.0.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.
@@ -0,0 +1,370 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { execSync } from "child_process";
4
+
5
+ export const HOME = process.env.HOME || process.env.USERPROFILE || "";
6
+ export const CWD = process.cwd();
7
+
8
+ // Git 仓库配置
9
+ export const GIT_REPO = "git@coding.jd.com:IBKFXHJDBM/RulesManager.git";
10
+ export const GIT_BRANCH = "develop";
11
+ export const RULES_CACHE_DIR = path.join(HOME, ".bmall-mcp/rules-cache");
12
+
13
+ // 项目类型检测
14
+ export type ProjectType = "ios" | "android" | "web" | "flutter" | "unknown";
15
+
16
+ // IDE 的 Rules 目录映射(项目级别)
17
+ export const IDE_PATHS: Record<string, string> = {
18
+ kiro: path.join(CWD, ".kiro/steering/bmall"),
19
+ cursor: path.join(CWD, ".cursor/rules/bmall"),
20
+ windsurf: path.join(CWD, ".windsurfrules"),
21
+ };
22
+
23
+ export function log(message: string) {
24
+ console.error(`[bmall-mcp] ${message}`);
25
+ }
26
+
27
+ export function detectProjectType(): ProjectType {
28
+ const checks: Array<[string, ProjectType]> = [
29
+ ["ios/Podfile", "ios"],
30
+ ["*.xcodeproj", "ios"],
31
+ ["Podfile", "ios"],
32
+ ["android/build.gradle", "android"],
33
+ ["build.gradle", "android"],
34
+ ["AndroidManifest.xml", "android"],
35
+ ["package.json", "web"],
36
+ ["webpack.config.js", "web"],
37
+ ["tsconfig.json", "web"],
38
+ ["vite.config.ts", "web"],
39
+ ["pubspec.yaml", "flutter"],
40
+ ["flutter.yaml", "flutter"],
41
+ ];
42
+
43
+ for (const [pattern, type] of checks) {
44
+ try {
45
+ if (pattern.includes("*")) {
46
+ const files = fs.readdirSync(CWD);
47
+ if (files.some(f => f.match(pattern.replace("*", ".*")))) {
48
+ log(`检测到 ${type} 项目特征: ${pattern}`);
49
+ return type;
50
+ }
51
+ } else {
52
+ const fullPath = path.join(CWD, pattern);
53
+ if (fs.existsSync(fullPath)) {
54
+ log(`检测到 ${type} 项目特征: ${pattern}`);
55
+ return type;
56
+ }
57
+ }
58
+ } catch (e) {
59
+ // 继续
60
+ }
61
+ }
62
+
63
+ log(`未检测到项目类型,使用默认值: iOS`);
64
+ return "ios";
65
+ }
66
+
67
+ export function detectProjectName(): string {
68
+ // 优先级:package.json > Podfile > pubspec.yaml > 目录名
69
+
70
+ // 1. 检查 package.json
71
+ const packageJsonPath = path.join(CWD, "package.json");
72
+ if (fs.existsSync(packageJsonPath)) {
73
+ try {
74
+ const content = fs.readFileSync(packageJsonPath, "utf-8");
75
+ const json = JSON.parse(content);
76
+ if (json.name) {
77
+ const name = sanitizeProjectName(json.name);
78
+ log(`从 package.json 检测到项目名: ${name}`);
79
+ return name;
80
+ }
81
+ } catch (e) {
82
+ // 继续
83
+ }
84
+ }
85
+
86
+ // 2. 检查 Podfile
87
+ const podfilePath = path.join(CWD, "Podfile");
88
+ if (fs.existsSync(podfilePath)) {
89
+ try {
90
+ const content = fs.readFileSync(podfilePath, "utf-8");
91
+ const match = content.match(/platform\s*:\s*:ios.*?target\s+['"](.*?)['"]/s);
92
+ if (match && match[1]) {
93
+ const name = sanitizeProjectName(match[1]);
94
+ log(`从 Podfile 检测到项目名: ${name}`);
95
+ return name;
96
+ }
97
+ } catch (e) {
98
+ // 继续
99
+ }
100
+ }
101
+
102
+ // 3. 检查 pubspec.yaml
103
+ const pubspecPath = path.join(CWD, "pubspec.yaml");
104
+ if (fs.existsSync(pubspecPath)) {
105
+ try {
106
+ const content = fs.readFileSync(pubspecPath, "utf-8");
107
+ const match = content.match(/^name:\s*(.+?)$/m);
108
+ if (match && match[1]) {
109
+ const name = sanitizeProjectName(match[1]);
110
+ log(`从 pubspec.yaml 检测到项目名: ${name}`);
111
+ return name;
112
+ }
113
+ } catch (e) {
114
+ // 继续
115
+ }
116
+ }
117
+
118
+ // 4. 使用目录名
119
+ const dirName = path.basename(CWD);
120
+ const name = sanitizeProjectName(dirName);
121
+ log(`使用目录名作为项目名: ${name}`);
122
+ return name;
123
+ }
124
+
125
+ // 清理项目名,确保可以用作分支名
126
+ export function sanitizeProjectName(name: string): string {
127
+ return name
128
+ .toLowerCase()
129
+ .replace(/[^a-z0-9-_]/g, "-") // 非法字符替换为 -
130
+ .replace(/-+/g, "-") // 多个 - 合并
131
+ .replace(/^-|-$/g, ""); // 去掉首尾 -
132
+ }
133
+
134
+ export function normalizeRulesType(input: string): ProjectType {
135
+ const normalized = input.toLowerCase().trim();
136
+
137
+ if (["ios", "i", "iphone", "objective-c", "objc", "oc"].includes(normalized)) {
138
+ return "ios";
139
+ }
140
+ if (["android", "a", "kotlin", "java"].includes(normalized)) {
141
+ return "android";
142
+ }
143
+ if (["web", "w", "javascript", "js", "typescript", "ts", "react", "vue", "angular"].includes(normalized)) {
144
+ return "web";
145
+ }
146
+ if (["flutter", "f", "dart"].includes(normalized)) {
147
+ return "flutter";
148
+ }
149
+
150
+ return "unknown";
151
+ }
152
+
153
+ export function validateAndGetRulesType(rulesType?: string): { type: ProjectType; error?: string } {
154
+ if (rulesType) {
155
+ const normalized = normalizeRulesType(rulesType);
156
+ if (normalized === "unknown") {
157
+ return {
158
+ type: "ios",
159
+ error: `❌ 不支持的 Rules 类型: ${rulesType}\n支持的类型: iOS/Android/Web/Flutter (或其他别名)`,
160
+ };
161
+ }
162
+ log(`使用用户指定的 Rules 类型: ${normalized}`);
163
+ return { type: normalized };
164
+ }
165
+
166
+ const projectType = detectProjectType();
167
+ return { type: projectType };
168
+ }
169
+
170
+ export function validateAndGetIDE(ide?: string): { ide: string; path: string; error?: string } {
171
+ const targetIDE = ide || detectCurrentIDE();
172
+
173
+ const idePath = IDE_PATHS[targetIDE];
174
+ if (!idePath) {
175
+ return {
176
+ ide: targetIDE,
177
+ path: "",
178
+ error: `❌ 不支持的 IDE: ${targetIDE}\n支持的 IDE: kiro, cursor, windsurf`,
179
+ };
180
+ }
181
+
182
+ return { ide: targetIDE, path: idePath };
183
+ }
184
+
185
+ export function getGitRulesDir(projectType: ProjectType): string {
186
+ const dirMap: Record<ProjectType, string> = {
187
+ ios: "iOS",
188
+ android: "Android",
189
+ web: "Web",
190
+ flutter: "Flutter",
191
+ unknown: "iOS",
192
+ };
193
+ return dirMap[projectType];
194
+ }
195
+
196
+ export function detectCurrentIDE(): string {
197
+ if (process.env.KIRO_HOME) return "kiro";
198
+ if (process.env.CURSOR_HOME) return "cursor";
199
+ if (process.env.WINDSURF_HOME) return "windsurf";
200
+
201
+ if (fs.existsSync(path.join(HOME, ".kiro"))) return "kiro";
202
+ if (fs.existsSync(path.join(HOME, ".cursor"))) return "cursor";
203
+ if (fs.existsSync(path.join(HOME, ".codeium/windsurf"))) return "windsurf";
204
+
205
+ return "kiro";
206
+ }
207
+
208
+ // Git 操作公共函数
209
+ export function initializeGitCache(): void {
210
+ const gitDir = path.join(RULES_CACHE_DIR, ".git");
211
+
212
+ // 检查 .git 目录是否存在且有效
213
+ if (fs.existsSync(gitDir)) {
214
+ try {
215
+ execSync(`git -C ${RULES_CACHE_DIR} status`, { stdio: "pipe" });
216
+ return; // Git 仓库有效
217
+ } catch (e) {
218
+ log(`Git 仓库损坏,重新初始化...`);
219
+ fs.rmSync(RULES_CACHE_DIR, { recursive: true, force: true });
220
+ }
221
+ }
222
+
223
+ log(`创建缓存目录: ${RULES_CACHE_DIR}`);
224
+ fs.mkdirSync(RULES_CACHE_DIR, { recursive: true });
225
+
226
+ log(`初始化 Git 仓库...`);
227
+ execSync(`git init ${RULES_CACHE_DIR}`, { stdio: "inherit" });
228
+
229
+ log(`添加远程: ${GIT_REPO}`);
230
+ execSync(
231
+ `git -C ${RULES_CACHE_DIR} remote add origin ${GIT_REPO}`,
232
+ { stdio: "inherit" }
233
+ );
234
+ }
235
+
236
+ export function setupSparseCheckout(rulesDir: string): void {
237
+ log(`配置 sparse-checkout...`);
238
+ execSync(`git -C ${RULES_CACHE_DIR} config core.sparseCheckout true`, {
239
+ stdio: "inherit",
240
+ });
241
+
242
+ const sparseCheckoutFile = path.join(
243
+ RULES_CACHE_DIR,
244
+ ".git/info/sparse-checkout"
245
+ );
246
+ fs.writeFileSync(sparseCheckoutFile, `${rulesDir}/\n`);
247
+ log(`设置只克隆 ${rulesDir} 目录...`);
248
+ }
249
+
250
+ export function checkoutOrCreateBranch(branchName: string, baseBranch: string = GIT_BRANCH): void {
251
+ log(`检出或创建分支 ${branchName}...`);
252
+
253
+ // 先尝试检出远程分支(如果存在)
254
+ try {
255
+ execSync(`git -C ${RULES_CACHE_DIR} fetch origin ${branchName}`, {
256
+ stdio: "pipe",
257
+ });
258
+ // 远程分支存在,检出并跟踪
259
+ execSync(`git -C ${RULES_CACHE_DIR} checkout -B ${branchName} origin/${branchName}`, {
260
+ stdio: "inherit",
261
+ });
262
+ log(`检出远程分支: ${branchName}`);
263
+ return;
264
+ } catch (e) {
265
+ // 远程分支不存在,继续创建新分支
266
+ }
267
+
268
+ // 基于主分支创建新分支
269
+ log(`远程分支不存在,基于 ${baseBranch} 创建...`);
270
+ try {
271
+ execSync(`git -C ${RULES_CACHE_DIR} checkout -B ${baseBranch} origin/${baseBranch}`, {
272
+ stdio: "inherit",
273
+ });
274
+ execSync(`git -C ${RULES_CACHE_DIR} checkout -b ${branchName}`, {
275
+ stdio: "inherit",
276
+ });
277
+ log(`创建新分支: ${branchName}`);
278
+ } catch (e2) {
279
+ log(`主分支不存在,创建孤立分支...`);
280
+ execSync(`git -C ${RULES_CACHE_DIR} checkout --orphan ${branchName}`, {
281
+ stdio: "inherit",
282
+ });
283
+ log(`创建孤立分支: ${branchName}`);
284
+ }
285
+ }
286
+
287
+ export async function fetchRulesFromGit(projectType: ProjectType): Promise<string> {
288
+ const rulesDir = getGitRulesDir(projectType);
289
+
290
+ log(`Git 仓库: ${GIT_REPO}`);
291
+ log(`分支: ${GIT_BRANCH}`);
292
+ log(`Rules 目录: ${rulesDir}`);
293
+
294
+ // 初始化缓存仓库
295
+ initializeGitCache();
296
+
297
+ // 设置 sparse-checkout
298
+ setupSparseCheckout(rulesDir);
299
+
300
+ // 拉取远程分支
301
+ log(`拉取分支 ${GIT_BRANCH}...`);
302
+ execSync(`git -C ${RULES_CACHE_DIR} fetch origin ${GIT_BRANCH}`, {
303
+ stdio: "inherit",
304
+ });
305
+
306
+ // 检出到 origin/develop(避免本地分支不存在的问题)
307
+ log(`检出 origin/${GIT_BRANCH}...`);
308
+ try {
309
+ execSync(`git -C ${RULES_CACHE_DIR} checkout -B ${GIT_BRANCH} origin/${GIT_BRANCH}`, {
310
+ stdio: "inherit",
311
+ });
312
+ } catch (e) {
313
+ // 如果失败,尝试直接检出 FETCH_HEAD
314
+ execSync(`git -C ${RULES_CACHE_DIR} checkout FETCH_HEAD`, {
315
+ stdio: "inherit",
316
+ });
317
+ }
318
+
319
+ const projectRulesDir = path.join(RULES_CACHE_DIR, rulesDir);
320
+ if (!fs.existsSync(projectRulesDir)) {
321
+ throw new Error(`${rulesDir} 目录不存在: ${projectRulesDir}`);
322
+ }
323
+
324
+ log(`${rulesDir} 目录: ${projectRulesDir}`);
325
+ return projectRulesDir;
326
+ }
327
+
328
+ export function copyMarkdownFiles(
329
+ src: string,
330
+ dest: string,
331
+ options: { backup?: boolean } = { backup: true }
332
+ ): { count: number; backups: string[] } {
333
+ if (!fs.existsSync(dest)) {
334
+ fs.mkdirSync(dest, { recursive: true });
335
+ }
336
+
337
+ let count = 0;
338
+ const backups: string[] = [];
339
+ const files = fs.readdirSync(src);
340
+
341
+ for (const file of files) {
342
+ if (file.startsWith(".") || file === "node_modules") {
343
+ continue;
344
+ }
345
+
346
+ const srcPath = path.join(src, file);
347
+ const destPath = path.join(dest, file);
348
+ const stat = fs.statSync(srcPath);
349
+
350
+ if (stat.isDirectory()) {
351
+ const result = copyMarkdownFiles(srcPath, destPath, options);
352
+ count += result.count;
353
+ backups.push(...result.backups);
354
+ } else if (file.endsWith(".md")) {
355
+ // 如果目标文件已存在且需要备份
356
+ if (options.backup && fs.existsSync(destPath)) {
357
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
358
+ const backupPath = `${destPath}.backup.${timestamp}`;
359
+ fs.copyFileSync(destPath, backupPath);
360
+ backups.push(backupPath);
361
+ log(`备份文件: ${destPath} → ${backupPath}`);
362
+ }
363
+
364
+ fs.copyFileSync(srcPath, destPath);
365
+ count++;
366
+ }
367
+ }
368
+
369
+ return { count, backups };
370
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "moduleResolution": "node",
6
+ "lib": ["ES2020"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }