ccman 0.0.4 → 0.1.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.
@@ -1,4 +1,4 @@
1
- import * as fs from 'fs';
1
+ import * as fse from 'fs-extra';
2
2
  import * as path from 'path';
3
3
  import * as os from 'os';
4
4
  import { ShellEnvVars, ShellWriteResult, ShellType } from '../types';
@@ -20,38 +20,136 @@ export class ShellManager {
20
20
  */
21
21
  async writeToShell(envVars: ShellEnvVars, envName?: string): Promise<ShellWriteResult> {
22
22
  try {
23
- // 1. 写入环境变量到独立的 ccmanrc 文件
23
+ // 1. 写入环境变量到独立的 ccmanrc 文件(通常不会有权限问题)
24
24
  await this.writeCCMANRC(envVars, envName);
25
25
 
26
- // 2. 确保 shell 配置文件中有对 ccmanrc 的引用
27
- const shellUpdateResult = await this.ensureShellReference();
26
+ // 2. 检查shell配置文件权限
27
+ const shellPermissionCheck = this.checkShellWritePermissions();
28
28
 
29
- return {
30
- success: true,
31
- filePath: this.ccmanrcPath,
32
- message: `Environment variables written to ${this.ccmanrcPath}${shellUpdateResult.updated ? ` and shell reference ${shellUpdateResult.action}` : ''}`
33
- };
29
+ // 3. 尝试更新 shell 配置文件引用
30
+ if (shellPermissionCheck.hasWritableShellConfig) {
31
+ try {
32
+ const shellUpdateResult = await this.ensureShellReference();
33
+ return {
34
+ success: true,
35
+ filePath: this.ccmanrcPath,
36
+ message: `环境变量已写入 ${this.ccmanrcPath}${shellUpdateResult.updated ? ` 并${shellUpdateResult.action}shell引用` : ''}`
37
+ };
38
+ } catch (error) {
39
+ // Shell引用失败但有可写配置文件时,提供具体的手动指导
40
+ return this.createManualConfigResult(shellPermissionCheck, String(error));
41
+ }
42
+ } else {
43
+ // 没有可写的shell配置文件,提供完整的手动配置指导
44
+ return this.createManualConfigResult(shellPermissionCheck);
45
+ }
34
46
  } catch (error) {
35
47
  return {
36
48
  success: false,
37
49
  filePath: this.ccmanrcPath,
38
- message: 'Failed to write environment variables',
50
+ message: '写入环境变量失败',
39
51
  error: String(error)
40
52
  };
41
53
  }
42
54
  }
43
55
 
56
+ /**
57
+ * 创建手动配置结果
58
+ */
59
+ private createManualConfigResult(
60
+ shellPermissionCheck: { shellConfigAccess: { file: string; writable: boolean; error?: string }[] },
61
+ shellError?: string
62
+ ): ShellWriteResult {
63
+ const reference = this.generateShellReference().trim();
64
+ const writableFiles = shellPermissionCheck.shellConfigAccess.filter(f => f.writable);
65
+ const nonWritableFiles = shellPermissionCheck.shellConfigAccess.filter(f => !f.writable);
66
+
67
+ let message = `环境变量已写入 ${this.ccmanrcPath},但需要手动配置shell引用。\n\n`;
68
+
69
+ if (writableFiles.length > 0) {
70
+ message += `推荐添加到以下文件之一:\n`;
71
+ writableFiles.forEach(f => {
72
+ message += ` ✅ ${f.file}\n`;
73
+ });
74
+ }
75
+
76
+ if (nonWritableFiles.length > 0) {
77
+ message += `以下文件无写入权限:\n`;
78
+ nonWritableFiles.forEach(f => {
79
+ message += ` ❌ ${f.file} (${f.error})\n`;
80
+ });
81
+ message += `\n可尝试修复权限:\n`;
82
+ nonWritableFiles.forEach(f => {
83
+ if (f.error === '无写入权限') {
84
+ message += ` chmod 644 ${f.file}\n`;
85
+ }
86
+ });
87
+ }
88
+
89
+ message += `\n需要手动添加的内容:\n${reference}`;
90
+
91
+ return {
92
+ success: true, // ccmanrc写入成功,只是需要手动配置
93
+ filePath: this.ccmanrcPath,
94
+ message,
95
+ error: shellError ? `Shell配置自动更新失败: ${shellError}` : '所有shell配置文件都无写入权限'
96
+ };
97
+ }
98
+
99
+ /**
100
+ * 检查shell配置文件写入权限
101
+ */
102
+ private checkShellWritePermissions(): {
103
+ hasWritableShellConfig: boolean;
104
+ shellConfigAccess: { file: string; writable: boolean; error?: string }[];
105
+ } {
106
+ const result = {
107
+ hasWritableShellConfig: false,
108
+ shellConfigAccess: [] as { file: string; writable: boolean; error?: string }[]
109
+ };
110
+
111
+ const shellType = this.detectShell();
112
+ const configFiles = this.getShellConfigFiles(shellType);
113
+
114
+ for (const configFile of configFiles) {
115
+ const fileCheck = { file: configFile, writable: false, error: undefined as string | undefined };
116
+
117
+ try {
118
+ if (fse.pathExistsSync(configFile)) {
119
+ // 文件存在,检查写入权限
120
+ fse.accessSync(configFile, fse.constants.W_OK);
121
+ fileCheck.writable = true;
122
+ result.hasWritableShellConfig = true;
123
+ } else {
124
+ // 文件不存在,检查父目录权限(能否创建文件)
125
+ const dir = path.dirname(configFile);
126
+ if (fse.pathExistsSync(dir)) {
127
+ fse.accessSync(dir, fse.constants.W_OK);
128
+ fileCheck.writable = true;
129
+ result.hasWritableShellConfig = true;
130
+ } else {
131
+ fileCheck.error = '目录不存在';
132
+ }
133
+ }
134
+ } catch (error: any) {
135
+ fileCheck.error = `无写入权限`;
136
+ }
137
+
138
+ result.shellConfigAccess.push(fileCheck);
139
+ }
140
+
141
+ return result;
142
+ }
143
+
44
144
  /**
45
145
  * 写入 ccmanrc 文件
46
146
  */
47
147
  private async writeCCMANRC(envVars: ShellEnvVars, envName?: string): Promise<void> {
48
148
  // 确保 .ccman 目录存在
49
- if (!fs.existsSync(this.ccmanDir)) {
50
- fs.mkdirSync(this.ccmanDir, { recursive: true });
51
- }
149
+ fse.ensureDirSync(this.ccmanDir);
52
150
 
53
151
  const content = this.generateExportStatements(envVars, envName);
54
- fs.writeFileSync(this.ccmanrcPath, content);
152
+ await fse.writeFile(this.ccmanrcPath, content, 'utf8');
55
153
  }
56
154
 
57
155
  /**
@@ -63,8 +161,8 @@ export class ShellManager {
63
161
 
64
162
  // 检查是否已经有引用
65
163
  for (const configFile of configFiles) {
66
- if (fs.existsSync(configFile)) {
67
- const content = fs.readFileSync(configFile, 'utf8');
164
+ if (fse.pathExistsSync(configFile)) {
165
+ const content = fse.readFileSync(configFile, 'utf8');
68
166
  if (this.hasShellReference(content)) {
69
167
  return { updated: false, action: 'already exists' };
70
168
  }
@@ -104,20 +202,32 @@ export class ShellManager {
104
202
  private async addShellReference(configFilePath: string): Promise<void> {
105
203
  // 确保目录存在
106
204
  const dir = path.dirname(configFilePath);
107
- if (!fs.existsSync(dir)) {
108
- fs.mkdirSync(dir, { recursive: true });
109
- }
205
+ fse.ensureDirSync(dir);
110
206
 
111
207
  let content = '';
112
- if (fs.existsSync(configFilePath)) {
113
- content = fs.readFileSync(configFilePath, 'utf8');
208
+ if (fse.pathExistsSync(configFilePath)) {
209
+ try {
210
+ content = fse.readFileSync(configFilePath, 'utf8');
211
+ } catch (error: any) {
212
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
213
+ throw new Error(`无权限读取shell配置文件 ${configFilePath}`);
214
+ }
215
+ throw error;
216
+ }
114
217
  }
115
218
 
116
219
  // 添加对 ccmanrc 的引用
117
220
  const reference = this.generateShellReference();
118
221
  content += reference;
119
222
 
120
- fs.writeFileSync(configFilePath, content);
223
+ try {
224
+ await fse.writeFile(configFilePath, content, 'utf8');
225
+ } catch (error: any) {
226
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
227
+ throw new Error(`无权限修改shell配置文件 ${configFilePath}。\n建议:\n 1. 检查文件权限:chmod 644 ${configFilePath}\n 2. 或手动添加以下内容到该文件:\n${reference.trim()}`);
228
+ }
229
+ throw error;
230
+ }
121
231
  }
122
232
 
123
233
  /**
@@ -148,9 +258,9 @@ export class ShellManager {
148
258
  let lastError: string | undefined;
149
259
 
150
260
  // 1. 删除 ccmanrc 文件
151
- if (fs.existsSync(this.ccmanrcPath)) {
261
+ if (fse.pathExistsSync(this.ccmanrcPath)) {
152
262
  try {
153
- fs.unlinkSync(this.ccmanrcPath);
263
+ fse.removeSync(this.ccmanrcPath);
154
264
  clearedAny = true;
155
265
  } catch (error) {
156
266
  lastError = String(error);
@@ -163,7 +273,7 @@ export class ShellManager {
163
273
 
164
274
  for (const configFile of configFiles) {
165
275
  try {
166
- if (fs.existsSync(configFile)) {
276
+ if (fse.pathExistsSync(configFile)) {
167
277
  await this.removeShellReference(configFile);
168
278
  clearedAny = true;
169
279
  }
@@ -192,14 +302,14 @@ export class ShellManager {
192
302
  * 从配置文件中移除 shell 引用
193
303
  */
194
304
  private async removeShellReference(filePath: string): Promise<void> {
195
- if (!fs.existsSync(filePath)) {
305
+ if (!fse.pathExistsSync(filePath)) {
196
306
  return;
197
307
  }
198
308
 
199
- const content = fs.readFileSync(filePath, 'utf8');
309
+ const content = fse.readFileSync(filePath, 'utf8');
200
310
  const cleanedContent = this.removeShellReferenceFromContent(content);
201
311
 
202
- fs.writeFileSync(filePath, cleanedContent);
312
+ await fse.writeFile(filePath, cleanedContent, 'utf8');
203
313
  }
204
314
 
205
315
  /**
@@ -313,7 +423,7 @@ export ${CONFIG.ENV_VARS.AUTH_TOKEN}="${envVars.ANTHROPIC_AUTH_TOKEN}"
313
423
  */
314
424
  hasEnvVarsInShell(): boolean {
315
425
  // 检查 ccmanrc 文件是否存在
316
- if (fs.existsSync(this.ccmanrcPath)) {
426
+ if (fse.pathExistsSync(this.ccmanrcPath)) {
317
427
  return true;
318
428
  }
319
429
 
@@ -322,8 +432,8 @@ export ${CONFIG.ENV_VARS.AUTH_TOKEN}="${envVars.ANTHROPIC_AUTH_TOKEN}"
322
432
  const configFiles = this.getShellConfigFiles(shellType);
323
433
 
324
434
  for (const configFile of configFiles) {
325
- if (fs.existsSync(configFile)) {
326
- const content = fs.readFileSync(configFile, 'utf8');
435
+ if (fse.pathExistsSync(configFile)) {
436
+ const content = fse.readFileSync(configFile, 'utf8');
327
437
  if (this.hasShellReference(content)) {
328
438
  return true;
329
439
  }
@@ -341,7 +451,7 @@ export ${CONFIG.ENV_VARS.AUTH_TOKEN}="${envVars.ANTHROPIC_AUTH_TOKEN}"
341
451
  const configFiles = this.getShellConfigFiles(shellType);
342
452
 
343
453
  // 找到第一个存在的配置文件
344
- const activeConfigFile = configFiles.find(file => fs.existsSync(file));
454
+ const activeConfigFile = configFiles.find(file => fse.pathExistsSync(file));
345
455
 
346
456
  if (!activeConfigFile) {
347
457
  return {
@@ -404,7 +514,7 @@ export ${CONFIG.ENV_VARS.AUTH_TOKEN}="${envVars.ANTHROPIC_AUTH_TOKEN}"
404
514
  const configFiles = this.getShellConfigFiles(shellType);
405
515
 
406
516
  // 找到第一个存在的配置文件作为活动配置文件
407
- const activeConfigFile = configFiles.find(file => fs.existsSync(file));
517
+ const activeConfigFile = configFiles.find(file => fse.pathExistsSync(file));
408
518
 
409
519
  return {
410
520
  shellType,