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.
Files changed (71) hide show
  1. package/.env.test +4 -0
  2. package/.eslintrc.js +29 -0
  3. package/dist/__tests__/commands/init.test.js +76 -0
  4. package/dist/__tests__/commands/install.test.js +422 -0
  5. package/dist/__tests__/commands/list.test.js +173 -0
  6. package/dist/__tests__/commands/login.test.js +281 -0
  7. package/dist/__tests__/commands/rule-check.test.js +72 -0
  8. package/dist/__tests__/commands/search.test.js +175 -0
  9. package/dist/__tests__/commands/upload.test.js +367 -0
  10. package/dist/__tests__/config.test.js +179 -0
  11. package/dist/__tests__/setup.js +8 -0
  12. package/dist/api/client.js +18 -0
  13. package/dist/api/generated/api.js +912 -0
  14. package/dist/api/generated/base.js +48 -0
  15. package/dist/api/generated/common.js +108 -0
  16. package/dist/api/generated/configuration.js +48 -0
  17. package/dist/api/generated/index.js +31 -0
  18. package/dist/commands/init.js +79 -0
  19. package/dist/commands/install.js +150 -0
  20. package/dist/commands/list.js +70 -0
  21. package/dist/commands/login.js +64 -0
  22. package/dist/commands/rule-check.js +81 -0
  23. package/dist/commands/search.js +59 -0
  24. package/dist/commands/upload.js +138 -0
  25. package/dist/config.js +84 -0
  26. package/dist/index.js +71 -0
  27. package/e2e/install.e2e.test.ts +237 -0
  28. package/e2e/integration.e2e.test.ts +346 -0
  29. package/e2e/login.e2e.test.ts +188 -0
  30. package/jest.config.js +24 -0
  31. package/openapitools.json +7 -0
  32. package/package.json +41 -0
  33. package/src/__tests__/commands/init.test.ts +52 -0
  34. package/src/__tests__/commands/install.test.ts +449 -0
  35. package/src/__tests__/commands/list.test.ts +164 -0
  36. package/src/__tests__/commands/login.test.ts +293 -0
  37. package/src/__tests__/commands/rule-check.test.ts +52 -0
  38. package/src/__tests__/commands/search.test.ts +168 -0
  39. package/src/__tests__/commands/upload.test.ts +404 -0
  40. package/src/__tests__/config.test.ts +181 -0
  41. package/src/__tests__/setup.ts +11 -0
  42. package/src/api/client.ts +20 -0
  43. package/src/api/generated/.openapi-generator/FILES +17 -0
  44. package/src/api/generated/.openapi-generator/VERSION +1 -0
  45. package/src/api/generated/.openapi-generator-ignore +23 -0
  46. package/src/api/generated/api.ts +1171 -0
  47. package/src/api/generated/base.ts +62 -0
  48. package/src/api/generated/common.ts +113 -0
  49. package/src/api/generated/configuration.ts +121 -0
  50. package/src/api/generated/docs/AuthApi.md +158 -0
  51. package/src/api/generated/docs/AuthResponseDto.md +22 -0
  52. package/src/api/generated/docs/AuthUserDto.md +24 -0
  53. package/src/api/generated/docs/HealthApi.md +183 -0
  54. package/src/api/generated/docs/LoginDto.md +22 -0
  55. package/src/api/generated/docs/RegisterDto.md +24 -0
  56. package/src/api/generated/docs/RuleAuthorDto.md +22 -0
  57. package/src/api/generated/docs/RuleResponseDto.md +36 -0
  58. package/src/api/generated/docs/RulesApi.md +289 -0
  59. package/src/api/generated/git_push.sh +57 -0
  60. package/src/api/generated/index.ts +18 -0
  61. package/src/commands/init.ts +46 -0
  62. package/src/commands/install.ts +129 -0
  63. package/src/commands/list.ts +71 -0
  64. package/src/commands/login.ts +65 -0
  65. package/src/commands/rule-check.ts +49 -0
  66. package/src/commands/search.ts +66 -0
  67. package/src/commands/upload.ts +117 -0
  68. package/src/config.ts +66 -0
  69. package/src/index.ts +79 -0
  70. package/test-cli-config.js +118 -0
  71. 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,11 @@
1
+ // CLI Test Setup
2
+ // Load test environment variables
3
+
4
+ if (process.env.NODE_ENV === 'test') {
5
+ // Set test environment variables for CLI
6
+ process.env.API_BASE_URL = 'http://localhost:3001/api';
7
+ process.env.CLI_ENV = 'test';
8
+ }
9
+
10
+
11
+
@@ -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