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
package/.env.test ADDED
@@ -0,0 +1,4 @@
1
+ # Test environment configuration for CLI
2
+ NODE_ENV=test
3
+ API_BASE_URL=http://localhost:3001
4
+ JWT_SECRET=test-secret-key-for-cli-tests-min-32-chars-long
package/.eslintrc.js ADDED
@@ -0,0 +1,29 @@
1
+ module.exports = {
2
+ parser: '@typescript-eslint/parser',
3
+ parserOptions: {
4
+ project: 'tsconfig.json',
5
+ tsconfigRootDir: __dirname,
6
+ sourceType: 'module',
7
+ },
8
+ plugins: ['@typescript-eslint/eslint-plugin'],
9
+ extends: [
10
+ 'eslint:recommended',
11
+ '@typescript-eslint/recommended',
12
+ ],
13
+ root: true,
14
+ env: {
15
+ node: true,
16
+ jest: true,
17
+ },
18
+ ignorePatterns: ['.eslintrc.js', 'dist'],
19
+ rules: {
20
+ '@typescript-eslint/interface-name-prefix': 'off',
21
+ '@typescript-eslint/explicit-function-return-type': 'off',
22
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
23
+ '@typescript-eslint/no-explicit-any': 'off',
24
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
25
+ },
26
+ };
27
+
28
+
29
+
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const init_1 = require("../../commands/init");
40
+ const fs = __importStar(require("fs-extra"));
41
+ const path = __importStar(require("path"));
42
+ const inquirer_1 = __importDefault(require("inquirer"));
43
+ jest.mock('fs-extra');
44
+ jest.mock('inquirer');
45
+ describe('Init Command', () => {
46
+ const cwd = process.cwd();
47
+ const cursorDir = path.join(cwd, '.cursor');
48
+ beforeEach(() => {
49
+ jest.clearAllMocks();
50
+ jest.spyOn(console, 'log').mockImplementation();
51
+ jest.spyOn(console, 'error').mockImplementation();
52
+ });
53
+ it('should initialize cursor kit directories', async () => {
54
+ fs.pathExists.mockResolvedValue(false);
55
+ fs.ensureDir.mockResolvedValue(undefined);
56
+ await (0, init_1.initCommand)();
57
+ expect(fs.ensureDir).toHaveBeenCalledWith(path.join(cursorDir, 'rules'));
58
+ expect(fs.ensureDir).toHaveBeenCalledWith(path.join(cursorDir, 'commands'));
59
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Successfully initialized Cursor Kit!'));
60
+ });
61
+ it('should ask for confirmation if .cursor exists', async () => {
62
+ fs.pathExists.mockResolvedValue(true);
63
+ inquirer_1.default.prompt.mockResolvedValue({ overwrite: true });
64
+ fs.ensureDir.mockResolvedValue(undefined);
65
+ await (0, init_1.initCommand)();
66
+ expect(inquirer_1.default.prompt).toHaveBeenCalled();
67
+ expect(fs.ensureDir).toHaveBeenCalled();
68
+ });
69
+ it('should cancel if user chooses not to overwrite', async () => {
70
+ fs.pathExists.mockResolvedValue(true);
71
+ inquirer_1.default.prompt.mockResolvedValue({ overwrite: false });
72
+ await (0, init_1.initCommand)();
73
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Initialization cancelled.'));
74
+ expect(fs.ensureDir).not.toHaveBeenCalled();
75
+ });
76
+ });
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const install_1 = require("../../commands/install");
40
+ const config = __importStar(require("../../config"));
41
+ const apiClient = __importStar(require("../../api/client"));
42
+ const axios_1 = __importDefault(require("axios"));
43
+ const fs = __importStar(require("fs-extra"));
44
+ const path = __importStar(require("path"));
45
+ // Mock dependencies
46
+ jest.mock('../../config', () => ({
47
+ getConfig: jest.fn(),
48
+ getInstallPaths: jest.fn(),
49
+ }));
50
+ jest.mock('../../api/client', () => ({
51
+ createApi: jest.fn(),
52
+ }));
53
+ jest.mock('axios');
54
+ jest.mock('fs-extra', () => ({
55
+ pathExists: jest.fn(),
56
+ ensureDir: jest.fn(),
57
+ writeFile: jest.fn(),
58
+ createReadStream: jest.fn(),
59
+ }));
60
+ jest.mock('inquirer', () => ({
61
+ prompt: jest.fn(),
62
+ }));
63
+ // TC-UNIT-CLI-003: Install command (success)
64
+ describe('Install Command', () => {
65
+ const mockGetConfig = config.getConfig;
66
+ const mockGetInstallPaths = config.getInstallPaths;
67
+ const mockCreateApi = apiClient.createApi;
68
+ const mockAxiosGet = axios_1.default.get;
69
+ const mockFsEnsureDir = fs.ensureDir;
70
+ const mockFsPathExists = fs.pathExists;
71
+ const mockFsWriteFile = fs.writeFile;
72
+ beforeEach(() => {
73
+ jest.clearAllMocks();
74
+ jest.spyOn(console, 'log').mockImplementation();
75
+ jest.spyOn(console, 'error').mockImplementation();
76
+ // Re-establish process.exit mock for each test
77
+ jest.spyOn(process, 'exit').mockImplementation((code) => {
78
+ throw new Error(`Process exit with code ${code}`);
79
+ });
80
+ });
81
+ afterEach(() => {
82
+ jest.restoreAllMocks();
83
+ });
84
+ it('should install rule successfully', async () => {
85
+ mockGetConfig.mockResolvedValue({
86
+ server: 'http://localhost:3000',
87
+ token: 'test-token',
88
+ user: { id: 'user-id', username: 'testuser' },
89
+ lastLogin: new Date().toISOString(),
90
+ });
91
+ mockGetInstallPaths.mockReturnValue({
92
+ rules: '/path/to/rules',
93
+ commands: '/path/to/commands',
94
+ });
95
+ const mockRulesApi = {
96
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
97
+ data: [
98
+ {
99
+ id: 'rule-id-1',
100
+ name: 'Test Rule',
101
+ tag: 'test',
102
+ type: 'rule',
103
+ },
104
+ ],
105
+ }),
106
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
107
+ data: {
108
+ downloadUrl: 'https://signed-url.com/file',
109
+ },
110
+ }),
111
+ };
112
+ mockCreateApi.mockResolvedValue({
113
+ authApi: {},
114
+ rulesApi: mockRulesApi,
115
+ });
116
+ mockAxiosGet.mockResolvedValue({
117
+ data: 'rule content',
118
+ });
119
+ mockFsPathExists.mockResolvedValue(false);
120
+ mockFsEnsureDir.mockResolvedValue(undefined);
121
+ mockFsWriteFile.mockResolvedValue(undefined);
122
+ // Mock inquirer when multiple rules exist
123
+ const mockInquirer = require('inquirer');
124
+ mockInquirer.prompt = jest.fn().mockResolvedValue({ rule: 'rule-id-1' });
125
+ await (0, install_1.installCommand)({
126
+ tag: 'test',
127
+ type: 'rule',
128
+ });
129
+ expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalled();
130
+ expect(mockRulesApi.rulesControllerGetDownloadUrl).toHaveBeenCalledWith('rule-id-1');
131
+ expect(mockAxiosGet).toHaveBeenCalledWith('https://signed-url.com/file', {
132
+ responseType: 'text',
133
+ });
134
+ expect(mockFsWriteFile).toHaveBeenCalled();
135
+ });
136
+ // TC-UNIT-CLI-026: Install with tag filtering
137
+ it('should filter rules by tag', async () => {
138
+ mockGetConfig.mockResolvedValue({
139
+ server: 'http://localhost:3000',
140
+ token: 'test-token',
141
+ user: { id: 'user-id', username: 'testuser' },
142
+ lastLogin: new Date().toISOString(),
143
+ });
144
+ mockGetInstallPaths.mockReturnValue({
145
+ rules: '/path/to/rules',
146
+ commands: '/path/to/commands',
147
+ });
148
+ const mockRulesApi = {
149
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
150
+ data: [
151
+ {
152
+ id: 'rule-id-1',
153
+ name: 'Test Rule',
154
+ tag: 'test',
155
+ type: 'rule',
156
+ version: '1.0.0',
157
+ author: { username: 'testuser' },
158
+ description: 'A test rule',
159
+ },
160
+ ],
161
+ }),
162
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
163
+ data: { downloadUrl: 'https://signed-url.com/file' },
164
+ }),
165
+ };
166
+ mockCreateApi.mockResolvedValue({
167
+ authApi: {},
168
+ rulesApi: mockRulesApi,
169
+ });
170
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
171
+ mockFsPathExists.mockResolvedValue(false);
172
+ mockFsEnsureDir.mockResolvedValue(undefined);
173
+ mockFsWriteFile.mockResolvedValue(undefined);
174
+ await (0, install_1.installCommand)({ tag: 'test' });
175
+ expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalledWith({ tag: 'test' });
176
+ });
177
+ // TC-UNIT-CLI-027: Install with type filtering
178
+ it('should filter rules by type', async () => {
179
+ mockGetConfig.mockResolvedValue({
180
+ server: 'http://localhost:3000',
181
+ token: 'test-token',
182
+ user: { id: 'user-id', username: 'testuser' },
183
+ lastLogin: new Date().toISOString(),
184
+ });
185
+ mockGetInstallPaths.mockReturnValue({
186
+ rules: '/path/to/rules',
187
+ commands: '/path/to/commands',
188
+ });
189
+ const mockRulesApi = {
190
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
191
+ data: [
192
+ {
193
+ id: 'rule-id-1',
194
+ name: 'Test Rule',
195
+ tag: 'test',
196
+ type: 'rule',
197
+ version: '1.0.0',
198
+ author: { username: 'testuser' },
199
+ },
200
+ ],
201
+ }),
202
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
203
+ data: { downloadUrl: 'https://signed-url.com/file' },
204
+ }),
205
+ };
206
+ mockCreateApi.mockResolvedValue({
207
+ authApi: {},
208
+ rulesApi: mockRulesApi,
209
+ });
210
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
211
+ mockFsPathExists.mockResolvedValue(false);
212
+ mockFsEnsureDir.mockResolvedValue(undefined);
213
+ mockFsWriteFile.mockResolvedValue(undefined);
214
+ await (0, install_1.installCommand)({ type: 'rule' });
215
+ expect(mockRulesApi.rulesControllerFindAll).toHaveBeenCalledWith({ type: 'rule' });
216
+ });
217
+ // TC-UNIT-CLI-028: Install with file download failure
218
+ it('should handle file download failure', async () => {
219
+ mockGetConfig.mockResolvedValue({
220
+ server: 'http://localhost:3000',
221
+ token: 'test-token',
222
+ user: { id: 'user-id', username: 'testuser' },
223
+ lastLogin: new Date().toISOString(),
224
+ });
225
+ mockGetInstallPaths.mockReturnValue({
226
+ rules: '/path/to/rules',
227
+ commands: '/path/to/commands',
228
+ });
229
+ const mockRulesApi = {
230
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
231
+ data: [
232
+ {
233
+ id: 'rule-id-1',
234
+ name: 'Test Rule',
235
+ tag: 'test',
236
+ type: 'rule',
237
+ version: '1.0.0',
238
+ author: { username: 'testuser' },
239
+ },
240
+ ],
241
+ }),
242
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
243
+ data: { downloadUrl: 'https://signed-url.com/file' },
244
+ }),
245
+ };
246
+ mockCreateApi.mockResolvedValue({
247
+ authApi: {},
248
+ rulesApi: mockRulesApi,
249
+ });
250
+ // Mock axios download failure
251
+ mockAxiosGet.mockRejectedValue(new Error('Download failed'));
252
+ mockFsPathExists.mockResolvedValue(false);
253
+ await expect((0, install_1.installCommand)({})).rejects.toThrow('Process exit with code 1');
254
+ expect(mockAxiosGet).toHaveBeenCalledWith('https://signed-url.com/file', {
255
+ responseType: 'text',
256
+ });
257
+ expect(console.error).toHaveBeenCalledWith('❌ Failed to download rule:', 'Download failed');
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
+ mockGetInstallPaths.mockReturnValue({
268
+ rules: '/path/to/rules',
269
+ commands: '/path/to/commands',
270
+ });
271
+ const mockRulesApi = {
272
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
273
+ data: [
274
+ {
275
+ id: 'rule-id-1',
276
+ name: 'Test Rule',
277
+ tag: 'test',
278
+ type: 'rule',
279
+ version: '1.0.0',
280
+ author: { username: 'testuser' },
281
+ },
282
+ ],
283
+ }),
284
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
285
+ data: { downloadUrl: 'https://signed-url.com/file' },
286
+ }),
287
+ };
288
+ mockCreateApi.mockResolvedValue({
289
+ authApi: {},
290
+ rulesApi: mockRulesApi,
291
+ });
292
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
293
+ mockFsPathExists.mockResolvedValue(false);
294
+ mockFsEnsureDir.mockRejectedValue(new Error('Permission denied'));
295
+ mockFsWriteFile.mockResolvedValue(undefined);
296
+ await expect((0, install_1.installCommand)({})).rejects.toThrow('Process exit with code 1');
297
+ expect(mockFsEnsureDir).toHaveBeenCalledWith('/path/to/rules');
298
+ expect(console.error).toHaveBeenCalledWith('❌ Failed to create installation directory:', 'Permission denied');
299
+ });
300
+ // TC-UNIT-CLI-030: Install with force overwrite
301
+ it('should handle force overwrite option', async () => {
302
+ mockGetConfig.mockResolvedValue({
303
+ server: 'http://localhost:3000',
304
+ token: 'test-token',
305
+ user: { id: 'user-id', username: 'testuser' },
306
+ lastLogin: new Date().toISOString(),
307
+ });
308
+ mockGetInstallPaths.mockReturnValue({
309
+ rules: '/path/to/rules',
310
+ commands: '/path/to/commands',
311
+ });
312
+ const mockRulesApi = {
313
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
314
+ data: [
315
+ {
316
+ id: 'rule-id-1',
317
+ name: 'Test Rule',
318
+ tag: 'test',
319
+ type: 'rule',
320
+ version: '1.0.0',
321
+ author: { username: 'testuser' },
322
+ },
323
+ ],
324
+ }),
325
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
326
+ data: { downloadUrl: 'https://signed-url.com/file' },
327
+ }),
328
+ };
329
+ mockCreateApi.mockResolvedValue({
330
+ authApi: {},
331
+ rulesApi: mockRulesApi,
332
+ });
333
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
334
+ mockFsPathExists.mockResolvedValue(true); // File already exists
335
+ mockFsEnsureDir.mockResolvedValue(undefined);
336
+ mockFsWriteFile.mockResolvedValue(undefined);
337
+ await (0, install_1.installCommand)({ force: true });
338
+ expect(mockFsWriteFile).toHaveBeenCalledWith(path.join('/path/to/rules', 'rule-id-1'), 'rule content');
339
+ expect(console.log).toHaveBeenCalledWith('✅ Successfully installed rule: Test Rule');
340
+ });
341
+ // TC-UNIT-CLI-031: Install with no rules found
342
+ it('should handle no rules found', async () => {
343
+ mockGetConfig.mockResolvedValue({
344
+ server: 'http://localhost:3000',
345
+ token: 'test-token',
346
+ user: { id: 'user-id', username: 'testuser' },
347
+ lastLogin: new Date().toISOString(),
348
+ });
349
+ const mockRulesApi = {
350
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
351
+ data: [],
352
+ }),
353
+ };
354
+ mockCreateApi.mockResolvedValue({
355
+ authApi: {},
356
+ rulesApi: mockRulesApi,
357
+ });
358
+ await (0, install_1.installCommand)({});
359
+ expect(console.log).toHaveBeenCalledWith('📭 No rules found matching the criteria.');
360
+ });
361
+ // TC-UNIT-CLI-032: Install with multiple rules (interactive selection)
362
+ it('should handle multiple rules with interactive selection', async () => {
363
+ const mockInquirer = require('inquirer');
364
+ mockInquirer.prompt = jest.fn().mockResolvedValue({ rule: 'rule-id-2' });
365
+ mockGetConfig.mockResolvedValue({
366
+ server: 'http://localhost:3000',
367
+ token: 'test-token',
368
+ user: { id: 'user-id', username: 'testuser' },
369
+ lastLogin: new Date().toISOString(),
370
+ });
371
+ mockGetInstallPaths.mockReturnValue({
372
+ rules: '/path/to/rules',
373
+ commands: '/path/to/commands',
374
+ });
375
+ const mockRulesApi = {
376
+ rulesControllerFindAll: jest.fn().mockResolvedValue({
377
+ data: [
378
+ {
379
+ id: 'rule-id-1',
380
+ name: 'Test Rule 1',
381
+ tag: 'test',
382
+ type: 'rule',
383
+ version: '1.0.0',
384
+ author: { username: 'testuser' },
385
+ },
386
+ {
387
+ id: 'rule-id-2',
388
+ name: 'Test Rule 2',
389
+ tag: 'test',
390
+ type: 'rule',
391
+ version: '1.0.0',
392
+ author: { username: 'testuser' },
393
+ },
394
+ ],
395
+ }),
396
+ rulesControllerGetDownloadUrl: jest.fn().mockResolvedValue({
397
+ data: { downloadUrl: 'https://signed-url.com/file' },
398
+ }),
399
+ };
400
+ mockCreateApi.mockResolvedValue({
401
+ authApi: {},
402
+ rulesApi: mockRulesApi,
403
+ });
404
+ mockAxiosGet.mockResolvedValue({ data: 'rule content' });
405
+ mockFsPathExists.mockResolvedValue(false);
406
+ mockFsEnsureDir.mockResolvedValue(undefined);
407
+ mockFsWriteFile.mockResolvedValue(undefined);
408
+ await (0, install_1.installCommand)({});
409
+ expect(mockInquirer.prompt).toHaveBeenCalledWith([
410
+ {
411
+ type: 'list',
412
+ name: 'rule',
413
+ message: 'Select a rule to install:',
414
+ choices: [
415
+ { name: 'Test Rule 1 (rule-id-1)', value: 'rule-id-1' },
416
+ { name: 'Test Rule 2 (rule-id-2)', value: 'rule-id-2' },
417
+ ],
418
+ },
419
+ ]);
420
+ expect(mockRulesApi.rulesControllerGetDownloadUrl).toHaveBeenCalledWith('rule-id-2');
421
+ });
422
+ });