cfix 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.
Files changed (52) hide show
  1. package/.env.example +69 -0
  2. package/README.md +1590 -0
  3. package/bin/cfix +14 -0
  4. package/bin/cfix.cmd +6 -0
  5. package/cli/commands/config.js +58 -0
  6. package/cli/commands/doctor.js +240 -0
  7. package/cli/commands/fix.js +211 -0
  8. package/cli/commands/help.js +62 -0
  9. package/cli/commands/init.js +226 -0
  10. package/cli/commands/logs.js +161 -0
  11. package/cli/commands/monitor.js +151 -0
  12. package/cli/commands/project.js +331 -0
  13. package/cli/commands/service.js +133 -0
  14. package/cli/commands/status.js +115 -0
  15. package/cli/commands/task.js +412 -0
  16. package/cli/commands/version.js +19 -0
  17. package/cli/index.js +269 -0
  18. package/cli/lib/config-manager.js +612 -0
  19. package/cli/lib/formatter.js +224 -0
  20. package/cli/lib/process-manager.js +233 -0
  21. package/cli/lib/service-client.js +271 -0
  22. package/cli/scripts/install-completion.js +133 -0
  23. package/package.json +85 -0
  24. package/public/monitor.html +1096 -0
  25. package/scripts/completion.bash +87 -0
  26. package/scripts/completion.zsh +102 -0
  27. package/src/assets/README.md +32 -0
  28. package/src/assets/error.png +0 -0
  29. package/src/assets/icon.png +0 -0
  30. package/src/assets/success.png +0 -0
  31. package/src/claude-cli-service.js +216 -0
  32. package/src/config/index.js +69 -0
  33. package/src/database/manager.js +391 -0
  34. package/src/database/migration.js +252 -0
  35. package/src/git-service.js +1278 -0
  36. package/src/index.js +1658 -0
  37. package/src/logger.js +139 -0
  38. package/src/metrics/collector.js +184 -0
  39. package/src/middleware/auth.js +86 -0
  40. package/src/middleware/rate-limit.js +85 -0
  41. package/src/queue/integration-example.js +283 -0
  42. package/src/queue/task-queue.js +333 -0
  43. package/src/services/notification-limiter.js +48 -0
  44. package/src/services/notification-service.js +115 -0
  45. package/src/services/system-notifier.js +130 -0
  46. package/src/task-manager.js +289 -0
  47. package/src/utils/exec.js +87 -0
  48. package/src/utils/project-lock.js +246 -0
  49. package/src/utils/retry.js +110 -0
  50. package/src/utils/sanitizer.js +174 -0
  51. package/src/websocket/notifier.js +363 -0
  52. package/src/wechat-notifier.js +97 -0
@@ -0,0 +1,612 @@
1
+ /**
2
+ * 配置管理器
3
+ * 管理全局配置和项目配置
4
+ */
5
+
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ /**
10
+ * 共享工作目录路径
11
+ */
12
+ const SHARED_WORK_DIR = process.env.WORK_DIR || path.join(os.homedir(), 'cfix-projects');
13
+
14
+ /**
15
+ * 默认配置
16
+ */
17
+ const DEFAULT_CONFIG = {
18
+ version: '2.0.0',
19
+ service: {
20
+ port: 3008,
21
+ host: 'localhost',
22
+ autoStart: true,
23
+ logLevel: 'info',
24
+ maxConcurrent: 3,
25
+ timeout: 30000
26
+ },
27
+ server: {
28
+ workDir: SHARED_WORK_DIR,
29
+ corsOrigin: '*'
30
+ },
31
+ projects: {
32
+ defaultWorkDir: SHARED_WORK_DIR,
33
+ defaultProject: null,
34
+ registry: []
35
+ },
36
+ ai: {
37
+ defaultEngine: 'claude-cli',
38
+ claudeCLI: {
39
+ path: 'claude',
40
+ permissionMode: 'acceptEdits',
41
+ allowedTools: 'Bash(*) Read(*) Edit(*) Write(*)',
42
+ debug: false,
43
+ maxTurns: 20,
44
+ timeout: 1800000
45
+ }
46
+ },
47
+ cli: {
48
+ output: 'interactive',
49
+ spinner: true,
50
+ colors: true,
51
+ confirmChanges: true,
52
+ autoOpenMonitor: false
53
+ },
54
+ notification: {
55
+ enabled: false,
56
+ wecomWebhook: null,
57
+ systemNotification: {
58
+ enabled: false,
59
+ showOnTaskStart: true,
60
+ showOnTaskComplete: true,
61
+ showOnTaskFailure: true,
62
+ showOnApproval: true
63
+ }
64
+ },
65
+ logging: {
66
+ level: 'info'
67
+ },
68
+ security: {
69
+ apiKey: null,
70
+ allowedIPs: [],
71
+ rateLimit: {
72
+ windowMs: 900000, // 15分钟
73
+ max: 20
74
+ }
75
+ },
76
+ redis: {
77
+ host: null,
78
+ port: 6379,
79
+ password: null
80
+ },
81
+ queue: {
82
+ maxConcurrent: 3,
83
+ useRedis: false
84
+ }
85
+ };
86
+
87
+ /**
88
+ * 配置管理器类
89
+ */
90
+ class ConfigManager {
91
+ constructor() {
92
+ this.store = {};
93
+ this.configDir = this.getConfigDir();
94
+ this.configFile = path.join(this.configDir, 'config.json');
95
+ this.init();
96
+ }
97
+
98
+ /**
99
+ * 获取配置目录
100
+ */
101
+ getConfigDir() {
102
+ const platform = process.platform;
103
+ let baseDir;
104
+
105
+ if (platform === 'win32') {
106
+ baseDir = process.env.LOCALAPPDATA || path.join(os.homedir(), '.cfix');
107
+ } else {
108
+ baseDir = path.join(os.homedir(), '.cfix');
109
+ }
110
+
111
+ return baseDir;
112
+ }
113
+
114
+ /**
115
+ * 从环境变量加载配置
116
+ */
117
+ loadFromEnv() {
118
+ return {
119
+ server: {
120
+ port: process.env.PORT ? parseInt(process.env.PORT) : undefined,
121
+ workDir: process.env.WORK_DIR || undefined,
122
+ corsOrigin: process.env.CORS_ORIGIN || undefined
123
+ },
124
+ ai: {
125
+ defaultEngine: process.env.AI_ENGINE || undefined,
126
+ claudeCLI: {
127
+ path: process.env.CLAUDE_CLI_PATH || undefined,
128
+ permissionMode: process.env.CLAUDE_PERMISSION_MODE || undefined,
129
+ allowedTools: process.env.CLAUDE_ALLOWED_TOOLS || undefined,
130
+ debug: process.env.CLAUDE_DEBUG === 'true' ? true : (process.env.CLAUDE_DEBUG === 'false' ? false : undefined),
131
+ maxTurns: process.env.MAX_CONVERSATION_TURNS ? parseInt(process.env.MAX_CONVERSATION_TURNS) : undefined
132
+ }
133
+ },
134
+ security: {
135
+ apiKey: process.env.API_KEY || undefined,
136
+ rateLimit: {
137
+ max: process.env.RATE_LIMIT_MAX ? parseInt(process.env.RATE_LIMIT_MAX) : undefined
138
+ }
139
+ },
140
+ notification: {
141
+ wecomWebhook: process.env.WECOM_WEBHOOK || undefined,
142
+ systemNotification: {
143
+ enabled: process.env.SYSTEM_NOTIFICATION_ENABLED === 'true' ? true : (process.env.SYSTEM_NOTIFICATION_ENABLED === 'false' ? false : undefined)
144
+ }
145
+ },
146
+ logging: {
147
+ level: process.env.LOG_LEVEL || undefined
148
+ },
149
+ redis: {
150
+ host: process.env.REDIS_HOST || undefined,
151
+ port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined,
152
+ password: process.env.REDIS_PASSWORD || undefined
153
+ },
154
+ queue: {
155
+ maxConcurrent: process.env.QUEUE_MAX_CONCURRENT ? parseInt(process.env.QUEUE_MAX_CONCURRENT) : undefined,
156
+ useRedis: process.env.USE_REDIS_QUEUE === 'true' ? true : (process.env.USE_REDIS_QUEUE === 'false' ? false : undefined)
157
+ }
158
+ };
159
+ }
160
+
161
+ /**
162
+ * 判断是否为对象
163
+ */
164
+ isObject(item) {
165
+ return item && typeof item === 'object' && !Array.isArray(item);
166
+ }
167
+
168
+ /**
169
+ * 深度合并配置
170
+ * 跳过 undefined 值,保留已有的有效值
171
+ */
172
+ deepMerge(target, ...sources) {
173
+ if (!sources.length) return target;
174
+ const source = sources.shift();
175
+
176
+ if (this.isObject(target) && this.isObject(source)) {
177
+ for (const key in source) {
178
+ if (source[key] === undefined) continue; // 跳过 undefined
179
+
180
+ if (this.isObject(source[key])) {
181
+ if (!target[key]) Object.assign(target, { [key]: {} });
182
+ this.deepMerge(target[key], source[key]);
183
+ } else {
184
+ Object.assign(target, { [key]: source[key] });
185
+ }
186
+ }
187
+ }
188
+
189
+ return this.deepMerge(target, ...sources);
190
+ }
191
+
192
+ /**
193
+ * 初始化配置
194
+ */
195
+ init() {
196
+ // 确保配置目录存在
197
+ if (!fs.existsSync(this.configDir)) {
198
+ fs.mkdirSync(this.configDir, { recursive: true });
199
+ }
200
+
201
+ // 1. 加载默认配置(深拷贝)
202
+ const defaults = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
203
+
204
+ // 2. 加载环境变量
205
+ const envConfig = this.loadFromEnv();
206
+
207
+ // 3. 加载配置文件
208
+ let fileConfig = {};
209
+ if (fs.existsSync(this.configFile)) {
210
+ try {
211
+ const content = fs.readFileSync(this.configFile, 'utf-8');
212
+ fileConfig = JSON.parse(content);
213
+ } catch (error) {
214
+ console.warn(`配置文件损坏,使用默认配置: ${error.message}`);
215
+ }
216
+ }
217
+ // console.log('envConfig', envConfig,defaults, fileConfig)
218
+ // 4. 深度合并: 默认值 <- 环境变量 <- 配置文件
219
+ this.store = this.deepMerge(this.deepMerge(defaults, envConfig), fileConfig);
220
+
221
+ // 5. 配置迁移
222
+ this.migrate();
223
+
224
+ // 6. 自动保存(如果配置文件不存在)
225
+ if (!fs.existsSync(this.configFile)) {
226
+ this.save();
227
+ }
228
+ }
229
+
230
+ /**
231
+ * 配置迁移
232
+ */
233
+ migrate() {
234
+ // 检查版本
235
+ if (!this.store.version || this.store.version < '2.0.0') {
236
+ console.log('检测到旧版配置,正在迁移...');
237
+
238
+ // 迁移 AI 配置
239
+ if (this.store.ai && !this.store.ai.claudeCLI) {
240
+ this.store.ai.claudeCLI = {
241
+ permissionMode: this.store.ai.permissionMode || 'acceptEdits',
242
+ maxTurns: this.store.ai.maxConversations || 20,
243
+ timeout: this.store.ai.timeout || 1800000,
244
+ path: 'claude',
245
+ allowedTools: 'Bash(*) Read(*) Edit(*) Write(*)',
246
+ debug: false
247
+ };
248
+ delete this.store.ai.maxConversations;
249
+ delete this.store.ai.permissionMode;
250
+ delete this.store.ai.timeout;
251
+ }
252
+
253
+ // 迁移 service.maxConcurrent 到 queue.maxConcurrent
254
+ if (this.store.service?.maxConcurrent && !this.store.queue?.maxConcurrent) {
255
+ if (!this.store.queue) this.store.queue = {};
256
+ this.store.queue.maxConcurrent = this.store.service.maxConcurrent;
257
+ }
258
+
259
+ // 迁移 notifications 到 notification
260
+ if (this.store.notifications && !this.store.notification) {
261
+ this.store.notification = this.store.notifications;
262
+ delete this.store.notifications;
263
+ }
264
+
265
+ // 添加缺少的配置块
266
+ if (!this.store.server) {
267
+ this.store.server = {
268
+ workDir: this.store.projects?.defaultWorkDir || DEFAULT_CONFIG.server.workDir,
269
+ corsOrigin: '*'
270
+ };
271
+ }
272
+
273
+ if (!this.store.security) {
274
+ this.store.security = DEFAULT_CONFIG.security;
275
+ }
276
+
277
+ if (!this.store.logging) {
278
+ this.store.logging = { level: this.store.service?.logLevel || 'info' };
279
+ }
280
+
281
+ if (!this.store.redis) {
282
+ this.store.redis = DEFAULT_CONFIG.redis;
283
+ }
284
+
285
+ this.store.version = '2.0.0';
286
+ this.save();
287
+ console.log('✅ 配置已迁移到 v2.0.0');
288
+ }
289
+ }
290
+
291
+ /**
292
+ * 保存配置
293
+ */
294
+ save() {
295
+ fs.writeFileSync(this.configFile, JSON.stringify(this.store, null, 2), 'utf-8');
296
+ }
297
+
298
+ /**
299
+ * 获取全局配置
300
+ * @param {string} key - 配置键(支持点号分隔的路径)
301
+ * @returns {*} 配置值
302
+ */
303
+ get(key) {
304
+ if (!key) {
305
+ return this.store;
306
+ }
307
+
308
+ // 支持点号分隔的路径,如 'service.port'
309
+ const keys = key.split('.');
310
+ let value = this.store;
311
+
312
+ for (const k of keys) {
313
+ if (value && typeof value === 'object') {
314
+ value = value[k];
315
+ } else {
316
+ return undefined;
317
+ }
318
+ }
319
+
320
+ return value;
321
+ }
322
+
323
+ /**
324
+ * 设置全局配置
325
+ * @param {string} key - 配置键
326
+ * @param {*} value - 配置值
327
+ */
328
+ set(key, value) {
329
+ // 支持点号分隔的路径
330
+ const keys = key.split('.');
331
+ let target = this.store;
332
+
333
+ for (let i = 0; i < keys.length - 1; i++) {
334
+ const k = keys[i];
335
+ if (!(k in target) || typeof target[k] !== 'object') {
336
+ target[k] = {};
337
+ }
338
+ target = target[k];
339
+ }
340
+
341
+ target[keys[keys.length - 1]] = value;
342
+
343
+ // 🔑 同步工作目录配置
344
+ // 当设置 server.workDir 时,同时更新 projects.defaultWorkDir
345
+ if (key === 'server.workDir') {
346
+ if (!this.store.projects) this.store.projects = {};
347
+ this.store.projects.defaultWorkDir = value;
348
+ }
349
+ // 当设置 projects.defaultWorkDir 时,同时更新 server.workDir
350
+ else if (key === 'projects.defaultWorkDir') {
351
+ if (!this.store.server) this.store.server = {};
352
+ this.store.server.workDir = value;
353
+ }
354
+
355
+ this.save();
356
+ }
357
+
358
+ /**
359
+ * 删除配置
360
+ * @param {string} key - 配置键
361
+ */
362
+ delete(key) {
363
+ const keys = key.split('.');
364
+ let target = this.store;
365
+
366
+ for (let i = 0; i < keys.length - 1; i++) {
367
+ const k = keys[i];
368
+ if (!(k in target) || typeof target[k] !== 'object') {
369
+ return;
370
+ }
371
+ target = target[k];
372
+ }
373
+
374
+ delete target[keys[keys.length - 1]];
375
+ this.save();
376
+ }
377
+
378
+ /**
379
+ * 清空所有配置
380
+ */
381
+ clear() {
382
+ this.store = { ...DEFAULT_CONFIG };
383
+ this.save();
384
+ }
385
+
386
+ /**
387
+ * 获取配置文件路径
388
+ */
389
+ getConfigPath() {
390
+ return this.configFile;
391
+ }
392
+
393
+ /**
394
+ * 获取项目配置
395
+ * @param {string} projectPath - 项目路径
396
+ * @returns {Object|null} 项目配置
397
+ */
398
+ getProjectConfig(projectPath) {
399
+ const configFile = path.join(projectPath, '.cfix.json');
400
+ if (!fs.existsSync(configFile)) {
401
+ return null;
402
+ }
403
+
404
+ try {
405
+ const content = fs.readFileSync(configFile, 'utf-8');
406
+ return JSON.parse(content);
407
+ } catch (error) {
408
+ throw new Error(`读取项目配置失败: ${error.message}`);
409
+ }
410
+ }
411
+
412
+ /**
413
+ * 保存项目配置
414
+ * @param {string} projectPath - 项目路径
415
+ * @param {Object} config - 配置对象
416
+ */
417
+ saveProjectConfig(projectPath, config) {
418
+ const configFile = path.join(projectPath, '.cfix.json');
419
+ const content = JSON.stringify(config, null, 2);
420
+ fs.writeFileSync(configFile, content, 'utf-8');
421
+ }
422
+
423
+ /**
424
+ * 添加项目到注册表
425
+ * @param {Object} project - 项目对象
426
+ */
427
+ addProject(project) {
428
+ const projects = this.get('projects.registry') || [];
429
+
430
+ // 检查是否已存在
431
+ const exists = projects.some(p => p.path === project.path);
432
+ if (exists) {
433
+ throw new Error('项目已存在');
434
+ }
435
+
436
+ projects.push(project);
437
+ this.set('projects.registry', projects);
438
+ }
439
+
440
+ /**
441
+ * 从注册表移除项目
442
+ * @param {string} identifier - 项目标识(路径/名称/别名)
443
+ */
444
+ removeProject(identifier) {
445
+ const projects = this.get('projects.registry') || [];
446
+ const project = this.findProject(identifier);
447
+
448
+ if (!project) {
449
+ throw new Error('项目不存在');
450
+ }
451
+
452
+ const index = projects.indexOf(project);
453
+ projects.splice(index, 1);
454
+ this.set('projects.registry', projects);
455
+ }
456
+
457
+ /**
458
+ * 查找项目
459
+ * @param {string} identifier - 项目标识
460
+ * @returns {Object|null} 项目对象
461
+ */
462
+ findProject(identifier) {
463
+ const projects = this.get('projects.registry') || [];
464
+
465
+ // 按路径查找
466
+ const resolvedPath = path.resolve(identifier);
467
+ let project = projects.find(p => p.path === resolvedPath);
468
+ if (project) return project;
469
+
470
+ // 按名称查找
471
+ project = projects.find(p => p.name === identifier);
472
+ if (project) return project;
473
+
474
+ // 按别名查找
475
+ project = projects.find(p => p.alias === identifier);
476
+ if (project) return project;
477
+
478
+ return null;
479
+ }
480
+
481
+ /**
482
+ * 列出所有项目
483
+ */
484
+ listProjects() {
485
+ return this.get('projects.registry') || [];
486
+ }
487
+
488
+ /**
489
+ * 设置默认项目
490
+ * @param {string} identifier - 项目标识
491
+ */
492
+ setDefaultProject(identifier) {
493
+ const project = this.findProject(identifier);
494
+ if (!project) {
495
+ throw new Error('项目不存在');
496
+ }
497
+ this.set('projects.defaultProject', project.path);
498
+ }
499
+
500
+ /**
501
+ * 获取默认项目
502
+ */
503
+ getDefaultProject() {
504
+ const defaultPath = this.get('projects.defaultProject');
505
+ if (!defaultPath) {
506
+ return null;
507
+ }
508
+ return this.findProject(defaultPath);
509
+ }
510
+
511
+ /**
512
+ * 检测当前目录项目
513
+ */
514
+ detectCurrentProject() {
515
+ const cwd = process.cwd();
516
+
517
+ // 检查是否为 Git 仓库
518
+ const gitDir = path.join(cwd, '.git');
519
+ if (!fs.existsSync(gitDir)) {
520
+ return null;
521
+ }
522
+
523
+ // 读取项目配置
524
+ const projectConfig = this.getProjectConfig(cwd);
525
+ if (projectConfig) {
526
+ return {
527
+ path: cwd,
528
+ ...projectConfig
529
+ };
530
+ }
531
+
532
+ // 在注册表中查找
533
+ return this.findProject(cwd);
534
+ }
535
+
536
+ /**
537
+ * 获取服务配置
538
+ */
539
+ getServiceConfig() {
540
+ return {
541
+ port: this.get('service.port'),
542
+ host: this.get('service.host'),
543
+ autoStart: this.get('service.autoStart'),
544
+ logLevel: this.get('service.logLevel'),
545
+ maxConcurrent: this.get('service.maxConcurrent'),
546
+ timeout: this.get('service.timeout')
547
+ };
548
+ }
549
+
550
+ /**
551
+ * 获取 AI 配置
552
+ */
553
+ getAIConfig() {
554
+ return {
555
+ defaultEngine: this.get('ai.defaultEngine'),
556
+ claudeCLI: this.get('ai.claudeCLI')
557
+ };
558
+ }
559
+
560
+ /**
561
+ * 获取 CLI 配置
562
+ */
563
+ getCLIConfig() {
564
+ return {
565
+ output: this.get('cli.output'),
566
+ spinner: this.get('cli.spinner'),
567
+ colors: this.get('cli.colors'),
568
+ confirmChanges: this.get('cli.confirmChanges'),
569
+ autoOpenMonitor: this.get('cli.autoOpenMonitor')
570
+ };
571
+ }
572
+
573
+ /**
574
+ * 验证配置
575
+ * @param {Object} config - 配置对象
576
+ */
577
+ validateConfig(config) {
578
+ // 端口验证(检查 service.port 和 server.port)
579
+ if (config.service && config.service.port) {
580
+ const port = parseInt(config.service.port);
581
+ if (isNaN(port) || port < 1 || port > 65535) {
582
+ throw new Error('无效的端口号');
583
+ }
584
+ }
585
+ if (config.server && config.server.port) {
586
+ const port = parseInt(config.server.port);
587
+ if (isNaN(port) || port < 1 || port > 65535) {
588
+ throw new Error('无效的端口号');
589
+ }
590
+ }
591
+
592
+ // AI 引擎验证
593
+ if (config.ai && config.ai.defaultEngine) {
594
+ if (config.ai.defaultEngine !== 'claude-cli') {
595
+ throw new Error('无效的 AI 引擎,当前仅支持 claude-cli');
596
+ }
597
+ }
598
+
599
+ // Claude CLI 配置验证
600
+ if (config.ai?.claudeCLI) {
601
+ const cli = config.ai.claudeCLI;
602
+ if (cli.maxTurns && (cli.maxTurns < 1 || cli.maxTurns > 100)) {
603
+ throw new Error('maxTurns 必须在 1-100 之间');
604
+ }
605
+ }
606
+
607
+ return true;
608
+ }
609
+ }
610
+
611
+ // 导出单例
612
+ module.exports = new ConfigManager();