@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.
- 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 +9 -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 +70 -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 +138 -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
|
@@ -1,262 +1,329 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runCommand } from '@nexical/cli-core';
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
3
|
import ModuleAddCommand from '../../../../src/commands/module/add.js';
|
|
4
4
|
import fs from 'fs-extra';
|
|
5
5
|
import * as git from '../../../../src/utils/git.js';
|
|
6
6
|
|
|
7
7
|
vi.mock('@nexical/cli-core', async (importOriginal) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
const mod = await importOriginal<typeof import('@nexical/cli-core')>();
|
|
9
|
+
return {
|
|
10
|
+
...mod,
|
|
11
|
+
logger: {
|
|
12
|
+
...mod.logger,
|
|
13
|
+
success: vi.fn(),
|
|
14
|
+
info: vi.fn(),
|
|
15
|
+
debug: vi.fn(),
|
|
16
|
+
error: vi.fn(),
|
|
17
|
+
warn: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
runCommand: vi.fn(),
|
|
20
|
+
};
|
|
21
21
|
});
|
|
22
22
|
vi.mock('fs-extra');
|
|
23
23
|
vi.mock('../../../../src/utils/git.js', () => ({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
clone: vi.fn(),
|
|
25
|
+
updateSubmodules: vi.fn(),
|
|
26
|
+
checkoutOrphan: vi.fn(),
|
|
27
|
+
addAll: vi.fn(),
|
|
28
|
+
commit: vi.fn(),
|
|
29
|
+
deleteBranch: vi.fn(),
|
|
30
|
+
renameBranch: vi.fn(),
|
|
31
|
+
removeRemote: vi.fn(),
|
|
32
|
+
branchExists: vi.fn(),
|
|
33
|
+
renameRemote: vi.fn(),
|
|
34
|
+
getRemoteUrl: vi.fn(),
|
|
35
35
|
}));
|
|
36
36
|
|
|
37
37
|
describe('ModuleAddCommand', () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Mock git default behaviors
|
|
57
|
-
vi.mocked(git.clone).mockResolvedValue(undefined as any);
|
|
58
|
-
vi.mocked(git.getRemoteUrl).mockResolvedValue('' as any);
|
|
59
|
-
|
|
60
|
-
// Force project root
|
|
61
|
-
await command.init();
|
|
62
|
-
(command as any).projectRoot = '/mock/root';
|
|
38
|
+
let command: ModuleAddCommand;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
command = new ModuleAddCommand({}, { rootDir: '/mock/root' });
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
vi.spyOn(command, 'error').mockImplementation((() => {}) as any);
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
vi.spyOn(command, 'success').mockImplementation((() => {}) as any);
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
vi.spyOn(command, 'info').mockImplementation((() => {}) as any);
|
|
49
|
+
|
|
50
|
+
// Setup mocks
|
|
51
|
+
vi.mocked(fs.ensureDir).mockImplementation(async () => {});
|
|
52
|
+
vi.mocked(fs.remove).mockImplementation(async () => {});
|
|
53
|
+
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
|
|
54
|
+
// We don't rely on this for init anymore since we force projectRoot
|
|
55
|
+
return false;
|
|
63
56
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
vi.mocked(fs.readFile).mockResolvedValue('name: test-module\n' as any);
|
|
59
|
+
|
|
60
|
+
// Mock git default behaviors
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
vi.mocked(git.clone).mockResolvedValue(undefined as any);
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
vi.mocked(git.getRemoteUrl).mockResolvedValue('' as any);
|
|
65
|
+
|
|
66
|
+
// Force project root
|
|
67
|
+
await command.init();
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
(command as any).projectRoot = '/mock/root';
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
vi.resetAllMocks();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should have correct static properties', () => {
|
|
77
|
+
expect(ModuleAddCommand.usage).toContain('module add');
|
|
78
|
+
expect(ModuleAddCommand.description).toBeDefined();
|
|
79
|
+
expect(ModuleAddCommand.requiresProject).toBe(true);
|
|
80
|
+
expect(ModuleAddCommand.args).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should error if project root is missing', async () => {
|
|
84
|
+
command = new ModuleAddCommand({}, { rootDir: undefined });
|
|
85
|
+
vi.spyOn(command, 'error').mockImplementation(() => {});
|
|
86
|
+
// Ensure init doesn't set it (mocked in beforeEach but this constructor overrides logic?)
|
|
87
|
+
// In beforeEach, we call command.init() then set projectRoot.
|
|
88
|
+
// Here we just created new command.
|
|
89
|
+
vi.spyOn(command, 'init').mockImplementation(async () => {});
|
|
90
|
+
|
|
91
|
+
await command.runInit({ url: 'arg' });
|
|
92
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
93
|
+
expect.stringContaining('requires to be run within an app project'),
|
|
94
|
+
1,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle gh@ syntax with .git suffix', async () => {
|
|
99
|
+
// We mock fs.pathExists to return false for targetDir to trigger install
|
|
100
|
+
// return true for stagingDir to simulate clone success
|
|
101
|
+
// return true for module.yaml check
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
vi.mocked(fs.pathExists).mockResolvedValue(false as any); // Default
|
|
104
|
+
|
|
105
|
+
await command.run({ url: 'gh@org/repo.git' }); // With .git suffix
|
|
106
|
+
|
|
107
|
+
// Should NOT append another .git
|
|
108
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
109
|
+
'https://github.com/org/repo.git',
|
|
110
|
+
expect.any(String),
|
|
111
|
+
expect.objectContaining({ depth: 1 }),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should error if url is missing', async () => {
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
+
await command.run({ url: undefined as any });
|
|
118
|
+
expect(command.error).toHaveBeenCalledWith('Please specify a repository URL.');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should error if module.yaml is missing', async () => {
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
|
+
vi.mocked(fs.pathExists).mockResolvedValue(false as any); // No yaml found
|
|
124
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
125
|
+
expect(command.error).toHaveBeenCalledWith(expect.stringContaining('No module.yaml found'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should error if module.yaml is missing in subdirectory', async () => {
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
130
|
+
vi.mocked(fs.pathExists).mockResolvedValue(false as any); // No yaml
|
|
131
|
+
|
|
132
|
+
// We mocked fs.pathExists to return false for everything in this test setup unless selective
|
|
133
|
+
// But the run() logic:
|
|
134
|
+
// await clone(...)
|
|
135
|
+
// if (subPath) ...
|
|
136
|
+
// if (!exists) throw
|
|
137
|
+
|
|
138
|
+
await command.run({ url: 'https://github.com/org/repo.git//subdir' });
|
|
139
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
140
|
+
expect.stringContaining('No module.yaml found in https://github.com/org/repo.git//subdir'),
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should error if name is missing in module.yaml', async () => {
|
|
145
|
+
vi.mocked(fs.pathExists)
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
|
+
.mockResolvedValueOnce(false as any)
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
.mockResolvedValueOnce(true as any);
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('dependencies: []' as any); // No name
|
|
152
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
153
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
154
|
+
expect.stringContaining("missing 'name' in module.yaml"),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should handle generic errors during install', async () => {
|
|
159
|
+
vi.mocked(git.clone).mockRejectedValue(new Error('Clone failed'));
|
|
160
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
161
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
162
|
+
expect.stringContaining('Failed to add module: Clone failed'),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should install a module using git submodule add', async () => {
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
168
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(false as any); // Staging yaml
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
170
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(true as any); // module.yaml in staging exists
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('name: my-module\n' as any);
|
|
173
|
+
|
|
174
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
175
|
+
|
|
176
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
177
|
+
'https://github.com/org/repo.git',
|
|
178
|
+
expect.stringContaining('staging-'),
|
|
179
|
+
{ depth: 1 },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Should use submodule add
|
|
183
|
+
expect(runCommand).toHaveBeenCalledWith(
|
|
184
|
+
expect.stringContaining(
|
|
185
|
+
'git submodule add https://github.com/org/repo.git modules/my-module',
|
|
186
|
+
),
|
|
187
|
+
'/mock/root',
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
|
|
191
|
+
expect(command.success).toHaveBeenCalledWith('All modules installed successfully.');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should recursively install dependencies', async () => {
|
|
195
|
+
// First module calls
|
|
196
|
+
vi.mocked(fs.pathExists)
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
198
|
+
.mockResolvedValueOnce(false as any)
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
200
|
+
.mockResolvedValueOnce(true as any) // module 1 yaml exists
|
|
201
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
202
|
+
.mockResolvedValueOnce(false as any) // target dir check
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
204
|
+
.mockResolvedValueOnce(false as any) // target dir check
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
206
|
+
.mockResolvedValueOnce(false as any)
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
+
.mockResolvedValueOnce(true as any) // module 2 yaml exists
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
+
.mockResolvedValueOnce(false as any); // target dir check
|
|
211
|
+
|
|
212
|
+
vi.mocked(fs.readFile)
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
214
|
+
.mockResolvedValueOnce('name: parent\ndependencies:\n - gh@org/child' as any)
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
+
.mockResolvedValueOnce('name: child' as any);
|
|
217
|
+
|
|
218
|
+
await command.run({ url: 'gh@org/parent' });
|
|
219
|
+
|
|
220
|
+
// Should clone parent
|
|
221
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
222
|
+
expect.stringContaining('parent.git'),
|
|
223
|
+
expect.anything(),
|
|
224
|
+
expect.anything(),
|
|
225
|
+
);
|
|
226
|
+
// Should clone child
|
|
227
|
+
expect(git.clone).toHaveBeenCalledWith(
|
|
228
|
+
expect.stringContaining('child.git'),
|
|
229
|
+
expect.anything(),
|
|
230
|
+
expect.anything(),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(runCommand).toHaveBeenCalledTimes(3); // 2 submodules + npm install
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle object-style dependencies', async () => {
|
|
237
|
+
vi.mocked(fs.pathExists)
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
239
|
+
.mockResolvedValueOnce(false as any)
|
|
240
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
241
|
+
.mockResolvedValueOnce(true as any) // module exists
|
|
242
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
243
|
+
.mockResolvedValueOnce(false as any); // target dir check
|
|
244
|
+
|
|
245
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
+
'name: parent\ndependencies:\n gh@org/child: main' as any,
|
|
248
|
+
); // Object style
|
|
249
|
+
|
|
250
|
+
await command.run({ url: 'gh@org/parent' });
|
|
251
|
+
|
|
252
|
+
expect(git.clone).toHaveBeenCalledTimes(2); // parent + child
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should detect conflicts (installed but different origin)', async () => {
|
|
256
|
+
vi.mocked(fs.pathExists)
|
|
257
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
258
|
+
.mockResolvedValueOnce(false as any)
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
|
+
.mockResolvedValueOnce(true as any); // staging yaml
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
262
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('name: conflict-mod' as any);
|
|
263
|
+
|
|
264
|
+
// Target dir check returns true (exists)
|
|
265
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
266
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(true as any);
|
|
267
|
+
|
|
268
|
+
// Origin check returns different URL
|
|
269
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
270
|
+
vi.mocked(git.getRemoteUrl).mockResolvedValueOnce('https://other.com/repo.git' as any);
|
|
271
|
+
|
|
272
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
273
|
+
|
|
274
|
+
expect(command.error).toHaveBeenCalledWith(
|
|
275
|
+
expect.stringContaining("Dependency Conflict! Module 'conflict-mod' exists but remote"),
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should skip installation if same module/origin already exists', async () => {
|
|
280
|
+
vi.mocked(fs.pathExists)
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
282
|
+
.mockResolvedValueOnce(false as any)
|
|
283
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
284
|
+
.mockResolvedValueOnce(true as any); // staging yaml
|
|
285
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
286
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('name: existing-mod' as any);
|
|
287
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
288
|
+
vi.mocked(fs.pathExists).mockResolvedValueOnce(true as any); // Exists
|
|
289
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
290
|
+
vi.mocked(git.getRemoteUrl).mockResolvedValueOnce('https://github.com/org/repo.git' as any); // Same URL
|
|
291
|
+
|
|
292
|
+
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
293
|
+
|
|
294
|
+
expect(command.info).toHaveBeenCalledWith('Module existing-mod already installed.');
|
|
295
|
+
// Should NOT call submodule add
|
|
296
|
+
expect(runCommand).not.toHaveBeenCalledWith(
|
|
297
|
+
expect.stringContaining('git submodule add'),
|
|
298
|
+
expect.anything(),
|
|
299
|
+
);
|
|
300
|
+
// But SHOULD call npm install at end
|
|
301
|
+
expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should handle circular dependencies', async () => {
|
|
305
|
+
// Module A depends on B, B depends on A
|
|
306
|
+
// A
|
|
307
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
308
|
+
vi.mocked(fs.pathExists).mockResolvedValue(true as any); // Simplify pathExists to always true for yamls
|
|
309
|
+
vi.mocked(fs.readFile)
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
311
|
+
.mockResolvedValueOnce('name: mod-a\ndependencies:\n - gh@org/mod-b' as any)
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
313
|
+
.mockResolvedValueOnce('name: mod-b\ndependencies:\n - gh@org/mod-a' as any);
|
|
314
|
+
|
|
315
|
+
// Target dir checks (false = not installed)
|
|
316
|
+
// We need to carefully mock pathExists sequence or use implementation based on path
|
|
317
|
+
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
|
|
318
|
+
if (p.includes('modules')) return false; // Not installed yet
|
|
319
|
+
return true; // Yaml exists
|
|
205
320
|
});
|
|
206
321
|
|
|
207
|
-
|
|
208
|
-
vi.mocked(fs.pathExists).mockResolvedValueOnce(false as any).mockResolvedValueOnce(true as any); // staging yaml
|
|
209
|
-
vi.mocked(fs.readFile).mockResolvedValueOnce('name: conflict-mod' as any);
|
|
322
|
+
await command.run({ url: 'gh@org/mod-a' });
|
|
210
323
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
218
|
-
|
|
219
|
-
expect(command.error).toHaveBeenCalledWith(
|
|
220
|
-
expect.stringContaining('Dependency Conflict! Module \'conflict-mod\' exists but remote')
|
|
221
|
-
);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('should skip installation if same module/origin already exists', async () => {
|
|
225
|
-
vi.mocked(fs.pathExists).mockResolvedValueOnce(false as any).mockResolvedValueOnce(true as any); // staging yaml
|
|
226
|
-
vi.mocked(fs.readFile).mockResolvedValueOnce('name: existing-mod' as any);
|
|
227
|
-
vi.mocked(fs.pathExists).mockResolvedValueOnce(true as any); // Exists
|
|
228
|
-
vi.mocked(git.getRemoteUrl).mockResolvedValueOnce('https://github.com/org/repo.git' as any); // Same URL
|
|
229
|
-
|
|
230
|
-
await command.run({ url: 'https://github.com/org/repo.git' });
|
|
231
|
-
|
|
232
|
-
expect(command.info).toHaveBeenCalledWith('Module existing-mod already installed.');
|
|
233
|
-
// Should NOT call submodule add
|
|
234
|
-
expect(runCommand).not.toHaveBeenCalledWith(expect.stringContaining('git submodule add'), expect.anything());
|
|
235
|
-
// But SHOULD call npm install at end
|
|
236
|
-
expect(runCommand).toHaveBeenCalledWith('npm install', '/mock/root');
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('should handle circular dependencies', async () => {
|
|
240
|
-
// Module A depends on B, B depends on A
|
|
241
|
-
// A
|
|
242
|
-
vi.mocked(fs.pathExists).mockResolvedValue(true as any); // Simplify pathExists to always true for yamls
|
|
243
|
-
vi.mocked(fs.readFile)
|
|
244
|
-
.mockResolvedValueOnce('name: mod-a\ndependencies:\n - gh@org/mod-b' as any)
|
|
245
|
-
.mockResolvedValueOnce('name: mod-b\ndependencies:\n - gh@org/mod-a' as any);
|
|
246
|
-
|
|
247
|
-
// Target dir checks (false = not installed)
|
|
248
|
-
// We need to carefully mock pathExists sequence or use implementation based on path
|
|
249
|
-
vi.mocked(fs.pathExists).mockImplementation(async (p: string) => {
|
|
250
|
-
if (p.includes('modules')) return false; // Not installed yet
|
|
251
|
-
return true; // Yaml exists
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
await command.run({ url: 'gh@org/mod-a' });
|
|
255
|
-
|
|
256
|
-
// Should install A and B, then see A again and skip
|
|
257
|
-
expect(git.clone).toHaveBeenCalledTimes(2);
|
|
258
|
-
// Should succeed
|
|
259
|
-
expect(command.success).toHaveBeenCalled();
|
|
260
|
-
});
|
|
324
|
+
// Should install A and B, then see A again and skip
|
|
325
|
+
expect(git.clone).toHaveBeenCalledTimes(2);
|
|
326
|
+
// Should succeed
|
|
327
|
+
expect(command.success).toHaveBeenCalled();
|
|
328
|
+
});
|
|
261
329
|
});
|
|
262
|
-
|