Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

CLAUDE.md
*.swp

# dependencies
/node_modules
/.pnp
Expand Down Expand Up @@ -34,4 +37,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts


117 changes: 107 additions & 10 deletions __tests__/claude-code-router-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,26 +126,42 @@ describe("ClaudeCodeRouterConfig", () => {
});

it("should create config file with API Key from environment variable", async () => {
process.env.DASHSCOPE_API_KEY = "test-api-key";

await config.createConfigFile();
const testApiKey = "test-api-key-from-env";
await config.createConfigFile(testApiKey);

expect(fs.writeJson).toHaveBeenCalledWith(
config.configFile,
expect.objectContaining({
Providers: expect.arrayContaining([
expect.objectContaining({
api_key: "test-api-key",
api_key: testApiKey,
}),
]),
}),
{ spaces: 2 }
);
});

it("should use undefined API Key when environment variable is not present", async () => {
delete process.env.DASHSCOPE_API_KEY;
it("should create config file with provided API Key", async () => {
const testApiKey = "test-api-key-provided";

await config.createConfigFile(testApiKey);

expect(fs.writeJson).toHaveBeenCalledWith(
config.configFile,
expect.objectContaining({
Providers: expect.arrayContaining([
expect.objectContaining({
api_key: testApiKey,
}),
]),
}),
{ spaces: 2 }
);
});

it("should use undefined API Key when no API Key is provided", async () => {
await config.createConfigFile();

expect(fs.writeJson).toHaveBeenCalledWith(
Expand Down Expand Up @@ -215,23 +231,27 @@ describe("ClaudeCodeRouterConfig", () => {
jest.spyOn(config, "createDirectories").mockResolvedValue();
jest.spyOn(config, "createConfigFile").mockResolvedValue();
jest.spyOn(config, "createTransformerFile").mockResolvedValue();
jest.spyOn(config, "promptForApiKey").mockResolvedValue("user-input-key");
});

afterEach(() => {
console.log.mockRestore();
console.error.mockRestore();
});

it("should successfully complete setup process", async () => {
it("should successfully complete setup process with environment variable", async () => {
process.env.DASHSCOPE_API_KEY = "env-test-key";

await config.setup();

expect(config.createDirectories).toHaveBeenCalled();
expect(config.createConfigFile).toHaveBeenCalled();
expect(config.createConfigFile).toHaveBeenCalledWith("env-test-key");
expect(config.createTransformerFile).toHaveBeenCalled();
expect(config.promptForApiKey).not.toHaveBeenCalled();
});

it("should detect API Key in environment variable", async () => {
process.env.DASHSCOPE_API_KEY = "test-key";
process.env.DASHSCOPE_API_KEY = "test-key-from-env";

await config.setup();

Expand All @@ -240,9 +260,10 @@ describe("ClaudeCodeRouterConfig", () => {
"DASHSCOPE_API_KEY environment variable detected"
)
);
expect(config.createConfigFile).toHaveBeenCalledWith("test-key-from-env");
});

it("should show warning when environment variable is not present", async () => {
it("should prompt for API Key when environment variable is not present", async () => {
delete process.env.DASHSCOPE_API_KEY;

await config.setup();
Expand All @@ -252,6 +273,8 @@ describe("ClaudeCodeRouterConfig", () => {
"DASHSCOPE_API_KEY environment variable not found"
)
);
expect(config.promptForApiKey).toHaveBeenCalled();
expect(config.createConfigFile).toHaveBeenCalledWith("user-input-key");
});

it("should handle errors during setup process", async () => {
Expand All @@ -263,4 +286,78 @@ describe("ClaudeCodeRouterConfig", () => {
expect(process.exit).toHaveBeenCalledWith(1);
});
});

describe("promptForApiKey", () => {
beforeEach(() => {
config = new ClaudeCodeRouterConfig();

// Mock console.log
jest.spyOn(console, "log").mockImplementation();
});

afterEach(() => {
console.log.mockRestore();
});

it("should prompt for API Key and return user input", async () => {
const mockReadline = {
question: jest.fn(),
close: jest.fn()
};

// Mock readline.createInterface
const readline = require("readline");
jest.spyOn(readline, "createInterface").mockReturnValue(mockReadline);

// Mock the question callback to simulate user input
mockReadline.question.mockImplementation((prompt, callback) => {
callback("user-provided-api-key");
});

const result = await config.promptForApiKey();

expect(readline.createInterface).toHaveBeenCalledWith({
input: process.stdin,
output: process.stdout
});
expect(mockReadline.question).toHaveBeenCalledWith(
expect.stringContaining("API Key"),
expect.any(Function)
);
expect(mockReadline.close).toHaveBeenCalled();
expect(result).toBe("user-provided-api-key");
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining("configured")
);
});

it("should re-prompt when empty API Key is provided", async () => {
const mockReadline = {
question: jest.fn(),
close: jest.fn()
};

const readline = require("readline");
jest.spyOn(readline, "createInterface").mockReturnValue(mockReadline);

// First call returns empty string, second call returns valid key
let callCount = 0;
mockReadline.question.mockImplementation((prompt, callback) => {
callCount++;
if (callCount === 1) {
callback(" "); // Empty/whitespace only
} else {
callback("valid-api-key");
}
});

const result = await config.promptForApiKey();

expect(mockReadline.question).toHaveBeenCalledTimes(2);
expect(result).toBe("valid-api-key");
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining("cannot be empty")
);
});
});
});
59 changes: 43 additions & 16 deletions bin/claude-code-router-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const fs = require("fs-extra");
const path = require("path");
const os = require("os");
const readline = require("readline");

class ClaudeCodeRouterConfig {
constructor() {
Expand Down Expand Up @@ -37,6 +38,10 @@ class ClaudeCodeRouterConfig {
step2: "2. 请确保已安装 @musistudio/claude-code-router",
step3Warning: "3. ⚠️ 请手动配置环境变量DASHSCOPE_API_KEY:",
step3Success: "3. ✅ API Key 已从环境变量自动配置",
promptApiKey: "请输入您的 DashScope API Key:",
apiKeyPrompt: "DashScope API Key",
apiKeyConfigured: "✅ API Key 已配置完成",
invalidApiKey: "❌ API Key 不能为空,请重新输入",
step4: "4. 运行 ccr code 开始使用",
configFailed: "❌ 配置失败:",
createDir: "📁 创建目录:",
Expand All @@ -60,6 +65,10 @@ class ClaudeCodeRouterConfig {
step2: "2. Please ensure @musistudio/claude-code-router is installed",
step3Warning: "3. ⚠️ Please manually set your DASHSCOPE_API_KEY environment variable:",
step3Success: "3. ✅ API Key automatically configured from environment variable",
promptApiKey: "Please enter your DashScope API Key:",
apiKeyPrompt: "DashScope API Key",
apiKeyConfigured: "✅ API Key configured successfully",
invalidApiKey: "❌ API Key cannot be empty, please try again",
step4: "4. Run ccr code to start using",
configFailed: "❌ Configuration failed:",
createDir: "📁 Creating directory:",
Expand All @@ -76,23 +85,51 @@ class ClaudeCodeRouterConfig {
return messages[this.language];
}

async promptForApiKey() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

return new Promise((resolve) => {
const askForKey = () => {
rl.question(`${this.messages.promptApiKey} `, (apiKey) => {
const trimmedKey = apiKey.trim();
if (trimmedKey) {
console.log(this.messages.apiKeyConfigured);
rl.close();
resolve(trimmedKey);
} else {
console.log(this.messages.invalidApiKey);
askForKey();
}
});
};
askForKey();
});
}

async setup() {
try {
console.log(this.messages.configuring);

// 检查环境变量
const hasEnvApiKey = !!process.env.DASHSCOPE_API_KEY;
let apiKey = process.env.DASHSCOPE_API_KEY;
const hasEnvApiKey = !!apiKey;

if (hasEnvApiKey) {
console.log(this.messages.envKeyDetected);
} else {
console.log(this.messages.envKeyNotFound);
// 提示用户输入 API Key
apiKey = await this.promptForApiKey();
}

// 创建配置目录
await this.createDirectories();

// 创建配置文件
await this.createConfigFile();
await this.createConfigFile(apiKey);

// 创建插件文件
await this.createTransformerFile();
Expand All @@ -103,17 +140,7 @@ class ClaudeCodeRouterConfig {
console.log(this.messages.usage);
console.log(this.messages.step1);
console.log(this.messages.step2);

if (!hasEnvApiKey) {
console.log(this.messages.step3Warning);
console.log(this.messages.editConfigInstructions[0] + this.configDir);
console.log(this.messages.editConfigInstructions[1]);
console.log(this.messages.editConfigInstructions[2]);
console.log(this.messages.editConfigInstructions[3]);
} else {
console.log(this.messages.step3Success);
}

console.log(this.messages.step3Success);
console.log(this.messages.step4);
} catch (error) {
console.error(this.messages.configFailed, error.message);
Expand All @@ -131,9 +158,9 @@ class ClaudeCodeRouterConfig {
console.log(this.messages.createDir, this.configDir);
}

async createConfigFile() {
// 优先使用环境变量中的 API Key
const dashscopeApiKey = process.env.DASHSCOPE_API_KEY;
async createConfigFile(apiKey) {
// 使用传入的 API Key(可能来自环境变量或用户输入)
const dashscopeApiKey = apiKey;

const configContent = {
LOG: true,
Expand Down
Loading