@nexical/cli 0.11.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.
- package/.github/workflows/deploy.yml +1 -1
- package/.husky/pre-commit +1 -0
- package/.prettierignore +8 -0
- package/.prettierrc +7 -0
- package/GEMINI.md +36 -30
- package/README.md +85 -56
- package/dist/chunk-AC4B3HPJ.js +93 -0
- package/dist/chunk-AC4B3HPJ.js.map +1 -0
- package/dist/{chunk-JYASTIIW.js → chunk-PJIOCW2A.js} +1 -1
- package/dist/chunk-PJIOCW2A.js.map +1 -0
- package/dist/{chunk-WKERTCM6.js → chunk-Q7YLW5HJ.js} +5 -2
- package/dist/chunk-Q7YLW5HJ.js.map +1 -0
- package/dist/index.js +41 -12
- package/dist/index.js.map +1 -1
- package/dist/src/commands/init.d.ts +4 -1
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/module/add.d.ts +3 -1
- package/dist/src/commands/module/add.js +24 -13
- package/dist/src/commands/module/add.js.map +1 -1
- package/dist/src/commands/module/list.js +9 -5
- package/dist/src/commands/module/list.js.map +1 -1
- package/dist/src/commands/module/remove.d.ts +3 -1
- package/dist/src/commands/module/remove.js +13 -7
- package/dist/src/commands/module/remove.js.map +1 -1
- package/dist/src/commands/module/update.d.ts +3 -1
- package/dist/src/commands/module/update.js +7 -5
- package/dist/src/commands/module/update.js.map +1 -1
- package/dist/src/commands/run.d.ts +4 -1
- package/dist/src/commands/run.js +10 -2
- package/dist/src/commands/run.js.map +1 -1
- package/dist/src/commands/setup.js +17 -4
- package/dist/src/commands/setup.js.map +1 -1
- package/dist/src/utils/discovery.js +1 -1
- package/dist/src/utils/git.js +1 -1
- package/dist/src/utils/url-resolver.js +1 -1
- package/eslint.config.mjs +67 -0
- package/index.ts +34 -20
- package/package.json +56 -32
- package/src/commands/init.ts +79 -76
- package/src/commands/module/add.ts +158 -148
- package/src/commands/module/list.ts +61 -50
- package/src/commands/module/remove.ts +59 -54
- package/src/commands/module/update.ts +44 -42
- package/src/commands/run.ts +89 -81
- package/src/commands/setup.ts +78 -60
- package/src/utils/discovery.ts +98 -113
- package/src/utils/git.ts +35 -28
- package/src/utils/url-resolver.ts +50 -45
- package/test/e2e/lifecycle.e2e.test.ts +139 -131
- package/test/integration/commands/init.integration.test.ts +64 -64
- package/test/integration/commands/module.integration.test.ts +122 -122
- package/test/integration/commands/run.integration.test.ts +70 -63
- package/test/integration/utils/command-loading.integration.test.ts +40 -53
- package/test/unit/commands/init.test.ts +163 -128
- package/test/unit/commands/module/add.test.ts +312 -245
- package/test/unit/commands/module/list.test.ts +108 -91
- package/test/unit/commands/module/remove.test.ts +74 -67
- package/test/unit/commands/module/update.test.ts +74 -70
- package/test/unit/commands/run.test.ts +253 -201
- package/test/unit/commands/setup.test.ts +146 -128
- package/test/unit/utils/command-discovery.test.ts +138 -125
- package/test/unit/utils/git.test.ts +135 -117
- package/test/unit/utils/integration-helpers.test.ts +59 -49
- package/test/unit/utils/url-resolver.test.ts +46 -34
- package/test/utils/integration-helpers.ts +36 -29
- package/tsconfig.json +15 -25
- package/tsup.config.ts +14 -14
- package/vitest.config.ts +10 -10
- package/vitest.e2e.config.ts +6 -6
- package/vitest.integration.config.ts +17 -17
- package/dist/chunk-JYASTIIW.js.map +0 -1
- package/dist/chunk-OKXOCNXP.js +0 -105
- package/dist/chunk-OKXOCNXP.js.map +0 -1
- package/dist/chunk-WKERTCM6.js.map +0 -1
|
@@ -8,162 +8,180 @@ import { CLI } from '@nexical/cli-core';
|
|
|
8
8
|
vi.mock('fs-extra');
|
|
9
9
|
|
|
10
10
|
describe('SetupCommand', () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
expect(command.warn).toHaveBeenCalledWith('App directory backend not found. Skipping.');
|
|
81
|
+
expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
|
|
82
|
+
});
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
93
|
+
// Shared assets in core exist
|
|
94
|
+
if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
expect(command.info).toHaveBeenCalledWith('Setting up frontend...');
|
|
96
|
+
return false;
|
|
78
97
|
});
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
93
|
-
});
|
|
102
|
+
await command.run();
|
|
94
103
|
|
|
95
|
-
|
|
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
|
-
|
|
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']
|
|
98
111
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
const assets = [
|
|
113
|
+
'prisma',
|
|
114
|
+
'src',
|
|
115
|
+
'public',
|
|
116
|
+
'locales',
|
|
117
|
+
'scripts',
|
|
118
|
+
'astro.config.mjs',
|
|
119
|
+
'tsconfig.json',
|
|
120
|
+
];
|
|
102
121
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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);
|
|
106
126
|
|
|
107
|
-
|
|
127
|
+
// Ensure removeSync called
|
|
128
|
+
expect(fs.removeSync).toHaveBeenCalledWith(dest);
|
|
108
129
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
// Ensure symlink called
|
|
131
|
+
// valid relative path calculation might vary, but verify arguments
|
|
132
|
+
expect(fs.symlink).toHaveBeenCalled();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
113
135
|
|
|
114
|
-
|
|
115
|
-
|
|
136
|
+
expect(command.success).toHaveBeenCalledWith('Application setup complete.');
|
|
137
|
+
});
|
|
116
138
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
122
145
|
|
|
123
|
-
|
|
124
|
-
|
|
146
|
+
// Mock that 'prisma' is missing in core
|
|
147
|
+
if (pStr.endsWith('core/prisma')) return false;
|
|
125
148
|
|
|
126
|
-
|
|
127
|
-
|
|
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;
|
|
149
|
+
// Others exist
|
|
150
|
+
if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
|
|
132
151
|
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
return false;
|
|
153
|
+
});
|
|
135
154
|
|
|
136
|
-
|
|
137
|
-
if (pStr.includes('core/') && !pStr.endsWith('core')) return true;
|
|
155
|
+
await command.run();
|
|
138
156
|
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
expect(command.warn).toHaveBeenCalledWith('Source asset prisma not found in core.');
|
|
158
|
+
});
|
|
141
159
|
|
|
142
|
-
|
|
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);
|
|
143
164
|
|
|
144
|
-
|
|
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;
|
|
145
170
|
});
|
|
146
171
|
|
|
147
|
-
|
|
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; });
|
|
172
|
+
await command.run();
|
|
154
173
|
|
|
155
|
-
|
|
174
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
|
|
175
|
+
});
|
|
156
176
|
|
|
157
|
-
|
|
158
|
-
|
|
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'));
|
|
159
182
|
|
|
160
|
-
|
|
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'));
|
|
183
|
+
await command.run();
|
|
164
184
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
|
|
168
|
-
});
|
|
185
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('Failed to symlink'));
|
|
186
|
+
});
|
|
169
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
72
|
-
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
78
|
+
const dirs = discoverCommandDirectories(cwd);
|
|
73
79
|
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
|
|
98
|
+
const dirs = discoverCommandDirectories(cwd);
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
expect(dirs).toContain(path.resolve('/app/src/modules/mod-src/src/commands'));
|
|
101
|
+
});
|
|
92
102
|
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
// Implementation check:
|
|
148
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
149
|
+
vi.mocked(fs.readdirSync).mockReturnValue([]);
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
155
|
+
it('should ignore duplicate paths', () => {
|
|
156
|
+
const corePath = path.resolve('/app/src/commands');
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
});
|