@nexical/cli 0.10.0 → 0.11.1

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 (76) 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 +199 -0
  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 +15 -10
  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 +27 -16
  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.d.ts +8 -0
  33. package/dist/src/commands/setup.js +75 -0
  34. package/dist/src/commands/setup.js.map +1 -0
  35. package/dist/src/utils/discovery.js +1 -1
  36. package/dist/src/utils/git.js +1 -1
  37. package/dist/src/utils/url-resolver.js +1 -1
  38. package/eslint.config.mjs +67 -0
  39. package/index.ts +34 -20
  40. package/package.json +57 -33
  41. package/src/commands/init.ts +79 -75
  42. package/src/commands/module/add.ts +158 -148
  43. package/src/commands/module/list.ts +61 -50
  44. package/src/commands/module/remove.ts +59 -54
  45. package/src/commands/module/update.ts +44 -42
  46. package/src/commands/run.ts +89 -81
  47. package/src/commands/setup.ts +92 -0
  48. package/src/utils/discovery.ts +98 -113
  49. package/src/utils/git.ts +35 -28
  50. package/src/utils/url-resolver.ts +50 -45
  51. package/test/e2e/lifecycle.e2e.test.ts +139 -130
  52. package/test/integration/commands/init.integration.test.ts +64 -61
  53. package/test/integration/commands/module.integration.test.ts +122 -122
  54. package/test/integration/commands/run.integration.test.ts +70 -63
  55. package/test/integration/utils/command-loading.integration.test.ts +40 -53
  56. package/test/unit/commands/init.test.ts +163 -128
  57. package/test/unit/commands/module/add.test.ts +312 -245
  58. package/test/unit/commands/module/list.test.ts +108 -91
  59. package/test/unit/commands/module/remove.test.ts +74 -67
  60. package/test/unit/commands/module/update.test.ts +74 -70
  61. package/test/unit/commands/run.test.ts +253 -201
  62. package/test/unit/commands/setup.test.ts +187 -0
  63. package/test/unit/utils/command-discovery.test.ts +138 -125
  64. package/test/unit/utils/git.test.ts +135 -117
  65. package/test/unit/utils/integration-helpers.test.ts +59 -49
  66. package/test/unit/utils/url-resolver.test.ts +46 -34
  67. package/test/utils/integration-helpers.ts +36 -29
  68. package/tsconfig.json +15 -25
  69. package/tsup.config.ts +14 -14
  70. package/vitest.config.ts +10 -10
  71. package/vitest.e2e.config.ts +6 -6
  72. package/vitest.integration.config.ts +17 -17
  73. package/dist/chunk-JYASTIIW.js.map +0 -1
  74. package/dist/chunk-OKXOCNXP.js +0 -105
  75. package/dist/chunk-OKXOCNXP.js.map +0 -1
  76. package/dist/chunk-WKERTCM6.js.map +0 -1
@@ -0,0 +1,187 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import SetupCommand from '../../../src/commands/setup.js';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import { CLI } from '@nexical/cli-core';
6
+
7
+ // Mock fs-extra
8
+ vi.mock('fs-extra');
9
+
10
+ describe('SetupCommand', () => {
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;
76
+ });
77
+
78
+ await command.run();
79
+
80
+ expect(command.warn).toHaveBeenCalledWith('App directory backend not found. Skipping.');
81
+ expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
82
+ });
83
+
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;
92
+
93
+ // Shared assets in core exist
94
+ if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
95
+
96
+ return false;
97
+ });
98
+
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
101
+
102
+ await command.run();
103
+
104
+ // Check if verify apps are processed
105
+ expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
106
+ expect(command.info).toHaveBeenCalledWith('Setting up backend...');
107
+
108
+ // Check symlink calls
109
+ // We have 2 apps * 7 shared assets = 14 symlinks
110
+ // sharedAssets = ['prisma', 'src', 'public', 'locales', 'scripts', 'astro.config.mjs', 'tsconfig.json']
111
+
112
+ const assets = [
113
+ 'prisma',
114
+ 'src',
115
+ 'public',
116
+ 'locales',
117
+ 'scripts',
118
+ 'astro.config.mjs',
119
+ 'tsconfig.json',
120
+ ];
121
+
122
+ for (const app of ['frontend', 'backend']) {
123
+ for (const asset of assets) {
124
+ const dest = path.join('/mock/project/root', 'apps', app, asset);
125
+ // const source = path.join('/mock/project/root', 'core', asset);
126
+
127
+ // Ensure removeSync called
128
+ expect(fs.removeSync).toHaveBeenCalledWith(dest);
129
+
130
+ // Ensure symlink called
131
+ // valid relative path calculation might vary, but verify arguments
132
+ expect(fs.symlink).toHaveBeenCalled();
133
+ }
134
+ }
135
+
136
+ expect(command.success).toHaveBeenCalledWith('Application setup complete.');
137
+ });
138
+
139
+ it('should warn if source asset is missing in core', async () => {
140
+ // Setup fs mocks
141
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
142
+ const pStr = p.toString();
143
+ if (pStr.endsWith('core')) return true;
144
+ if (pStr.includes('apps/')) return true;
145
+
146
+ // Mock that 'prisma' is missing in core
147
+ if (pStr.endsWith('core/prisma')) return false;
148
+
149
+ // Others exist
150
+ if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
151
+
152
+ return false;
153
+ });
154
+
155
+ await command.run();
156
+
157
+ expect(command.warn).toHaveBeenCalledWith('Source asset prisma not found in core.');
158
+ });
159
+
160
+ it('should throw error if removal fails with non-ENOENT', async () => {
161
+ vi.mocked(fs.existsSync).mockReturnValue(true);
162
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
+ vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
164
+
165
+ const error = new Error('Permission denied');
166
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
+ (error as any).code = 'EACCES';
168
+ vi.mocked(fs.removeSync).mockImplementation(() => {
169
+ throw error;
170
+ });
171
+
172
+ await command.run();
173
+
174
+ expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
175
+ });
176
+
177
+ it('should log error if symlink fails', async () => {
178
+ vi.mocked(fs.existsSync).mockReturnValue(true);
179
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
+ vi.mocked(fs.lstatSync).mockReturnValue({ isSymbolicLink: () => true } as any);
181
+ vi.mocked(fs.symlink).mockRejectedValue(new Error('Symlink failed'));
182
+
183
+ await command.run();
184
+
185
+ expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
186
+ });
187
+ });
@@ -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
  });