kitstore-cli 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.
- package/.env.test +4 -0
- package/.eslintrc.js +29 -0
- package/dist/__tests__/commands/init.test.js +76 -0
- package/dist/__tests__/commands/install.test.js +422 -0
- package/dist/__tests__/commands/list.test.js +173 -0
- package/dist/__tests__/commands/login.test.js +281 -0
- package/dist/__tests__/commands/rule-check.test.js +72 -0
- package/dist/__tests__/commands/search.test.js +175 -0
- package/dist/__tests__/commands/upload.test.js +367 -0
- package/dist/__tests__/config.test.js +179 -0
- package/dist/__tests__/setup.js +8 -0
- package/dist/api/client.js +18 -0
- package/dist/api/generated/api.js +912 -0
- package/dist/api/generated/base.js +48 -0
- package/dist/api/generated/common.js +108 -0
- package/dist/api/generated/configuration.js +48 -0
- package/dist/api/generated/index.js +31 -0
- package/dist/commands/init.js +79 -0
- package/dist/commands/install.js +150 -0
- package/dist/commands/list.js +70 -0
- package/dist/commands/login.js +64 -0
- package/dist/commands/rule-check.js +81 -0
- package/dist/commands/search.js +59 -0
- package/dist/commands/upload.js +138 -0
- package/dist/config.js +84 -0
- package/dist/index.js +71 -0
- package/e2e/install.e2e.test.ts +237 -0
- package/e2e/integration.e2e.test.ts +346 -0
- package/e2e/login.e2e.test.ts +188 -0
- package/jest.config.js +24 -0
- package/openapitools.json +7 -0
- package/package.json +41 -0
- package/src/__tests__/commands/init.test.ts +52 -0
- package/src/__tests__/commands/install.test.ts +449 -0
- package/src/__tests__/commands/list.test.ts +164 -0
- package/src/__tests__/commands/login.test.ts +293 -0
- package/src/__tests__/commands/rule-check.test.ts +52 -0
- package/src/__tests__/commands/search.test.ts +168 -0
- package/src/__tests__/commands/upload.test.ts +404 -0
- package/src/__tests__/config.test.ts +181 -0
- package/src/__tests__/setup.ts +11 -0
- package/src/api/client.ts +20 -0
- package/src/api/generated/.openapi-generator/FILES +17 -0
- package/src/api/generated/.openapi-generator/VERSION +1 -0
- package/src/api/generated/.openapi-generator-ignore +23 -0
- package/src/api/generated/api.ts +1171 -0
- package/src/api/generated/base.ts +62 -0
- package/src/api/generated/common.ts +113 -0
- package/src/api/generated/configuration.ts +121 -0
- package/src/api/generated/docs/AuthApi.md +158 -0
- package/src/api/generated/docs/AuthResponseDto.md +22 -0
- package/src/api/generated/docs/AuthUserDto.md +24 -0
- package/src/api/generated/docs/HealthApi.md +183 -0
- package/src/api/generated/docs/LoginDto.md +22 -0
- package/src/api/generated/docs/RegisterDto.md +24 -0
- package/src/api/generated/docs/RuleAuthorDto.md +22 -0
- package/src/api/generated/docs/RuleResponseDto.md +36 -0
- package/src/api/generated/docs/RulesApi.md +289 -0
- package/src/api/generated/git_push.sh +57 -0
- package/src/api/generated/index.ts +18 -0
- package/src/commands/init.ts +46 -0
- package/src/commands/install.ts +129 -0
- package/src/commands/list.ts +71 -0
- package/src/commands/login.ts +65 -0
- package/src/commands/rule-check.ts +49 -0
- package/src/commands/search.ts +66 -0
- package/src/commands/upload.ts +117 -0
- package/src/config.ts +66 -0
- package/src/index.ts +79 -0
- package/test-cli-config.js +118 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { uploadCommand } from '../../commands/upload';
|
|
2
|
+
import * as config from '../../config';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import * as fs from 'fs-extra';
|
|
5
|
+
import FormData from 'form-data';
|
|
6
|
+
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
jest.mock('../../config', () => ({
|
|
9
|
+
getConfig: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock('axios');
|
|
13
|
+
jest.mock('fs-extra', () => ({
|
|
14
|
+
pathExists: jest.fn(),
|
|
15
|
+
stat: jest.fn(),
|
|
16
|
+
createReadStream: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
jest.mock('form-data', () => {
|
|
19
|
+
return jest.fn().mockImplementation(function() {
|
|
20
|
+
const data = new Map();
|
|
21
|
+
return {
|
|
22
|
+
append: jest.fn((key, value) => {
|
|
23
|
+
data.set(key, value);
|
|
24
|
+
}),
|
|
25
|
+
get: jest.fn((key) => {
|
|
26
|
+
return data.get(key);
|
|
27
|
+
}),
|
|
28
|
+
getHeaders: jest.fn(() => ({
|
|
29
|
+
'Content-Type': 'multipart/form-data',
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// TC-UNIT-CLI-004 to TC-UNIT-CLI-005
|
|
36
|
+
describe('Upload Command', () => {
|
|
37
|
+
const mockGetConfig = config.getConfig as jest.MockedFunction<typeof config.getConfig>;
|
|
38
|
+
const mockAxiosPost = axios.post as jest.MockedFunction<typeof axios.post>;
|
|
39
|
+
const mockFsPathExists = fs.pathExists as jest.MockedFunction<any>;
|
|
40
|
+
const mockFsStat = (fs.stat as jest.MockedFunction<any>);
|
|
41
|
+
const mockFsCreateReadStream = fs.createReadStream as jest.MockedFunction<any>;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
jest.clearAllMocks();
|
|
45
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
46
|
+
jest.spyOn(console, 'error').mockImplementation();
|
|
47
|
+
jest.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined) => {
|
|
48
|
+
throw new Error(`Process exit with code ${code}`);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
jest.restoreAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// TC-UNIT-CLI-004: Upload command (success)
|
|
57
|
+
describe('upload success', () => {
|
|
58
|
+
it('should upload file successfully', async () => {
|
|
59
|
+
mockGetConfig.mockResolvedValue({
|
|
60
|
+
server: 'http://localhost:3000',
|
|
61
|
+
token: 'test-token',
|
|
62
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
63
|
+
lastLogin: new Date().toISOString(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
67
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
68
|
+
mockFsCreateReadStream.mockReturnValue({} as any);
|
|
69
|
+
|
|
70
|
+
const mockFormData = {
|
|
71
|
+
append: jest.fn(),
|
|
72
|
+
getHeaders: jest.fn().mockReturnValue({ 'content-type': 'multipart/form-data' }),
|
|
73
|
+
};
|
|
74
|
+
(FormData as any).mockImplementation(() => mockFormData);
|
|
75
|
+
|
|
76
|
+
mockAxiosPost.mockResolvedValue({
|
|
77
|
+
data: {
|
|
78
|
+
id: 'rule-id',
|
|
79
|
+
name: 'test.md',
|
|
80
|
+
tag: 'test',
|
|
81
|
+
type: 'rule',
|
|
82
|
+
version: '1.0.0',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await uploadCommand('./test.md', {
|
|
87
|
+
name: 'Test Rule',
|
|
88
|
+
tag: 'test',
|
|
89
|
+
type: 'rule',
|
|
90
|
+
description: 'Test description',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(mockFsPathExists).toHaveBeenCalledWith('./test.md');
|
|
94
|
+
expect(mockAxiosPost).toHaveBeenCalledWith(
|
|
95
|
+
'http://localhost:3000/api/rules',
|
|
96
|
+
expect.any(Object),
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
headers: expect.objectContaining({
|
|
99
|
+
Authorization: 'Bearer test-token',
|
|
100
|
+
}),
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// TC-UNIT-CLI-005: Upload command (file not found)
|
|
107
|
+
describe('upload failure', () => {
|
|
108
|
+
it('should handle file not found error', async () => {
|
|
109
|
+
mockGetConfig.mockResolvedValue({
|
|
110
|
+
server: 'http://localhost:3000',
|
|
111
|
+
token: 'test-token',
|
|
112
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
113
|
+
lastLogin: new Date().toISOString(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
mockFsPathExists.mockResolvedValue(false);
|
|
117
|
+
|
|
118
|
+
await expect(
|
|
119
|
+
uploadCommand('./nonexistent.md', {
|
|
120
|
+
name: 'Test Rule',
|
|
121
|
+
tag: 'test',
|
|
122
|
+
type: 'rule',
|
|
123
|
+
}),
|
|
124
|
+
).rejects.toThrow();
|
|
125
|
+
|
|
126
|
+
expect(mockAxiosPost).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// TC-UNIT-CLI-033: Upload with file size validation
|
|
130
|
+
it('should validate file size limits', async () => {
|
|
131
|
+
mockGetConfig.mockResolvedValue({
|
|
132
|
+
server: 'http://localhost:3000',
|
|
133
|
+
token: 'test-token',
|
|
134
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
135
|
+
lastLogin: new Date().toISOString(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Mock a large file (20MB)
|
|
139
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
140
|
+
mockFsStat.mockResolvedValue({ size: 20 * 1024 * 1024 }); // 20MB
|
|
141
|
+
|
|
142
|
+
await expect(
|
|
143
|
+
uploadCommand('./large.md', {
|
|
144
|
+
name: 'Large Rule',
|
|
145
|
+
tag: 'test',
|
|
146
|
+
type: 'rule',
|
|
147
|
+
}),
|
|
148
|
+
).rejects.toThrow('Process exit with code 1');
|
|
149
|
+
|
|
150
|
+
expect(console.error).toHaveBeenCalledWith('❌ File size exceeds limit (20.00 MB > 10 MB)');
|
|
151
|
+
expect(mockAxiosPost).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// TC-UNIT-CLI-034: Upload with file type validation
|
|
155
|
+
it('should validate file type', async () => {
|
|
156
|
+
mockGetConfig.mockResolvedValue({
|
|
157
|
+
server: 'http://localhost:3000',
|
|
158
|
+
token: 'test-token',
|
|
159
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
160
|
+
lastLogin: new Date().toISOString(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
164
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
165
|
+
|
|
166
|
+
await expect(
|
|
167
|
+
uploadCommand('./invalid.exe', {
|
|
168
|
+
name: 'Invalid Rule',
|
|
169
|
+
tag: 'test',
|
|
170
|
+
type: 'rule',
|
|
171
|
+
}),
|
|
172
|
+
).rejects.toThrow('Process exit with code 1');
|
|
173
|
+
|
|
174
|
+
expect(console.error).toHaveBeenCalledWith('❌ Invalid file type. Allowed types: .md, .txt, .json, .cursorrules');
|
|
175
|
+
expect(mockAxiosPost).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// TC-UNIT-CLI-035: Upload with API failure
|
|
179
|
+
it.skip('should handle API upload failure', async () => {
|
|
180
|
+
mockGetConfig.mockResolvedValue({
|
|
181
|
+
server: 'http://localhost:3000',
|
|
182
|
+
token: 'test-token',
|
|
183
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
184
|
+
lastLogin: new Date().toISOString(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
188
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
189
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 } as any);
|
|
190
|
+
mockAxiosPost.mockRejectedValue({
|
|
191
|
+
response: {
|
|
192
|
+
status: 413,
|
|
193
|
+
data: { error: 'File too large' },
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await expect(
|
|
198
|
+
uploadCommand('./test.md', {
|
|
199
|
+
name: 'Test Rule',
|
|
200
|
+
tag: 'test',
|
|
201
|
+
type: 'rule',
|
|
202
|
+
}),
|
|
203
|
+
).rejects.toThrow('Process exit with code 1');
|
|
204
|
+
|
|
205
|
+
expect(console.error).toHaveBeenCalledWith('❌ Upload failed: File too large');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// TC-UNIT-CLI-036: Upload with authentication error
|
|
209
|
+
it.skip('should handle authentication errors', async () => {
|
|
210
|
+
mockGetConfig.mockResolvedValue({
|
|
211
|
+
server: 'http://localhost:3000',
|
|
212
|
+
token: 'invalid-token',
|
|
213
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
214
|
+
lastLogin: new Date().toISOString(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
218
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
219
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 } as any);
|
|
220
|
+
mockAxiosPost.mockRejectedValue({
|
|
221
|
+
response: {
|
|
222
|
+
status: 401,
|
|
223
|
+
data: { error: 'Invalid token' },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await expect(
|
|
228
|
+
uploadCommand('./test.md', {
|
|
229
|
+
name: 'Test Rule',
|
|
230
|
+
tag: 'test',
|
|
231
|
+
type: 'rule',
|
|
232
|
+
}),
|
|
233
|
+
).rejects.toThrow('Process exit with code 1');
|
|
234
|
+
|
|
235
|
+
expect(console.error).toHaveBeenCalledWith('❌ Upload failed: Invalid token');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// TC-UNIT-CLI-037: Upload with network error
|
|
239
|
+
it.skip('should handle network errors', async () => {
|
|
240
|
+
mockGetConfig.mockResolvedValue({
|
|
241
|
+
server: 'http://localhost:3000',
|
|
242
|
+
token: 'test-token',
|
|
243
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
244
|
+
lastLogin: new Date().toISOString(),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
248
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
249
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 } as any);
|
|
250
|
+
mockAxiosPost.mockRejectedValue(new Error('Network connection failed'));
|
|
251
|
+
|
|
252
|
+
await expect(
|
|
253
|
+
uploadCommand('./test.md', {
|
|
254
|
+
name: 'Test Rule',
|
|
255
|
+
tag: 'test',
|
|
256
|
+
type: 'rule',
|
|
257
|
+
}),
|
|
258
|
+
).rejects.toThrow('Process exit with code 1');
|
|
259
|
+
|
|
260
|
+
expect(console.error).toHaveBeenCalledWith('❌ Upload failed:', 'Network connection failed');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// TC-UNIT-CLI-038: Upload with metadata extraction
|
|
264
|
+
it.skip('should extract and send metadata correctly', async () => {
|
|
265
|
+
mockGetConfig.mockResolvedValue({
|
|
266
|
+
server: 'http://localhost:3000',
|
|
267
|
+
token: 'test-token',
|
|
268
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
269
|
+
lastLogin: new Date().toISOString(),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
273
|
+
mockFsStat.mockResolvedValue({ size: 2048 }); // Medium file
|
|
274
|
+
mockFsCreateReadStream.mockReturnValue({ size: 2048 } as any);
|
|
275
|
+
mockAxiosPost.mockResolvedValue({
|
|
276
|
+
data: {
|
|
277
|
+
id: 'uploaded-rule-id',
|
|
278
|
+
name: 'Test Rule',
|
|
279
|
+
tag: 'test',
|
|
280
|
+
type: 'rule',
|
|
281
|
+
version: '1.0.0',
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await uploadCommand('./test.md', {
|
|
286
|
+
name: 'Test Rule',
|
|
287
|
+
tag: 'test',
|
|
288
|
+
type: 'rule',
|
|
289
|
+
description: 'A comprehensive test rule',
|
|
290
|
+
version: '2.0.0',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(mockAxiosPost).toHaveBeenCalledWith(
|
|
294
|
+
'http://localhost:3000/api/rules',
|
|
295
|
+
expect.any(Object),
|
|
296
|
+
expect.objectContaining({
|
|
297
|
+
headers: expect.objectContaining({
|
|
298
|
+
Authorization: 'Bearer test-token',
|
|
299
|
+
}),
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// Verify the FormData contains correct metadata
|
|
304
|
+
const formDataCall = mockAxiosPost.mock.calls[0][1] as any;
|
|
305
|
+
expect(formDataCall.get('name')).toBe('Test Rule');
|
|
306
|
+
expect(formDataCall.get('tag')).toBe('test');
|
|
307
|
+
expect(formDataCall.get('type')).toBe('rule');
|
|
308
|
+
expect(formDataCall.get('description')).toBe('A comprehensive test rule');
|
|
309
|
+
expect(formDataCall.get('version')).toBe('2.0.0');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// TC-UNIT-CLI-039: Upload with default version
|
|
313
|
+
it.skip('should use default version when not specified', async () => {
|
|
314
|
+
mockGetConfig.mockResolvedValue({
|
|
315
|
+
server: 'http://localhost:3000',
|
|
316
|
+
token: 'test-token',
|
|
317
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
318
|
+
lastLogin: new Date().toISOString(),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
322
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
323
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 } as any);
|
|
324
|
+
mockAxiosPost.mockResolvedValue({
|
|
325
|
+
data: {
|
|
326
|
+
id: 'uploaded-rule-id',
|
|
327
|
+
name: 'Test Rule',
|
|
328
|
+
version: '1.0.0',
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
await uploadCommand('./test.md', {
|
|
333
|
+
name: 'Test Rule',
|
|
334
|
+
tag: 'test',
|
|
335
|
+
type: 'rule',
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const formDataCall = mockAxiosPost.mock.calls[0][1] as any;
|
|
339
|
+
expect(formDataCall.get('version')).toBe('1.0.0'); // Default version
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// TC-UNIT-CLI-040: Upload with missing required parameters
|
|
343
|
+
it.skip('should validate required parameters', async () => {
|
|
344
|
+
mockGetConfig.mockResolvedValue({
|
|
345
|
+
server: 'http://localhost:3000',
|
|
346
|
+
token: 'test-token',
|
|
347
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
348
|
+
lastLogin: new Date().toISOString(),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
352
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
353
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 } as any);
|
|
354
|
+
|
|
355
|
+
// Missing tag
|
|
356
|
+
await expect(
|
|
357
|
+
uploadCommand('./test.md', {
|
|
358
|
+
name: 'Test Rule',
|
|
359
|
+
type: 'rule',
|
|
360
|
+
}),
|
|
361
|
+
).rejects.toThrow('Process exit with code 1');
|
|
362
|
+
|
|
363
|
+
expect(console.error).toHaveBeenCalledWith('❌ Missing required parameters: tag, type');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// TC-UNIT-CLI-041: Upload command success with all parameters
|
|
367
|
+
it.skip('should handle successful upload with all parameters', async () => {
|
|
368
|
+
mockGetConfig.mockResolvedValue({
|
|
369
|
+
server: 'http://localhost:3000',
|
|
370
|
+
token: 'test-token',
|
|
371
|
+
user: { id: 'user-id', username: 'testuser' },
|
|
372
|
+
lastLogin: new Date().toISOString(),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
mockFsPathExists.mockResolvedValue(true);
|
|
376
|
+
mockFsStat.mockResolvedValue({ size: 1024 }); // Small file
|
|
377
|
+
mockFsCreateReadStream.mockReturnValue({ size: 1024 } as any);
|
|
378
|
+
mockAxiosPost.mockResolvedValue({
|
|
379
|
+
data: {
|
|
380
|
+
id: 'uploaded-rule-id',
|
|
381
|
+
name: 'Comprehensive Rule',
|
|
382
|
+
tag: 'comprehensive',
|
|
383
|
+
type: 'rule',
|
|
384
|
+
version: '3.0.0',
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
await uploadCommand('./comprehensive.md', {
|
|
389
|
+
name: 'Comprehensive Rule',
|
|
390
|
+
tag: 'comprehensive',
|
|
391
|
+
type: 'rule',
|
|
392
|
+
description: 'A comprehensive test rule',
|
|
393
|
+
version: '3.0.0',
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(console.log).toHaveBeenCalledWith('✅ Successfully uploaded rule: Comprehensive Rule');
|
|
397
|
+
expect(console.log).toHaveBeenCalledWith('📋 Rule ID: uploaded-rule-id');
|
|
398
|
+
expect(console.log).toHaveBeenCalledWith('🏷️ Tag: comprehensive');
|
|
399
|
+
expect(console.log).toHaveBeenCalledWith('📄 Type: rule');
|
|
400
|
+
expect(console.log).toHaveBeenCalledWith('🔢 Version: 3.0.0');
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { getConfig, saveConfig, getInstallPaths, Config } from '../config';
|
|
3
|
+
|
|
4
|
+
// Mock dependencies
|
|
5
|
+
jest.mock('fs-extra');
|
|
6
|
+
jest.mock('os', () => ({
|
|
7
|
+
homedir: jest.fn().mockReturnValue('C:\\Users\\trieu'),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs-extra';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
|
|
14
|
+
describe('Config', () => {
|
|
15
|
+
const mockHomeDir = 'C:\\Users\\trieu';
|
|
16
|
+
const mockConfigDir = path.join(mockHomeDir, '.kitstore');
|
|
17
|
+
const mockConfigFile = path.join(mockConfigDir, 'config.json');
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
|
|
22
|
+
// Setup default mocks
|
|
23
|
+
// @ts-ignore - Jest mock types are complex
|
|
24
|
+
os.homedir.mockReturnValue(mockHomeDir);
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
fs.ensureDir.mockResolvedValue(undefined);
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
fs.pathExists.mockResolvedValue(false);
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
fs.writeJson.mockResolvedValue(undefined);
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
fs.existsSync.mockReturnValue(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('getConfig', () => {
|
|
36
|
+
it('should return default config when config file does not exist', async () => {
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
|
39
|
+
|
|
40
|
+
const result = await getConfig();
|
|
41
|
+
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
server: 'http://localhost:3000'
|
|
44
|
+
});
|
|
45
|
+
expect(fs.ensureDir).toHaveBeenCalledWith(mockConfigDir);
|
|
46
|
+
expect(fs.pathExists).toHaveBeenCalledWith(mockConfigFile);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should load and merge config from file', async () => {
|
|
50
|
+
const fileConfig = {
|
|
51
|
+
token: 'test-token',
|
|
52
|
+
server: 'https://api.example.com',
|
|
53
|
+
user: { id: '123', username: 'testuser' }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
fs.pathExists.mockResolvedValue(true);
|
|
58
|
+
// @ts-ignore
|
|
59
|
+
fs.readJson.mockResolvedValue(fileConfig);
|
|
60
|
+
|
|
61
|
+
const result = await getConfig();
|
|
62
|
+
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
server: 'https://api.example.com', // From file
|
|
65
|
+
token: 'test-token',
|
|
66
|
+
user: { id: '123', username: 'testuser' }
|
|
67
|
+
});
|
|
68
|
+
expect(fs.readJson).toHaveBeenCalledWith(mockConfigFile);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle file read errors gracefully', async () => {
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
fs.pathExists.mockResolvedValue(true);
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
fs.readJson.mockRejectedValue(new Error('Read failed'));
|
|
76
|
+
|
|
77
|
+
const result = await getConfig();
|
|
78
|
+
|
|
79
|
+
expect(result).toEqual({
|
|
80
|
+
server: 'http://localhost:3000'
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle ensureDir errors gracefully', async () => {
|
|
85
|
+
// @ts-ignore
|
|
86
|
+
fs.ensureDir.mockRejectedValue(new Error('Permission denied'));
|
|
87
|
+
|
|
88
|
+
const result = await getConfig();
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual({
|
|
91
|
+
server: 'http://localhost:3000'
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('saveConfig', () => {
|
|
97
|
+
it('should save config to file', async () => {
|
|
98
|
+
const config: Config = {
|
|
99
|
+
token: 'new-token',
|
|
100
|
+
server: 'https://new-server.com',
|
|
101
|
+
user: { id: '456', username: 'newuser' }
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await saveConfig(config);
|
|
105
|
+
|
|
106
|
+
expect(fs.ensureDir).toHaveBeenCalledWith(mockConfigDir);
|
|
107
|
+
expect(fs.writeJson).toHaveBeenCalledWith(
|
|
108
|
+
mockConfigFile,
|
|
109
|
+
config,
|
|
110
|
+
{ spaces: 2 }
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should handle write errors', async () => {
|
|
115
|
+
const config: Config = { server: 'http://localhost:3000' };
|
|
116
|
+
// @ts-ignore
|
|
117
|
+
(fs.writeJson as jest.Mock).mockRejectedValue(new Error('Write failed'));
|
|
118
|
+
|
|
119
|
+
await expect(saveConfig(config)).rejects.toThrow('Write failed');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('getInstallPaths', () => {
|
|
124
|
+
it('should return paths in current directory .cursor folder when it exists', () => {
|
|
125
|
+
// @ts-ignore
|
|
126
|
+
(fs.existsSync as jest.Mock).mockReturnValue(true);
|
|
127
|
+
|
|
128
|
+
const result = getInstallPaths();
|
|
129
|
+
|
|
130
|
+
const cwd = process.cwd();
|
|
131
|
+
expect(result).toEqual({
|
|
132
|
+
rules: path.join(cwd, '.cursor', 'rules'),
|
|
133
|
+
commands: path.join(cwd, '.cursor', 'commands')
|
|
134
|
+
});
|
|
135
|
+
expect(fs.existsSync).toHaveBeenCalledWith(path.join(cwd, '.cursor'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return home directory paths when .cursor does not exist in cwd', () => {
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
(fs.existsSync as jest.Mock).mockReturnValue(false);
|
|
141
|
+
|
|
142
|
+
const result = getInstallPaths();
|
|
143
|
+
|
|
144
|
+
expect(result).toEqual({
|
|
145
|
+
rules: path.join(mockHomeDir, '.cursor', 'rules'),
|
|
146
|
+
commands: path.join(mockHomeDir, '.cursor', 'commands')
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Config interface', () => {
|
|
152
|
+
it('should support all config properties', () => {
|
|
153
|
+
const config: Config = {
|
|
154
|
+
token: 'jwt-token',
|
|
155
|
+
server: 'https://api.test.com',
|
|
156
|
+
user: {
|
|
157
|
+
id: 'user-123',
|
|
158
|
+
username: 'testuser',
|
|
159
|
+
email: 'test@example.com'
|
|
160
|
+
},
|
|
161
|
+
lastLogin: '2024-01-01T00:00:00Z'
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
expect(config.token).toBe('jwt-token');
|
|
165
|
+
expect(config.server).toBe('https://api.test.com');
|
|
166
|
+
expect(config.user?.username).toBe('testuser');
|
|
167
|
+
expect(config.lastLogin).toBe('2024-01-01T00:00:00Z');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should allow optional properties', () => {
|
|
171
|
+
const minimalConfig: Config = {
|
|
172
|
+
server: 'http://localhost:3000'
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
expect(minimalConfig.server).toBe('http://localhost:3000');
|
|
176
|
+
expect(minimalConfig.token).toBeUndefined();
|
|
177
|
+
expect(minimalConfig.user).toBeUndefined();
|
|
178
|
+
expect(minimalConfig.lastLogin).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Configuration, AuthApi, RulesApi } from './generated';
|
|
2
|
+
import { getConfig } from '../config';
|
|
3
|
+
|
|
4
|
+
export async function createApi(options?: { server?: string; token?: string }) {
|
|
5
|
+
const cfg = await getConfig();
|
|
6
|
+
const basePath = options?.server || cfg.server || 'http://localhost:3000';
|
|
7
|
+
const token = options?.token || cfg.token;
|
|
8
|
+
|
|
9
|
+
const configuration = new Configuration({
|
|
10
|
+
basePath,
|
|
11
|
+
accessToken: token,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
authApi: new AuthApi(configuration),
|
|
16
|
+
rulesApi: new RulesApi(configuration),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
.npmignore
|
|
3
|
+
api.ts
|
|
4
|
+
base.ts
|
|
5
|
+
common.ts
|
|
6
|
+
configuration.ts
|
|
7
|
+
docs/AuthApi.md
|
|
8
|
+
docs/AuthResponseDto.md
|
|
9
|
+
docs/AuthUserDto.md
|
|
10
|
+
docs/HealthApi.md
|
|
11
|
+
docs/LoginDto.md
|
|
12
|
+
docs/RegisterDto.md
|
|
13
|
+
docs/RuleAuthorDto.md
|
|
14
|
+
docs/RuleResponseDto.md
|
|
15
|
+
docs/RulesApi.md
|
|
16
|
+
git_push.sh
|
|
17
|
+
index.ts
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
7.17.0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# OpenAPI Generator Ignore
|
|
2
|
+
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
|
3
|
+
|
|
4
|
+
# Use this file to prevent files from being overwritten by the generator.
|
|
5
|
+
# The patterns follow closely to .gitignore or .dockerignore.
|
|
6
|
+
|
|
7
|
+
# As an example, the C# client generator defines ApiClient.cs.
|
|
8
|
+
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
|
9
|
+
#ApiClient.cs
|
|
10
|
+
|
|
11
|
+
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
|
12
|
+
#foo/*/qux
|
|
13
|
+
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
|
14
|
+
|
|
15
|
+
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
|
16
|
+
#foo/**/qux
|
|
17
|
+
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
|
18
|
+
|
|
19
|
+
# You can also negate patterns with an exclamation (!).
|
|
20
|
+
# For example, you can ignore all files in a docs folder with the file extension .md:
|
|
21
|
+
#docs/*.md
|
|
22
|
+
# Then explicitly reverse the ignore rule for a single file:
|
|
23
|
+
#!docs/README.md
|