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,449 @@
1
+ import { installCommand } from '../../commands/install';
2
+ import * as config from '../../config';
3
+ import * as apiClient from '../../api/client';
4
+ import axios from 'axios';
5
+ import * as fs from 'fs-extra';
6
+ import * as path from 'path';
7
+ import inquirer from 'inquirer';
8
+
9
+ // Mock dependencies
10
+ jest.mock('../../config', () => ({
11
+ getConfig: jest.fn(),
12
+ getInstallPaths: jest.fn(),
13
+ }));
14
+
15
+ jest.mock('../../api/client', () => ({
16
+ createApi: jest.fn(),
17
+ }));
18
+
19
+ jest.mock('axios');
20
+ jest.mock('fs-extra', () => ({
21
+ pathExists: jest.fn(),
22
+ ensureDir: jest.fn(),
23
+ writeFile: jest.fn(),
24
+ createReadStream: jest.fn(),
25
+ }));
26
+ jest.mock('inquirer', () => ({
27
+ prompt: jest.fn(),
28
+ }));
29
+
30
+ // TC-UNIT-CLI-003: Install command (success)
31
+ describe('Install Command', () => {
32
+ const mockGetConfig = config.getConfig as jest.MockedFunction<typeof config.getConfig>;
33
+ const mockGetInstallPaths = config.getInstallPaths as jest.MockedFunction<typeof config.getInstallPaths>;
34
+ const mockCreateApi = apiClient.createApi as jest.MockedFunction<typeof apiClient.createApi>;
35
+ const mockAxiosGet = axios.get as jest.MockedFunction<typeof axios.get>;
36
+ const mockFsEnsureDir = fs.ensureDir as jest.MockedFunction<any>;
37
+ const mockFsPathExists = fs.pathExists as jest.MockedFunction<any>;
38
+ const mockFsWriteFile = fs.writeFile as jest.MockedFunction<any>;
39
+
40
+ beforeEach(() => {
41
+ jest.clearAllMocks();
42
+ jest.spyOn(console, 'log').mockImplementation();
43
+ jest.spyOn(console, 'error').mockImplementation();
44
+ // Re-establish process.exit mock for each test
45
+ jest.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined) => {
46
+ throw new Error(`Process exit with code ${code}`);
47
+ }) as any;
48
+ });
49
+
50
+ afterEach(() => {
51
+ jest.restoreAllMocks();
52
+ });
53
+
54
+ it('should install rule successfully', async () => {
55
+ mockGetConfig.mockResolvedValue({
56
+ server: 'http://localhost:3000',
57
+ token: 'test-token',
58
+ user: { id: 'user-id', username: 'testuser' },
59
+ lastLogin: new Date().toISOString(),
60
+ });
61
+
62
+ mockGetInstallPaths.mockReturnValue({
63
+ rules: '/path/to/rules',
64
+ commands: '/path/to/commands',
65
+ });
66
+
67
+ const mockRulesApi = {
68
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
69
+ data: [
70
+ {
71
+ id: 'rule-id-1',
72
+ name: 'Test Rule',
73
+ tag: 'test',
74
+ type: 'rule',
75
+ },
76
+ ],
77
+ }),
78
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
79
+ data: {
80
+ downloadUrl: 'https://signed-url.com/file',
81
+ },
82
+ }),
83
+ };
84
+
85
+ mockCreateApi.mockResolvedValue({
86
+ authApi: {} as any,
87
+ rulesApi: mockRulesApi as any,
88
+ } as any);
89
+
90
+ mockAxiosGet.mockResolvedValue({
91
+ data: 'rule content',
92
+ });
93
+
94
+ mockFsPathExists.mockResolvedValue(false);
95
+ mockFsEnsureDir.mockResolvedValue(undefined);
96
+ mockFsWriteFile.mockResolvedValue(undefined);
97
+
98
+ // Mock inquirer when multiple rules exist
99
+ const mockInquirer = require('inquirer');
100
+ mockInquirer.prompt = jest.fn().mockResolvedValue({ rule: 'rule-id-1' });
101
+
102
+ await installCommand({
103
+ tag: 'test',
104
+ type: 'rule',
105
+ });
106
+
107
+ expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalled();
108
+ expect(mockRulesApi.rulesControllerGetDownloadUrl).toHaveBeenCalledWith('rule-id-1');
109
+ expect(mockAxiosGet).toHaveBeenCalledWith('https://signed-url.com/file', {
110
+ responseType: 'text',
111
+ });
112
+ expect(mockFsWriteFile).toHaveBeenCalled();
113
+ });
114
+
115
+ // TC-UNIT-CLI-026: Install with tag filtering
116
+ it('should filter rules by tag', async () => {
117
+ mockGetConfig.mockResolvedValue({
118
+ server: 'http://localhost:3000',
119
+ token: 'test-token',
120
+ user: { id: 'user-id', username: 'testuser' },
121
+ lastLogin: new Date().toISOString(),
122
+ });
123
+
124
+ mockGetInstallPaths.mockReturnValue({
125
+ rules: '/path/to/rules',
126
+ commands: '/path/to/commands',
127
+ });
128
+
129
+ const mockRulesApi = {
130
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
131
+ data: [
132
+ {
133
+ id: 'rule-id-1',
134
+ name: 'Test Rule',
135
+ tag: 'test',
136
+ type: 'rule',
137
+ version: '1.0.0',
138
+ author: { username: 'testuser' },
139
+ description: 'A test rule',
140
+ },
141
+ ],
142
+ }),
143
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
144
+ data: { downloadUrl: 'https://signed-url.com/file' },
145
+ }),
146
+ };
147
+
148
+ mockCreateApi.mockResolvedValue({
149
+ authApi: {} as any,
150
+ rulesApi: mockRulesApi as any,
151
+ } as any);
152
+
153
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
154
+ mockFsPathExists.mockResolvedValue(false);
155
+ mockFsEnsureDir.mockResolvedValue(undefined);
156
+ mockFsWriteFile.mockResolvedValue(undefined);
157
+
158
+ await installCommand({ tag: 'test' });
159
+
160
+ expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalledWith({ tag: 'test' });
161
+ });
162
+
163
+ // TC-UNIT-CLI-027: Install with type filtering
164
+ it('should filter rules by type', async () => {
165
+ mockGetConfig.mockResolvedValue({
166
+ server: 'http://localhost:3000',
167
+ token: 'test-token',
168
+ user: { id: 'user-id', username: 'testuser' },
169
+ lastLogin: new Date().toISOString(),
170
+ });
171
+
172
+ mockGetInstallPaths.mockReturnValue({
173
+ rules: '/path/to/rules',
174
+ commands: '/path/to/commands',
175
+ });
176
+
177
+ const mockRulesApi = {
178
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
179
+ data: [
180
+ {
181
+ id: 'rule-id-1',
182
+ name: 'Test Rule',
183
+ tag: 'test',
184
+ type: 'rule',
185
+ version: '1.0.0',
186
+ author: { username: 'testuser' },
187
+ },
188
+ ],
189
+ }),
190
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
191
+ data: { downloadUrl: 'https://signed-url.com/file' },
192
+ }),
193
+ };
194
+
195
+ mockCreateApi.mockResolvedValue({
196
+ authApi: {} as any,
197
+ rulesApi: mockRulesApi as any,
198
+ } as any);
199
+
200
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
201
+ mockFsPathExists.mockResolvedValue(false);
202
+ mockFsEnsureDir.mockResolvedValue(undefined);
203
+ mockFsWriteFile.mockResolvedValue(undefined);
204
+
205
+ await installCommand({ type: 'rule' });
206
+
207
+ expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalledWith({ type: 'rule' });
208
+ });
209
+
210
+ // TC-UNIT-CLI-028: Install with file download failure
211
+ it('should handle file download failure', async () => {
212
+ mockGetConfig.mockResolvedValue({
213
+ server: 'http://localhost:3000',
214
+ token: 'test-token',
215
+ user: { id: 'user-id', username: 'testuser' },
216
+ lastLogin: new Date().toISOString(),
217
+ });
218
+
219
+ mockGetInstallPaths.mockReturnValue({
220
+ rules: '/path/to/rules',
221
+ commands: '/path/to/commands',
222
+ });
223
+
224
+ const mockRulesApi = {
225
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
226
+ data: [
227
+ {
228
+ id: 'rule-id-1',
229
+ name: 'Test Rule',
230
+ tag: 'test',
231
+ type: 'rule',
232
+ version: '1.0.0',
233
+ author: { username: 'testuser' },
234
+ },
235
+ ],
236
+ }),
237
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
238
+ data: { downloadUrl: 'https://signed-url.com/file' },
239
+ }),
240
+ };
241
+
242
+ mockCreateApi.mockResolvedValue({
243
+ authApi: {} as any,
244
+ rulesApi: mockRulesApi as any,
245
+ } as any);
246
+
247
+ // Mock axios download failure
248
+ mockAxiosGet.mockRejectedValue(new Error('Download failed'));
249
+ mockFsPathExists.mockResolvedValue(false);
250
+
251
+ await expect(installCommand({})).rejects.toThrow('Process exit with code 1');
252
+
253
+ expect(mockAxiosGet).toHaveBeenCalledWith('https://signed-url.com/file', {
254
+ responseType: 'text',
255
+ });
256
+ expect(console.error).toHaveBeenCalledWith('❌ Failed to download rule:', 'Download failed');
257
+ });
258
+
259
+ // TC-UNIT-CLI-029: Install with directory permission issues
260
+ it('should handle directory creation permission issues', async () => {
261
+ mockGetConfig.mockResolvedValue({
262
+ server: 'http://localhost:3000',
263
+ token: 'test-token',
264
+ user: { id: 'user-id', username: 'testuser' },
265
+ lastLogin: new Date().toISOString(),
266
+ });
267
+
268
+ mockGetInstallPaths.mockReturnValue({
269
+ rules: '/path/to/rules',
270
+ commands: '/path/to/commands',
271
+ });
272
+
273
+ const mockRulesApi = {
274
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
275
+ data: [
276
+ {
277
+ id: 'rule-id-1',
278
+ name: 'Test Rule',
279
+ tag: 'test',
280
+ type: 'rule',
281
+ version: '1.0.0',
282
+ author: { username: 'testuser' },
283
+ },
284
+ ],
285
+ }),
286
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
287
+ data: { downloadUrl: 'https://signed-url.com/file' },
288
+ }),
289
+ };
290
+
291
+ mockCreateApi.mockResolvedValue({
292
+ authApi: {} as any,
293
+ rulesApi: mockRulesApi as any,
294
+ } as any);
295
+
296
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
297
+ mockFsPathExists.mockResolvedValue(false);
298
+ mockFsEnsureDir.mockRejectedValue(new Error('Permission denied'));
299
+ mockFsWriteFile.mockResolvedValue(undefined);
300
+
301
+ await expect(installCommand({})).rejects.toThrow('Process exit with code 1');
302
+
303
+ expect(mockFsEnsureDir).toHaveBeenCalledWith('/path/to/rules');
304
+ expect(console.error).toHaveBeenCalledWith('❌ Failed to create installation directory:', 'Permission denied');
305
+ });
306
+
307
+ // TC-UNIT-CLI-030: Install with force overwrite
308
+ it('should handle force overwrite option', async () => {
309
+ mockGetConfig.mockResolvedValue({
310
+ server: 'http://localhost:3000',
311
+ token: 'test-token',
312
+ user: { id: 'user-id', username: 'testuser' },
313
+ lastLogin: new Date().toISOString(),
314
+ });
315
+
316
+ mockGetInstallPaths.mockReturnValue({
317
+ rules: '/path/to/rules',
318
+ commands: '/path/to/commands',
319
+ });
320
+
321
+ const mockRulesApi = {
322
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
323
+ data: [
324
+ {
325
+ id: 'rule-id-1',
326
+ name: 'Test Rule',
327
+ tag: 'test',
328
+ type: 'rule',
329
+ version: '1.0.0',
330
+ author: { username: 'testuser' },
331
+ },
332
+ ],
333
+ }),
334
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
335
+ data: { downloadUrl: 'https://signed-url.com/file' },
336
+ }),
337
+ };
338
+
339
+ mockCreateApi.mockResolvedValue({
340
+ authApi: {} as any,
341
+ rulesApi: mockRulesApi as any,
342
+ } as any);
343
+
344
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
345
+ mockFsPathExists.mockResolvedValue(true); // File already exists
346
+ mockFsEnsureDir.mockResolvedValue(undefined);
347
+ mockFsWriteFile.mockResolvedValue(undefined);
348
+
349
+ await installCommand({ force: true });
350
+
351
+ expect(mockFsWriteFile).toHaveBeenCalledWith(path.join('/path/to/rules', 'rule-id-1'), 'rule content');
352
+ expect(console.log).toHaveBeenCalledWith('✅ Successfully installed rule: Test Rule');
353
+ });
354
+
355
+ // TC-UNIT-CLI-031: Install with no rules found
356
+ it('should handle no rules found', async () => {
357
+ mockGetConfig.mockResolvedValue({
358
+ server: 'http://localhost:3000',
359
+ token: 'test-token',
360
+ user: { id: 'user-id', username: 'testuser' },
361
+ lastLogin: new Date().toISOString(),
362
+ });
363
+
364
+ const mockRulesApi = {
365
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
366
+ data: [],
367
+ }),
368
+ };
369
+
370
+ mockCreateApi.mockResolvedValue({
371
+ authApi: {} as any,
372
+ rulesApi: mockRulesApi as any,
373
+ } as any);
374
+
375
+ await installCommand({});
376
+
377
+ expect(console.log).toHaveBeenCalledWith('📭 No rules found matching the criteria.');
378
+ });
379
+
380
+ // TC-UNIT-CLI-032: Install with multiple rules (interactive selection)
381
+ it('should handle multiple rules with interactive selection', async () => {
382
+ const mockInquirer = require('inquirer');
383
+ mockInquirer.prompt = jest.fn().mockResolvedValue({ rule: 'rule-id-2' });
384
+
385
+ mockGetConfig.mockResolvedValue({
386
+ server: 'http://localhost:3000',
387
+ token: 'test-token',
388
+ user: { id: 'user-id', username: 'testuser' },
389
+ lastLogin: new Date().toISOString(),
390
+ });
391
+
392
+ mockGetInstallPaths.mockReturnValue({
393
+ rules: '/path/to/rules',
394
+ commands: '/path/to/commands',
395
+ });
396
+
397
+ const mockRulesApi = {
398
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
399
+ data: [
400
+ {
401
+ id: 'rule-id-1',
402
+ name: 'Test Rule 1',
403
+ tag: 'test',
404
+ type: 'rule',
405
+ version: '1.0.0',
406
+ author: { username: 'testuser' },
407
+ },
408
+ {
409
+ id: 'rule-id-2',
410
+ name: 'Test Rule 2',
411
+ tag: 'test',
412
+ type: 'rule',
413
+ version: '1.0.0',
414
+ author: { username: 'testuser' },
415
+ },
416
+ ],
417
+ }),
418
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
419
+ data: { downloadUrl: 'https://signed-url.com/file' },
420
+ }),
421
+ };
422
+
423
+ mockCreateApi.mockResolvedValue({
424
+ authApi: {} as any,
425
+ rulesApi: mockRulesApi as any,
426
+ } as any);
427
+
428
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
429
+ mockFsPathExists.mockResolvedValue(false);
430
+ mockFsEnsureDir.mockResolvedValue(undefined);
431
+ mockFsWriteFile.mockResolvedValue(undefined);
432
+
433
+ await installCommand({});
434
+
435
+ expect(mockInquirer.prompt).toHaveBeenCalledWith([
436
+ {
437
+ type: 'list',
438
+ name: 'rule',
439
+ message: 'Select a rule to install:',
440
+ choices: [
441
+ { name: 'Test Rule 1 (rule-id-1)', value: 'rule-id-1' },
442
+ { name: 'Test Rule 2 (rule-id-2)', value: 'rule-id-2' },
443
+ ],
444
+ },
445
+ ]);
446
+ expect(mockRulesApi.rulesControllerGetDownloadUrl).toHaveBeenCalledWith('rule-id-2');
447
+ });
448
+ });
449
+
@@ -0,0 +1,164 @@
1
+ import { listCommand } from '../../commands/list';
2
+ import * as config from '../../config';
3
+
4
+ // Mock dependencies
5
+ jest.mock('../../config', () => ({
6
+ getConfig: jest.fn(),
7
+ }));
8
+
9
+ jest.mock('../../api/client', () => ({
10
+ createApi: jest.fn(),
11
+ }));
12
+
13
+ describe('List Command', () => {
14
+ const mockGetConfig = config.getConfig as jest.MockedFunction<typeof config.getConfig>;
15
+ const mockCreateApi = require('../../api/client').createApi as jest.MockedFunction<any>;
16
+
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ jest.spyOn(console, 'log').mockImplementation();
20
+ jest.spyOn(console, 'error').mockImplementation();
21
+ // Re-establish process.exit mock for each test
22
+ jest.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined) => {
23
+ throw new Error(`Process exit with code ${code}`);
24
+ }) as any;
25
+ });
26
+
27
+ afterEach(() => {
28
+ jest.restoreAllMocks();
29
+ });
30
+
31
+ describe('list success', () => {
32
+ it('should list all rules when no filters provided', async () => {
33
+ mockGetConfig.mockResolvedValue({
34
+ server: 'http://localhost:3000',
35
+ token: 'test-token',
36
+ user: { id: 'user-id', username: 'testuser' },
37
+ lastLogin: new Date().toISOString(),
38
+ });
39
+
40
+ const mockRulesApi = {
41
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
42
+ data: [
43
+ {
44
+ id: 'rule-1',
45
+ name: 'Test Rule 1',
46
+ tag: 'test',
47
+ type: 'rule',
48
+ description: 'First test rule',
49
+ version: '1.0.0',
50
+ author: { username: 'user1' },
51
+ createdAt: '2024-01-01T00:00:00.000Z',
52
+ },
53
+ {
54
+ id: 'rule-2',
55
+ name: 'Test Rule 2',
56
+ tag: 'util',
57
+ type: 'command',
58
+ description: 'Second test rule',
59
+ version: '2.0.0',
60
+ author: { username: 'user2' },
61
+ createdAt: '2024-01-02T00:00:00.000Z',
62
+ },
63
+ ],
64
+ }),
65
+ };
66
+
67
+ mockCreateApi.mockResolvedValue({ rulesApi: mockRulesApi });
68
+
69
+ await listCommand({});
70
+
71
+ expect(mockCreateApi).toHaveBeenCalledWith({ server: 'http://localhost:3000' });
72
+ expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalledWith({});
73
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('📋 Found 2 rules/commands:'));
74
+ });
75
+
76
+ it('should apply filters when provided', async () => {
77
+ mockGetConfig.mockResolvedValue({
78
+ server: 'http://localhost:3000',
79
+ token: 'test-token',
80
+ user: { id: 'user-id', username: 'testuser' },
81
+ lastLogin: new Date().toISOString(),
82
+ });
83
+
84
+ const mockRulesApi = {
85
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
86
+ data: [
87
+ {
88
+ id: 'rule-1',
89
+ name: 'Filtered Rule',
90
+ tag: 'test',
91
+ type: 'rule',
92
+ description: 'Filtered test rule',
93
+ version: '1.0.0',
94
+ author: { username: 'user1' },
95
+ createdAt: '2024-01-01T00:00:00.000Z',
96
+ },
97
+ ],
98
+ }),
99
+ };
100
+
101
+ mockCreateApi.mockResolvedValue({ rulesApi: mockRulesApi });
102
+
103
+ await listCommand({ type: 'rule', tag: 'test', limit: '10' });
104
+
105
+ expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalledWith({
106
+ type: 'rule',
107
+ tag: 'test',
108
+ limit: 10,
109
+ });
110
+ });
111
+
112
+ it('should handle empty results', async () => {
113
+ mockGetConfig.mockResolvedValue({
114
+ server: 'http://localhost:3000',
115
+ token: 'test-token',
116
+ user: { id: 'user-id', username: 'testuser' },
117
+ lastLogin: new Date().toISOString(),
118
+ });
119
+
120
+ const mockRulesApi = {
121
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
122
+ data: [],
123
+ }),
124
+ };
125
+
126
+ mockCreateApi.mockResolvedValue({ rulesApi: mockRulesApi });
127
+
128
+ await listCommand({});
129
+
130
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('📭 No rules or commands found.'));
131
+ });
132
+ });
133
+
134
+ describe('list failure', () => {
135
+ it('should exit with error when not logged in', async () => {
136
+ mockGetConfig.mockResolvedValue({
137
+ server: 'http://localhost:3000',
138
+ token: undefined,
139
+ user: undefined,
140
+ });
141
+
142
+ await expect(listCommand({})).rejects.toThrow('Process exit with code 1');
143
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('❌ Not logged in. Please run `kitstore login` first.'));
144
+ });
145
+
146
+ it('should handle API errors gracefully', async () => {
147
+ mockGetConfig.mockResolvedValue({
148
+ server: 'http://localhost:3000',
149
+ token: 'test-token',
150
+ user: { id: 'user-id', username: 'testuser' },
151
+ lastLogin: new Date().toISOString(),
152
+ });
153
+
154
+ const mockRulesApi = {
155
+ rulesControllerFindAll: jest.fn().mockRejectedValue(new Error('API Error')),
156
+ };
157
+
158
+ mockCreateApi.mockResolvedValue({ rulesApi: mockRulesApi });
159
+
160
+ await expect(listCommand({})).rejects.toThrow('Process exit with code 1');
161
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('❌ Failed to list rules:'));
162
+ });
163
+ });
164
+ });