@nexical/cli 0.11.0 → 0.11.2

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 (75) hide show
  1. package/.github/workflows/deploy.yml +1 -1
  2. package/.husky/pre-commit +1 -0
  3. package/.prettierignore +8 -0
  4. package/.prettierrc +7 -0
  5. package/GEMINI.md +36 -30
  6. package/README.md +85 -56
  7. package/dist/chunk-AC4B3HPJ.js +93 -0
  8. package/dist/chunk-AC4B3HPJ.js.map +1 -0
  9. package/dist/{chunk-JYASTIIW.js → chunk-PJIOCW2A.js} +1 -1
  10. package/dist/chunk-PJIOCW2A.js.map +1 -0
  11. package/dist/{chunk-WKERTCM6.js → chunk-Q7YLW5HJ.js} +5 -2
  12. package/dist/chunk-Q7YLW5HJ.js.map +1 -0
  13. package/dist/index.js +41 -12
  14. package/dist/index.js.map +1 -1
  15. package/dist/src/commands/init.d.ts +4 -1
  16. package/dist/src/commands/init.js +8 -4
  17. package/dist/src/commands/init.js.map +1 -1
  18. package/dist/src/commands/module/add.d.ts +3 -1
  19. package/dist/src/commands/module/add.js +24 -13
  20. package/dist/src/commands/module/add.js.map +1 -1
  21. package/dist/src/commands/module/list.js +9 -5
  22. package/dist/src/commands/module/list.js.map +1 -1
  23. package/dist/src/commands/module/remove.d.ts +3 -1
  24. package/dist/src/commands/module/remove.js +13 -7
  25. package/dist/src/commands/module/remove.js.map +1 -1
  26. package/dist/src/commands/module/update.d.ts +3 -1
  27. package/dist/src/commands/module/update.js +7 -5
  28. package/dist/src/commands/module/update.js.map +1 -1
  29. package/dist/src/commands/run.d.ts +4 -1
  30. package/dist/src/commands/run.js +10 -2
  31. package/dist/src/commands/run.js.map +1 -1
  32. package/dist/src/commands/setup.js +9 -4
  33. package/dist/src/commands/setup.js.map +1 -1
  34. package/dist/src/utils/discovery.js +1 -1
  35. package/dist/src/utils/git.js +1 -1
  36. package/dist/src/utils/url-resolver.js +1 -1
  37. package/eslint.config.mjs +67 -0
  38. package/index.ts +34 -20
  39. package/package.json +56 -32
  40. package/src/commands/init.ts +79 -76
  41. package/src/commands/module/add.ts +158 -148
  42. package/src/commands/module/list.ts +61 -50
  43. package/src/commands/module/remove.ts +59 -54
  44. package/src/commands/module/update.ts +44 -42
  45. package/src/commands/run.ts +89 -81
  46. package/src/commands/setup.ts +70 -60
  47. package/src/utils/discovery.ts +98 -113
  48. package/src/utils/git.ts +35 -28
  49. package/src/utils/url-resolver.ts +50 -45
  50. package/test/e2e/lifecycle.e2e.test.ts +139 -131
  51. package/test/integration/commands/init.integration.test.ts +64 -64
  52. package/test/integration/commands/module.integration.test.ts +122 -122
  53. package/test/integration/commands/run.integration.test.ts +70 -63
  54. package/test/integration/utils/command-loading.integration.test.ts +40 -53
  55. package/test/unit/commands/init.test.ts +163 -128
  56. package/test/unit/commands/module/add.test.ts +312 -245
  57. package/test/unit/commands/module/list.test.ts +108 -91
  58. package/test/unit/commands/module/remove.test.ts +74 -67
  59. package/test/unit/commands/module/update.test.ts +74 -70
  60. package/test/unit/commands/run.test.ts +253 -201
  61. package/test/unit/commands/setup.test.ts +138 -128
  62. package/test/unit/utils/command-discovery.test.ts +138 -125
  63. package/test/unit/utils/git.test.ts +135 -117
  64. package/test/unit/utils/integration-helpers.test.ts +59 -49
  65. package/test/unit/utils/url-resolver.test.ts +46 -34
  66. package/test/utils/integration-helpers.ts +36 -29
  67. package/tsconfig.json +15 -25
  68. package/tsup.config.ts +14 -14
  69. package/vitest.config.ts +10 -10
  70. package/vitest.e2e.config.ts +6 -6
  71. package/vitest.integration.config.ts +17 -17
  72. package/dist/chunk-JYASTIIW.js.map +0 -1
  73. package/dist/chunk-OKXOCNXP.js +0 -105
  74. package/dist/chunk-OKXOCNXP.js.map +0 -1
  75. package/dist/chunk-WKERTCM6.js.map +0 -1
@@ -8,162 +8,172 @@ import { CLI } from '@nexical/cli-core';
8
8
  vi.mock('fs-extra');
9
9
 
10
10
  describe('SetupCommand', () => {
11
- let command: SetupCommand;
12
- let mockCli: CLI;
13
- let exitSpy: any;
14
-
15
- // Mock BaseCommand methods
16
- // We need to extend SetupCommand or mock the prototype to capture error/warn/success
17
- // Or we can just spy on them if we can access the instance methods.
18
-
19
- // Better approach: Spy on the prototype methods of BaseCommand or the instance itself.
20
- // However, BaseCommand methods like `error` might process.exit.
21
-
22
- // Let's create a subclass for testing or mock the CLI and use the standard instantiation.
23
- // The current SetupCommand implementation calls `process.exit(1)` in `error` logic in `run`.
24
- // Wait, looking at `setup.ts`:
25
- // if (!fs.existsSync(path.join(rootDir, 'core'))) {
26
- // this.error('Could not find "core" directory. Are you in the project root?');
27
- // process.exit(1);
28
- // }
29
-
30
- // So we need to stub process.exit to prevent test runner from exiting.
31
-
32
- beforeEach(() => {
33
- vi.clearAllMocks();
34
- mockCli = new CLI({ commandName: 'test-cli' });
35
- command = new SetupCommand(mockCli);
36
-
37
- // Spy on logging methods
38
- vi.spyOn(command, 'error').mockImplementation(() => { });
39
- vi.spyOn(command, 'warn').mockImplementation(() => { });
40
- vi.spyOn(command, 'info').mockImplementation(() => { });
41
- vi.spyOn(command, 'success').mockImplementation(() => { });
42
-
43
- // Mock process.cwd to return a known path
44
- vi.spyOn(process, 'cwd').mockReturnValue('/mock/project/root');
45
-
46
- // Mock process.exit
47
- exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { }) as any);
11
+ let command: SetupCommand;
12
+ let mockCli: CLI;
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ let exitSpy: any;
15
+
16
+ // Mock BaseCommand methods
17
+ // We need to extend SetupCommand or mock the prototype to capture error/warn/success
18
+ // Or we can just spy on them if we can access the instance methods.
19
+
20
+ // Better approach: Spy on the prototype methods of BaseCommand or the instance itself.
21
+ // However, BaseCommand methods like `error` might process.exit.
22
+
23
+ // Let's create a subclass for testing or mock the CLI and use the standard instantiation.
24
+ // The current SetupCommand implementation calls `process.exit(1)` in `error` logic in `run`.
25
+ // Wait, looking at `setup.ts`:
26
+ // if (!fs.existsSync(path.join(rootDir, 'core'))) {
27
+ // this.error('Could not find "core" directory. Are you in the project root?');
28
+ // process.exit(1);
29
+ // }
30
+
31
+ // So we need to stub process.exit to prevent test runner from exiting.
32
+
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ mockCli = new CLI({ commandName: 'test-cli' });
36
+ command = new SetupCommand(mockCli);
37
+
38
+ // Spy on logging methods
39
+ vi.spyOn(command, 'error').mockImplementation(() => {});
40
+ vi.spyOn(command, 'warn').mockImplementation(() => {});
41
+ vi.spyOn(command, 'info').mockImplementation(() => {});
42
+ vi.spyOn(command, 'success').mockImplementation(() => {});
43
+
44
+ // Mock process.cwd to return a known path
45
+ vi.spyOn(process, 'cwd').mockReturnValue('/mock/project/root');
46
+
47
+ // Mock process.exit
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
50
+ });
51
+
52
+ afterEach(() => {
53
+ vi.restoreAllMocks();
54
+ });
55
+
56
+ it('should error if "core" directory is missing', async () => {
57
+ // specific check: fs.existsSync returns false for core
58
+ vi.mocked(fs.existsSync).mockReturnValue(false);
59
+
60
+ await command.run();
61
+
62
+ expect(command.error).toHaveBeenCalledWith(
63
+ 'Could not find "core" directory. Are you in the project root?',
64
+ );
65
+ expect(exitSpy).toHaveBeenCalledWith(1);
66
+ });
67
+
68
+ it('should warn and skip if app directory is missing', async () => {
69
+ // Setup fs mocks
70
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
71
+ const pStr = p.toString();
72
+ if (pStr.endsWith('core')) return true;
73
+ if (pStr.endsWith('apps/frontend')) return true;
74
+ if (pStr.endsWith('apps/backend')) return false; // Missing backend
75
+ return false;
48
76
  });
49
77
 
50
- afterEach(() => {
51
- vi.restoreAllMocks();
52
- });
53
-
54
- it('should error if "core" directory is missing', async () => {
55
- // specific check: fs.existsSync returns false for core
56
- vi.mocked(fs.existsSync).mockReturnValue(false);
57
-
58
- await command.run();
78
+ await command.run();
59
79
 
60
- expect(command.error).toHaveBeenCalledWith('Could not find "core" directory. Are you in the project root?');
61
- expect(exitSpy).toHaveBeenCalledWith(1);
62
- });
80
+ expect(command.warn).toHaveBeenCalledWith('App directory backend not found. Skipping.');
81
+ expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
82
+ });
63
83
 
64
- it('should warn and skip if app directory is missing', async () => {
65
- // Setup fs mocks
66
- vi.mocked(fs.existsSync).mockImplementation((p) => {
67
- const pStr = p.toString();
68
- if (pStr.endsWith('core')) return true;
69
- if (pStr.endsWith('apps/frontend')) return true;
70
- if (pStr.endsWith('apps/backend')) return false; // Missing backend
71
- return false;
72
- });
84
+ it('should symlink shared assets', async () => {
85
+ // Setup fs mocks
86
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
87
+ const pStr = p.toString();
88
+ // Core exists
89
+ if (pStr.endsWith('core')) return true;
90
+ // Apps exist
91
+ if (pStr.endsWith('apps/frontend') || pStr.endsWith('apps/backend')) return true;
73
92
 
74
- await command.run();
93
+ // Shared assets in core exist
94
+ if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
75
95
 
76
- expect(command.warn).toHaveBeenCalledWith('App directory backend not found. Skipping.');
77
- expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
96
+ return false;
78
97
  });
79
98
 
80
- it('should symlink shared assets', async () => {
81
- // Setup fs mocks
82
- vi.mocked(fs.existsSync).mockImplementation((p) => {
83
- const pStr = p.toString();
84
- // Core exists
85
- if (pStr.endsWith('core')) return true;
86
- // Apps exist
87
- if (pStr.endsWith('apps/frontend') || pStr.endsWith('apps/backend')) return true;
88
-
89
- // Shared assets in core exist
90
- if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
91
101
 
92
- return false;
93
- });
102
+ await command.run();
94
103
 
95
- vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
104
+ // Check if verify apps are processed
105
+ expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
106
+ expect(command.info).toHaveBeenCalledWith('Setting up backend...');
96
107
 
97
- await command.run();
108
+ // Check symlink calls
109
+ // We have 2 apps * 7 shared assets = 14 symlinks
110
+ // sharedAssets = ['prisma', 'src', 'public', 'locales', 'scripts']
98
111
 
99
- // Check if verify apps are processed
100
- expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
101
- expect(command.info).toHaveBeenCalledWith('Setting up backend...');
112
+ const assets = ['prisma', 'src', 'public', 'locales', 'scripts'];
102
113
 
103
- // Check symlink calls
104
- // We have 2 apps * 7 shared assets = 14 symlinks
105
- // sharedAssets = ['prisma', 'src', 'public', 'locales', 'scripts', 'astro.config.mjs', 'tsconfig.json']
114
+ for (const app of ['frontend', 'backend']) {
115
+ for (const asset of assets) {
116
+ const dest = path.join('/mock/project/root', 'apps', app, asset);
117
+ // const source = path.join('/mock/project/root', 'core', asset);
106
118
 
107
- const assets = ['prisma', 'src', 'public', 'locales', 'scripts', 'astro.config.mjs', 'tsconfig.json'];
119
+ // Ensure removeSync called
120
+ expect(fs.removeSync).toHaveBeenCalledWith(dest);
108
121
 
109
- for (const app of ['frontend', 'backend']) {
110
- for (const asset of assets) {
111
- const dest = path.join('/mock/project/root', 'apps', app, asset);
112
- const source = path.join('/mock/project/root', 'core', asset);
122
+ // Ensure symlink called
123
+ // valid relative path calculation might vary, but verify arguments
124
+ expect(fs.symlink).toHaveBeenCalled();
125
+ }
126
+ }
113
127
 
114
- // Ensure removeSync called
115
- expect(fs.removeSync).toHaveBeenCalledWith(dest);
128
+ expect(command.success).toHaveBeenCalledWith('Application setup complete.');
129
+ });
116
130
 
117
- // Ensure symlink called
118
- // valid relative path calculation might vary, but verify arguments
119
- expect(fs.symlink).toHaveBeenCalled();
120
- }
121
- }
131
+ it('should warn if source asset is missing in core', async () => {
132
+ // Setup fs mocks
133
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
134
+ const pStr = p.toString();
135
+ if (pStr.endsWith('core')) return true;
136
+ if (pStr.includes('apps/')) return true;
122
137
 
123
- expect(command.success).toHaveBeenCalledWith('Application setup complete.');
124
- });
138
+ // Mock that 'prisma' is missing in core
139
+ if (pStr.endsWith('core/prisma')) return false;
125
140
 
126
- it('should warn if source asset is missing in core', async () => {
127
- // Setup fs mocks
128
- vi.mocked(fs.existsSync).mockImplementation((p) => {
129
- const pStr = p.toString();
130
- if (pStr.endsWith('core')) return true;
131
- if (pStr.includes('apps/')) return true;
141
+ // Others exist
142
+ if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
132
143
 
133
- // Mock that 'prisma' is missing in core
134
- if (pStr.endsWith('core/prisma')) return false;
144
+ return false;
145
+ });
135
146
 
136
- // Others exist
137
- if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
147
+ await command.run();
138
148
 
139
- return false;
140
- });
149
+ expect(command.warn).toHaveBeenCalledWith('Source asset prisma not found in core.');
150
+ });
141
151
 
142
- await command.run();
152
+ it('should throw error if removal fails with non-ENOENT', async () => {
153
+ vi.mocked(fs.existsSync).mockReturnValue(true);
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
143
156
 
144
- expect(command.warn).toHaveBeenCalledWith('Source asset prisma not found in core.');
157
+ const error = new Error('Permission denied');
158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
+ (error as any).code = 'EACCES';
160
+ vi.mocked(fs.removeSync).mockImplementation(() => {
161
+ throw error;
145
162
  });
146
163
 
147
- it('should throw error if removal fails with non-ENOENT', async () => {
148
- vi.mocked(fs.existsSync).mockReturnValue(true);
149
- vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
150
-
151
- const error = new Error('Permission denied');
152
- (error as any).code = 'EACCES';
153
- vi.mocked(fs.removeSync).mockImplementation(() => { throw error; });
164
+ await command.run();
154
165
 
155
- await command.run();
166
+ expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
167
+ });
156
168
 
157
- expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
158
- });
169
+ it('should log error if symlink fails', async () => {
170
+ vi.mocked(fs.existsSync).mockReturnValue(true);
171
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
172
+ vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
173
+ vi.mocked(fs.symlink).mockRejectedValue(new Error('Symlink failed'));
159
174
 
160
- it('should log error if symlink fails', async () => {
161
- vi.mocked(fs.existsSync).mockReturnValue(true);
162
- vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
163
- vi.mocked(fs.symlink).mockRejectedValue(new Error('Symlink failed'));
175
+ await command.run();
164
176
 
165
- await command.run();
166
-
167
- expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
168
- });
177
+ expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
178
+ });
169
179
  });
@@ -1,5 +1,4 @@
1
-
2
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
2
  import { discoverCommandDirectories } from '../../../src/utils/discovery';
4
3
  import fs from 'node:fs';
5
4
  import path from 'node:path';
@@ -9,126 +8,135 @@ vi.mock('node:fs');
9
8
  // Mock path module to allow controlled resolution for duplicate testing
10
9
  const originalPath = await import('node:path');
11
10
  const originalResolve = originalPath.resolve;
12
- const originalJoin = originalPath.join;
13
11
 
14
12
  vi.mock('node:path', async (importOriginal) => {
15
- const mod = await importOriginal<any>();
16
- return {
17
- ...mod,
18
- default: {
19
- ...mod.default,
20
- resolve: vi.fn((...args: string[]) => mod.default.resolve(...args)),
21
- },
22
- resolve: vi.fn((...args: string[]) => mod.resolve(...args)),
23
- };
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ const mod: any = await importOriginal();
15
+ return {
16
+ ...mod,
17
+ default: {
18
+ ...mod.default,
19
+ resolve: vi.fn((...args: string[]) => mod.default.resolve(...args)),
20
+ },
21
+ resolve: vi.fn((...args: string[]) => mod.resolve(...args)),
22
+ };
24
23
  });
25
24
 
26
25
  vi.mock('@nexical/cli-core', () => ({
27
- logger: {
28
- debug: vi.fn(),
29
- warn: vi.fn(),
30
- error: vi.fn()
31
- }
26
+ logger: {
27
+ debug: vi.fn(),
28
+ warn: vi.fn(),
29
+ error: vi.fn(),
30
+ },
32
31
  }));
33
32
 
34
33
  describe('discoverCommandDirectories', () => {
35
- // ... setup ...
36
- const cwd = '/app';
37
-
38
- beforeEach(() => {
39
- vi.resetAllMocks();
40
- // Restore default path behavior
41
- vi.mocked(path.resolve).mockImplementation(originalResolve);
42
- // Default fs mocks
43
- vi.mocked(fs.existsSync).mockReturnValue(false);
44
- vi.mocked(fs.readdirSync).mockReturnValue([]);
45
- vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
46
- });
47
-
48
- it('should return empty list if no directories exist', () => {
49
- const dirs = discoverCommandDirectories(cwd);
50
- expect(dirs).toHaveLength(0);
34
+ // ... setup ...
35
+ const cwd = '/app';
36
+
37
+ beforeEach(() => {
38
+ vi.resetAllMocks();
39
+ // Restore default path behavior
40
+ vi.mocked(path.resolve).mockImplementation(originalResolve);
41
+ // Default fs mocks
42
+ vi.mocked(fs.existsSync).mockReturnValue(false);
43
+ vi.mocked(fs.readdirSync).mockReturnValue([]);
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
46
+ });
47
+
48
+ it('should return empty list if no directories exist', () => {
49
+ const dirs = discoverCommandDirectories(cwd);
50
+ expect(dirs).toHaveLength(0);
51
+ });
52
+
53
+ it('should find core commands in project directory', () => {
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ vi.mocked(fs.existsSync).mockImplementation((p: any) => {
56
+ return p === path.resolve('/app/src/commands');
51
57
  });
52
58
 
53
- it('should find core commands in project directory', () => {
54
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
55
- return p === path.resolve('/app/src/commands');
56
- });
57
-
58
- const dirs = discoverCommandDirectories(cwd);
59
- expect(dirs).toContain(path.resolve('/app/src/commands'));
59
+ const dirs = discoverCommandDirectories(cwd);
60
+ expect(dirs).toContain(path.resolve('/app/src/commands'));
61
+ });
62
+
63
+ it('should scan modules for commands', () => {
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ vi.mocked(fs.existsSync).mockImplementation((p: any) => {
66
+ if (p === path.resolve('/app/modules')) return true;
67
+ if (p === path.resolve('/app/modules/mod1')) return true;
68
+ if (p === path.resolve('/app/modules/mod1/src/commands')) return true;
69
+ if (p === path.resolve('/app/modules/mod2')) return true;
70
+ return false;
60
71
  });
61
72
 
62
- it('should scan modules for commands', () => {
63
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
64
- if (p === path.resolve('/app/modules')) return true;
65
- if (p === path.resolve('/app/modules/mod1')) return true;
66
- if (p === path.resolve('/app/modules/mod1/src/commands')) return true;
67
- if (p === path.resolve('/app/modules/mod2')) return true;
68
- return false;
69
- });
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ vi.mocked(fs.readdirSync).mockReturnValue(['mod1', 'mod2', '.hidden'] as any);
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
70
77
 
71
- vi.mocked(fs.readdirSync).mockReturnValue(['mod1', 'mod2', '.hidden'] as any);
72
- vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
78
+ const dirs = discoverCommandDirectories(cwd);
73
79
 
74
- const dirs = discoverCommandDirectories(cwd);
80
+ expect(dirs).toContain(path.resolve('/app/modules/mod1/src/commands'));
81
+ expect(dirs).not.toContain(path.resolve('/app/modules/mod2/src/commands'));
82
+ });
75
83
 
76
- expect(dirs).toContain(path.resolve('/app/modules/mod1/src/commands'));
77
- expect(dirs).not.toContain(path.resolve('/app/modules/mod2/src/commands'));
84
+ it('should scan src/modules for commands', () => {
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ vi.mocked(fs.existsSync).mockImplementation((p: any) => {
87
+ if (p === path.resolve('/app/src/modules')) return true;
88
+ if (p === path.resolve('/app/src/modules/mod-src')) return true;
89
+ if (p === path.resolve('/app/src/modules/mod-src/src/commands')) return true;
90
+ return false;
78
91
  });
79
92
 
80
- it('should scan src/modules for commands', () => {
81
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
82
- if (p === path.resolve('/app/src/modules')) return true;
83
- if (p === path.resolve('/app/src/modules/mod-src')) return true;
84
- if (p === path.resolve('/app/src/modules/mod-src/src/commands')) return true;
85
- return false;
86
- });
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ vi.mocked(fs.readdirSync).mockReturnValue(['mod-src'] as any);
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
87
97
 
88
- vi.mocked(fs.readdirSync).mockReturnValue(['mod-src'] as any);
89
- vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
98
+ const dirs = discoverCommandDirectories(cwd);
90
99
 
91
- const dirs = discoverCommandDirectories(cwd);
100
+ expect(dirs).toContain(path.resolve('/app/src/modules/mod-src/src/commands'));
101
+ });
92
102
 
93
- expect(dirs).toContain(path.resolve('/app/src/modules/mod-src/src/commands'));
103
+ it('should handle errors when scanning modules', () => {
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ vi.mocked(fs.existsSync).mockImplementation((p: any) => {
106
+ return p === path.resolve('/app/src/commands');
94
107
  });
95
-
96
- it('should handle errors when scanning modules', () => {
97
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
98
- return p === path.resolve('/app/src/commands');
99
- });
100
- vi.mocked(fs.readdirSync).mockImplementation((p: any) => {
101
- if (p.includes('modules')) throw new Error('Permission denied');
102
- return [];
103
- });
104
-
105
- const dirs = discoverCommandDirectories(cwd);
106
- // Should not crash
107
- expect(dirs).toHaveLength(1);
108
- expect(dirs).toContain(path.resolve('/app/src/commands'));
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ vi.mocked(fs.readdirSync).mockImplementation((p: any) => {
110
+ if (p.includes('modules')) throw new Error('Permission denied');
111
+ return [];
109
112
  });
110
113
 
111
- it('should deduplicate dist and src core commands', () => {
112
- const srcPath = path.resolve('/app/src/commands');
113
- const distPath = path.resolve('/app/dist/src/commands');
114
+ const dirs = discoverCommandDirectories(cwd);
115
+ // Should not crash
116
+ expect(dirs).toHaveLength(1);
117
+ expect(dirs).toContain(path.resolve('/app/src/commands'));
118
+ });
119
+
120
+ it('should deduplicate dist and src core commands', () => {
121
+ // const srcPath = path.resolve('/app/src/commands');
114
122
 
115
- // First we add distPath (manually simulate index.ts adding it to visited if we could,
116
- // but here we test the internal visited set of discoverCommandDirectories for multiple calls if we used it that way,
117
- // or rather we test how it handles its OWN loops.
118
- // Actually discoverCommandDirectories doesn't see distPath unless we add it to its loops.
123
+ // First we add distPath (manually simulate index.ts adding it to visited if we could,
124
+ // but here we test the internal visited set of discoverCommandDirectories for multiple calls if we used it that way,
125
+ // or rather we test how it handles its OWN loops.
126
+ // Actually discoverCommandDirectories doesn't see distPath unless we add it to its loops.
119
127
 
120
- // Let's test if it skips src/commands if it SHOULD.
121
- // Wait, the new logic in discovery.ts skips src/commands if dist/src/commands is in visited.
122
- // So we need to simulate adding dist/src/commands first.
128
+ // Let's test if it skips src/commands if it SHOULD.
129
+ // Wait, the new logic in discovery.ts skips src/commands if dist/src/commands is in visited.
130
+ // So we need to simulate adding dist/src/commands first.
123
131
 
124
- // Actually my new logic in discovery.ts DOES NOT scan for dist/src/commands automatically.
125
- // It relies on index.ts adding it, OR if it's found in a module.
132
+ // Actually my new logic in discovery.ts DOES NOT scan for dist/src/commands automatically.
133
+ // It relies on index.ts adding it, OR if it's found in a module.
126
134
 
127
- // Let's test the deduplication logic in addDir specifically if we can.
128
- // I'll add a test case that calls it twice conceptually.
135
+ // Let's test the deduplication logic in addDir specifically if we can.
136
+ // I'll add a test case that calls it twice conceptually.
129
137
 
130
- // Wait, discovery.ts:
131
- /*
138
+ // Wait, discovery.ts:
139
+ /*
132
140
  const isSrc = resolved.endsWith(path.join('src', 'commands'));
133
141
  if (isSrc) {
134
142
  const distEquivalent = resolved.replace(path.sep + 'src' + path.sep, path.sep + 'dist' + path.sep + 'src' + path.sep);
@@ -136,41 +144,46 @@ describe('discoverCommandDirectories', () => {
136
144
  }
137
145
  */
138
146
 
139
- // Implementation check:
140
- vi.mocked(fs.existsSync).mockReturnValue(true);
141
- vi.mocked(fs.readdirSync).mockReturnValue([]);
147
+ // Implementation check:
148
+ vi.mocked(fs.existsSync).mockReturnValue(true);
149
+ vi.mocked(fs.readdirSync).mockReturnValue([]);
142
150
 
143
- // Since we can't easily control 'visited' from outside, we trust the logic.
144
- // But we can verify it doesn't return BOTH if they resolve to same thing (already handled by visited.has(resolved)).
145
- });
146
-
147
- it('should ignore duplicate paths', () => {
148
- const corePath = path.resolve('/app/src/commands');
149
-
150
- vi.mocked(fs.existsSync).mockImplementation((p: any) => {
151
- return p === corePath;
152
- });
151
+ // Since we can't easily control 'visited' from outside, we trust the logic.
152
+ // But we can verify it doesn't return BOTH if they resolve to same thing (already handled by visited.has(resolved)).
153
+ });
153
154
 
154
- const dirs = discoverCommandDirectories(cwd);
155
+ it('should ignore duplicate paths', () => {
156
+ const corePath = path.resolve('/app/src/commands');
155
157
 
156
- expect(dirs).toContain(corePath);
157
- expect(dirs).toHaveLength(1);
158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
+ vi.mocked(fs.existsSync).mockImplementation((p: any) => {
160
+ return p === corePath;
158
161
  });
159
162
 
160
- it('should ignore files in modules directory', () => {
161
- vi.mocked(fs.existsSync).mockReturnValue(true);
162
- vi.mocked(fs.readdirSync).mockReturnValue(['mod1', 'file.txt'] as any);
163
- vi.mocked(fs.statSync).mockImplementation((p: any) => {
164
- if (typeof p === 'string' && p.endsWith('file.txt')) {
165
- return { isDirectory: () => false } as any;
166
- }
167
- return { isDirectory: () => true } as any;
168
- });
169
-
170
- const dirs = discoverCommandDirectories(cwd);
171
- // Should process mod1, ignore file.txt
172
- // The logic prefers dist/src/commands if it exists, and our mock returns true for all existsSync
173
- expect(dirs).toContain(path.resolve('/app/modules/mod1/dist/src/commands'));
174
- expect(dirs).not.toContain(path.resolve('/app/modules/file.txt/src/commands'));
163
+ const dirs = discoverCommandDirectories(cwd);
164
+
165
+ expect(dirs).toContain(corePath);
166
+ expect(dirs).toHaveLength(1);
167
+ });
168
+
169
+ it('should ignore files in modules directory', () => {
170
+ vi.mocked(fs.existsSync).mockReturnValue(true);
171
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
172
+ vi.mocked(fs.readdirSync).mockReturnValue(['mod1', 'file.txt'] as any);
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ vi.mocked(fs.statSync).mockImplementation((p: any) => {
175
+ if (typeof p === 'string' && p.endsWith('file.txt')) {
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ return { isDirectory: () => false } as any;
178
+ }
179
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
+ return { isDirectory: () => true } as any;
175
181
  });
182
+
183
+ const dirs = discoverCommandDirectories(cwd);
184
+ // Should process mod1, ignore file.txt
185
+ // The logic prefers dist/src/commands if it exists, and our mock returns true for all existsSync
186
+ expect(dirs).toContain(path.resolve('/app/modules/mod1/dist/src/commands'));
187
+ expect(dirs).not.toContain(path.resolve('/app/modules/file.txt/src/commands'));
188
+ });
176
189
  });