recoder-code 1.0.113
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/.babelrc +4 -0
- package/.claude/commands/commit-push-pr.md +19 -0
- package/.claude/commands/dedupe.md +38 -0
- package/.devcontainer/Dockerfile +91 -0
- package/.devcontainer/devcontainer.json +57 -0
- package/.devcontainer/init-firewall.sh +137 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +188 -0
- package/.github/ISSUE_TEMPLATE/config.yml +17 -0
- package/.github/ISSUE_TEMPLATE/documentation.yml +117 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +132 -0
- package/.github/ISSUE_TEMPLATE/model_behavior.yml +220 -0
- package/.github/workflows/auto-close-duplicates.yml +31 -0
- package/.github/workflows/backfill-duplicate-comments.yml +44 -0
- package/.github/workflows/claude-dedupe-issues.yml +80 -0
- package/.github/workflows/claude-issue-triage.yml +106 -0
- package/.github/workflows/claude.yml +37 -0
- package/.github/workflows/issue-opened-dispatch.yml +28 -0
- package/.github/workflows/lock-closed-issues.yml +92 -0
- package/.github/workflows/log-issue-events.yml +40 -0
- package/CHANGELOG.md +646 -0
- package/KILO.md +1273 -0
- package/LICENSE.md +21 -0
- package/README.md +176 -0
- package/SECURITY.md +12 -0
- package/Script/run_devcontainer_claude_code.ps1 +152 -0
- package/api/githubApi.ts +144 -0
- package/babel.config.js +7 -0
- package/cli/.gitkeep +0 -0
- package/cli/auto-close-duplicates.ts +5 -0
- package/cli/configure.js +33 -0
- package/cli/list-models.js +48 -0
- package/cli/run.js +61 -0
- package/cli/set-api-key.js +26 -0
- package/config.json +4 -0
- package/demo.gif +0 -0
- package/examples/gpt-3.5-turbo.js +38 -0
- package/examples/gpt-4.js +38 -0
- package/examples/hooks/bash_command_validator_example.py +83 -0
- package/index.d.ts +3 -0
- package/index.js +62 -0
- package/jest.config.js +6 -0
- package/openapi.yaml +61 -0
- package/package.json +47 -0
- package/scripts/backfill-duplicate-comments.ts +213 -0
- package/tests/api-githubApi.test.ts +30 -0
- package/tests/auto-close-duplicates.test.ts +145 -0
- package/tests/cli-configure.test.ts +88 -0
- package/tests/cli-list-models.test.ts +44 -0
- package/tests/cli-run.test.ts +97 -0
- package/tests/cli-set-api-key.test.ts +54 -0
- package/tests/cli-validate-api-key.test.ts +52 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { autoCloseDuplicates } from '../api/githubApi';
|
|
2
|
+
import { Octokit } from '@octokit/rest';
|
|
3
|
+
import { expect, describe, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
vi.mock('@octokit/rest', () => {
|
|
6
|
+
return {
|
|
7
|
+
Octokit: vi.fn().mockImplementation(() => ({
|
|
8
|
+
issues: {
|
|
9
|
+
listForRepo: vi.fn(),
|
|
10
|
+
createComment: vi.fn(),
|
|
11
|
+
update: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
})),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
vi.mock('openai', async () => {
|
|
18
|
+
const actual = await vi.importActual('openai');
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
default: class MockOpenAI {
|
|
22
|
+
constructor() {}
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('autoCloseDuplicates', () => {
|
|
28
|
+
it('should close duplicate issues', async () => {
|
|
29
|
+
const mockOctokit = new Octokit();
|
|
30
|
+
const listForRepo = mockOctokit.issues.listForRepo as unknown as ReturnType<typeof vi.fn>;
|
|
31
|
+
const createComment = mockOctokit.issues.createComment as unknown as ReturnType<typeof vi.fn>;
|
|
32
|
+
const update = mockOctokit.issues.update as unknown as ReturnType<typeof vi.fn>;
|
|
33
|
+
listForRepo.mockResolvedValue({
|
|
34
|
+
data: [
|
|
35
|
+
{ number: 1, title: 'Duplicate Issue' },
|
|
36
|
+
{ number: 2, title: 'Duplicate Issue' },
|
|
37
|
+
{ number: 3, title: 'Unique Issue' },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await autoCloseDuplicates();
|
|
42
|
+
|
|
43
|
+
expect(listForRepo).toHaveBeenCalledWith({
|
|
44
|
+
owner: process.env.GITHUB_OWNER,
|
|
45
|
+
repo: process.env.GITHUB_REPO,
|
|
46
|
+
state: 'open',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(createComment).toHaveBeenCalledWith({
|
|
50
|
+
owner: process.env.GITHUB_OWNER,
|
|
51
|
+
repo: process.env.GITHUB_REPO,
|
|
52
|
+
issue_number: 2,
|
|
53
|
+
body: 'Closing as duplicate of #1',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(update).toHaveBeenCalledWith({
|
|
57
|
+
owner: process.env.GITHUB_OWNER,
|
|
58
|
+
repo: process.env.GITHUB_REPO,
|
|
59
|
+
issue_number: 2,
|
|
60
|
+
state: 'closed',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should not close issues if no duplicates are found', async () => {
|
|
65
|
+
const mockOctokit = new Octokit();
|
|
66
|
+
const listForRepo = mockOctokit.issues.listForRepo as unknown as ReturnType<typeof vi.fn>;
|
|
67
|
+
const createComment = mockOctokit.issues.createComment as unknown as ReturnType<typeof vi.fn>;
|
|
68
|
+
const update = mockOctokit.issues.update as unknown as ReturnType<typeof vi.fn>;
|
|
69
|
+
listForRepo.mockResolvedValue({
|
|
70
|
+
data: [
|
|
71
|
+
{ number: 1, title: 'Unique Issue 1' },
|
|
72
|
+
{ number: 2, title: 'Unique Issue 2' },
|
|
73
|
+
{ number: 3, title: 'Unique Issue 3' },
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await autoCloseDuplicates();
|
|
78
|
+
|
|
79
|
+
expect(listForRepo).toHaveBeenCalledWith({
|
|
80
|
+
owner: process.env.GITHUB_OWNER,
|
|
81
|
+
repo: process.env.GITHUB_REPO,
|
|
82
|
+
state: 'open',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(createComment).not.toHaveBeenCalled();
|
|
86
|
+
expect(update).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle errors when fetching issues', async () => {
|
|
90
|
+
const mockOctokit = new Octokit();
|
|
91
|
+
const listForRepo = mockOctokit.issues.listForRepo as unknown as ReturnType<typeof vi.fn>;
|
|
92
|
+
const createComment = mockOctokit.issues.createComment as unknown as ReturnType<typeof vi.fn>;
|
|
93
|
+
const update = mockOctokit.issues.update as unknown as ReturnType<typeof vi.fn>;
|
|
94
|
+
listForRepo.mockRejectedValue(new Error('Failed to fetch issues'));
|
|
95
|
+
|
|
96
|
+
await autoCloseDuplicates();
|
|
97
|
+
|
|
98
|
+
expect(listForRepo).toHaveBeenCalledWith({
|
|
99
|
+
owner: process.env.GITHUB_OWNER,
|
|
100
|
+
repo: process.env.GITHUB_REPO,
|
|
101
|
+
state: 'open',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(createComment).not.toHaveBeenCalled();
|
|
105
|
+
expect(update).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle errors when closing issues', async () => {
|
|
109
|
+
const mockOctokit = new Octokit();
|
|
110
|
+
const listForRepo = mockOctokit.issues.listForRepo as unknown as ReturnType<typeof vi.fn>;
|
|
111
|
+
const createComment = mockOctokit.issues.createComment as unknown as ReturnType<typeof vi.fn>;
|
|
112
|
+
const update = mockOctokit.issues.update as unknown as ReturnType<typeof vi.fn>;
|
|
113
|
+
listForRepo.mockResolvedValue({
|
|
114
|
+
data: [
|
|
115
|
+
{ number: 1, title: 'Duplicate Issue' },
|
|
116
|
+
{ number: 2, title: 'Duplicate Issue' },
|
|
117
|
+
{ number: 3, title: 'Unique Issue' },
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
createComment.mockRejectedValue(new Error('Failed to create comment'));
|
|
121
|
+
update.mockRejectedValue(new Error('Failed to update issue'));
|
|
122
|
+
|
|
123
|
+
await autoCloseDuplicates();
|
|
124
|
+
|
|
125
|
+
expect(listForRepo).toHaveBeenCalledWith({
|
|
126
|
+
owner: process.env.GITHUB_OWNER,
|
|
127
|
+
repo: process.env.GITHUB_REPO,
|
|
128
|
+
state: 'open',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(createComment).toHaveBeenCalledWith({
|
|
132
|
+
owner: process.env.GITHUB_OWNER,
|
|
133
|
+
repo: process.env.GITHUB_REPO,
|
|
134
|
+
issue_number: 2,
|
|
135
|
+
body: 'Closing as duplicate of #1',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(update).toHaveBeenCalledWith({
|
|
139
|
+
owner: process.env.GITHUB_OWNER,
|
|
140
|
+
repo: process.env.GITHUB_REPO,
|
|
141
|
+
issue_number: 2,
|
|
142
|
+
state: 'closed',
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
const configPath = path.join(__dirname, '..', '.recoderrc');
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
if (fs.existsSync(configPath)) {
|
|
9
|
+
fs.unlinkSync(configPath);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fs.writeFileSync(configPath, JSON.stringify({}), 'utf8');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (fs.existsSync(configPath)) {
|
|
17
|
+
fs.unlinkSync(configPath);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('CLI Configure Command', () => {
|
|
22
|
+
it('should set the Open Router API key and model in .recoderrc', () => {
|
|
23
|
+
const mockApiKey = 'dummy-key';
|
|
24
|
+
const mockModel = 'gpt-3.5-turbo';
|
|
25
|
+
execSync(`npx recoder-code configure --key ${mockApiKey} --model ${mockModel}`, { encoding: 'utf8' });
|
|
26
|
+
|
|
27
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
28
|
+
expect(config.openRouterApiKey).toBe(mockApiKey);
|
|
29
|
+
expect(config.openRouterModel).toBe(mockModel);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should create .recoderrc if it does not exist', () => {
|
|
33
|
+
const mockApiKey = 'dummy-key';
|
|
34
|
+
const mockModel = 'gpt-3.5-turbo';
|
|
35
|
+
execSync(`npx recoder-code configure --key ${mockApiKey} --model ${mockModel}`, { encoding: 'utf8' });
|
|
36
|
+
|
|
37
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
38
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
39
|
+
expect(config.openRouterApiKey).toBe(mockApiKey);
|
|
40
|
+
expect(config.openRouterModel).toBe(mockModel);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle missing key argument gracefully', () => {
|
|
44
|
+
const mockModel = 'gpt-3.5-turbo';
|
|
45
|
+
try {
|
|
46
|
+
execSync(`npx recoder-code configure --model ${mockModel}`, { encoding: 'utf8' });
|
|
47
|
+
} catch (error: unknown) {
|
|
48
|
+
if (error instanceof Error) {
|
|
49
|
+
expect(error.message).toContain('Please provide an API key.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle missing model argument gracefully', () => {
|
|
55
|
+
const mockApiKey = 'dummy-key';
|
|
56
|
+
try {
|
|
57
|
+
execSync(`npx recoder-code configure --key ${mockApiKey}`, { encoding: 'utf8' });
|
|
58
|
+
} catch (error: unknown) {
|
|
59
|
+
if (error instanceof Error) {
|
|
60
|
+
expect(error.message).toContain('Please provide a model.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should validate the API key format', () => {
|
|
66
|
+
const invalidApiKey = 'invalid-key';
|
|
67
|
+
const mockModel = 'gpt-3.5-turbo';
|
|
68
|
+
try {
|
|
69
|
+
execSync(`npx recoder-code configure --key ${invalidApiKey} --model ${mockModel}`, { encoding: 'utf8' });
|
|
70
|
+
} catch (error: unknown) {
|
|
71
|
+
if (error instanceof Error) {
|
|
72
|
+
expect(error.message).toContain('Invalid Open Router API key format');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should validate the model format', () => {
|
|
78
|
+
const mockApiKey = 'dummy-key';
|
|
79
|
+
const invalidModel = 'invalid-model';
|
|
80
|
+
try {
|
|
81
|
+
execSync(`npx recoder-code configure --key ${mockApiKey} --model ${invalidModel}`, { encoding: 'utf8' });
|
|
82
|
+
} catch (error: unknown) {
|
|
83
|
+
if (error instanceof Error) {
|
|
84
|
+
expect(error.message).toContain('Invalid Open Router model format');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { mocked } from 'jest-mock';
|
|
4
|
+
|
|
5
|
+
jest.mock('axios');
|
|
6
|
+
|
|
7
|
+
describe('CLI List Models Command', () => {
|
|
8
|
+
it('should list available models successfully', () => {
|
|
9
|
+
const mockModels = [
|
|
10
|
+
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' },
|
|
11
|
+
{ id: 'gpt-4', name: 'GPT-4' }
|
|
12
|
+
];
|
|
13
|
+
(axios.get as jest.Mock).mockResolvedValue({ data: mockModels });
|
|
14
|
+
|
|
15
|
+
const result = execSync('npx recoder-code list-models', { encoding: 'utf8' });
|
|
16
|
+
expect(result).toContain('Available models:');
|
|
17
|
+
expect(result).toContain('gpt-3.5-turbo');
|
|
18
|
+
expect(result).toContain('gpt-4');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should handle network errors gracefully', () => {
|
|
22
|
+
(axios.get as jest.Mock).mockRejectedValue(new Error('Network Error'));
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
execSync('npx recoder-code list-models', { encoding: 'utf8' });
|
|
26
|
+
} catch (error: unknown) {
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
expect(error.message).toContain('Error fetching models: Network Error');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle API errors gracefully', () => {
|
|
34
|
+
(axios.get as jest.Mock).mockResolvedValue({ data: { error: 'API Error' } });
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
execSync('npx recoder-code list-models', { encoding: 'utf8' });
|
|
38
|
+
} catch (error: unknown) {
|
|
39
|
+
if (error instanceof Error) {
|
|
40
|
+
expect(error.message).toContain('Error fetching models: API Error');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { mocked } from 'jest-mock';
|
|
6
|
+
|
|
7
|
+
jest.mock('axios');
|
|
8
|
+
|
|
9
|
+
describe('CLI Run Command', () => {
|
|
10
|
+
const configPath = path.join(process.cwd(), '.recoderrc');
|
|
11
|
+
const mockConfig = { openRouterApiKey: 'dummy-key', openRouterModel: 'gpt-3.5-turbo' };
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
if (fs.existsSync(configPath)) {
|
|
15
|
+
fs.renameSync(configPath, `${configPath}.bak`);
|
|
16
|
+
}
|
|
17
|
+
fs.writeFileSync(configPath, JSON.stringify(mockConfig), 'utf8');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.unlinkSync(configPath);
|
|
22
|
+
if (fs.existsSync(`${configPath}.bak`)) {
|
|
23
|
+
fs.renameSync(`${configPath}.bak`, configPath);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should run a prompt successfully', () => {
|
|
28
|
+
const mockResponse = { response: 'Hello, world!' };
|
|
29
|
+
(axios.post as jest.Mock).mockResolvedValue({ data: mockResponse });
|
|
30
|
+
|
|
31
|
+
const result = execSync('npx recoder-code run --prompt "Hello"', { encoding: 'utf8' });
|
|
32
|
+
expect(result).toContain('Hello, world!');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle missing API key gracefully', () => {
|
|
36
|
+
fs.unlinkSync(configPath);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
execSync('npx recoder-code run --prompt "Hello"', { encoding: 'utf8' });
|
|
40
|
+
} catch (error: unknown) {
|
|
41
|
+
if (error instanceof Error) {
|
|
42
|
+
expect(error.message).toContain('Open Router API key not found');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle invalid API keys gracefully', () => {
|
|
48
|
+
const mockConfig = { openRouterApiKey: 'invalid-key', openRouterModel: 'gpt-3.5-turbo' };
|
|
49
|
+
fs.writeFileSync(configPath, JSON.stringify(mockConfig), 'utf8');
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
execSync('npx recoder-code run --prompt "Hello"', { encoding: 'utf8' });
|
|
53
|
+
} catch (error: unknown) {
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
expect(error.message).toContain('Invalid Open Router API key');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle missing model gracefully', () => {
|
|
61
|
+
const mockConfig = { openRouterApiKey: 'dummy-key' };
|
|
62
|
+
fs.writeFileSync(configPath, JSON.stringify(mockConfig), 'utf8');
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
execSync('npx recoder-code run --prompt "Hello"', { encoding: 'utf8' });
|
|
66
|
+
} catch (error: unknown) {
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
expect(error.message).toContain('Open Router model not found');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle invalid models gracefully', () => {
|
|
74
|
+
const mockConfig = { openRouterApiKey: 'dummy-key', openRouterModel: 'invalid-model' };
|
|
75
|
+
fs.writeFileSync(configPath, JSON.stringify(mockConfig), 'utf8');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
execSync('npx recoder-code run --prompt "Hello"', { encoding: 'utf8' });
|
|
79
|
+
} catch (error: unknown) {
|
|
80
|
+
if (error instanceof Error) {
|
|
81
|
+
expect(error.message).toContain('Invalid Open Router model');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle network errors gracefully', () => {
|
|
87
|
+
(axios.post as jest.Mock).mockRejectedValue(new Error('Network Error'));
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
execSync('npx recoder-code run --prompt "Hello"', { encoding: 'utf8' });
|
|
91
|
+
} catch (error: unknown) {
|
|
92
|
+
if (error instanceof Error) {
|
|
93
|
+
expect(error.message).toContain('Error running prompt: Network Error');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
const configPath = path.join(__dirname, '..', '.recoderrc');
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
if (fs.existsSync(configPath)) {
|
|
9
|
+
fs.unlinkSync(configPath);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fs.writeFileSync(configPath, JSON.stringify({}), 'utf8');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (fs.existsSync(configPath)) {
|
|
17
|
+
fs.unlinkSync(configPath);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('CLI Set API Key Command', () => {
|
|
22
|
+
it('should set the Open Router API key in .recoderrc', () => {
|
|
23
|
+
const mockApiKey = 'dummy-key';
|
|
24
|
+
execSync(`npx recoder-code set-api-key --key ${mockApiKey}`, { encoding: 'utf8' });
|
|
25
|
+
|
|
26
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
27
|
+
expect(config.openRouterApiKey).toBe(mockApiKey);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should create .recoderrc if it does not exist', () => {
|
|
31
|
+
const mockApiKey = 'dummy-key';
|
|
32
|
+
execSync(`npx recoder-code set-api-key --key ${mockApiKey}`, { encoding: 'utf8' });
|
|
33
|
+
|
|
34
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
35
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
36
|
+
expect(config.openRouterApiKey).toBe(mockApiKey);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle missing key argument gracefully', () => {
|
|
40
|
+
const result = execSync('npx recoder-code set-api-key', { encoding: 'utf8' });
|
|
41
|
+
expect(result).toContain('Please provide an API key.');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should validate the API key format', () => {
|
|
45
|
+
const invalidApiKey = 'invalid-key';
|
|
46
|
+
try {
|
|
47
|
+
execSync(`npx recoder-code set-api-key --key ${invalidApiKey}`, { encoding: 'utf8' });
|
|
48
|
+
} catch (error: unknown) {
|
|
49
|
+
if (error instanceof Error) {
|
|
50
|
+
expect(error.message).toContain('Invalid Open Router API key format');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
const configPath = path.join(__dirname, '..', '.recoderrc');
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
if (fs.existsSync(configPath)) {
|
|
9
|
+
fs.unlinkSync(configPath);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fs.writeFileSync(configPath, JSON.stringify({}), 'utf8');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (fs.existsSync(configPath)) {
|
|
17
|
+
fs.unlinkSync(configPath);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('CLI Validate API Key Command', () => {
|
|
22
|
+
it('should validate a valid API key', () => {
|
|
23
|
+
const mockApiKey = 'valid-key';
|
|
24
|
+
fs.writeFileSync(configPath, JSON.stringify({ openRouterApiKey: mockApiKey }), 'utf8');
|
|
25
|
+
|
|
26
|
+
const result = execSync('npx recoder-code validate-api-key', { encoding: 'utf8' });
|
|
27
|
+
expect(result).toContain('API key is valid.');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle missing API key gracefully', () => {
|
|
31
|
+
try {
|
|
32
|
+
execSync('npx recoder-code validate-api-key', { encoding: 'utf8' });
|
|
33
|
+
} catch (error: unknown) {
|
|
34
|
+
if (error instanceof Error) {
|
|
35
|
+
expect(error.message).toContain('Open Router API key not found');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle invalid API keys gracefully', () => {
|
|
41
|
+
const mockApiKey = 'invalid-key';
|
|
42
|
+
fs.writeFileSync(configPath, JSON.stringify({ openRouterApiKey: mockApiKey }), 'utf8');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
execSync('npx recoder-code validate-api-key', { encoding: 'utf8' });
|
|
46
|
+
} catch (error: unknown) {
|
|
47
|
+
if (error instanceof Error) {
|
|
48
|
+
expect(error.message).toContain('Invalid Open Router API key');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES6",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"strict": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"baseUrl": ".",
|
|
11
|
+
"paths": {
|
|
12
|
+
"@/*": ["*"]
|
|
13
|
+
},
|
|
14
|
+
"rootDir": "./src"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules"]
|
|
18
|
+
}
|