@nexical/cli 0.11.8 → 0.11.10

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 (46) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/commands/deploy.d.ts +2 -0
  4. package/dist/src/commands/deploy.js +3 -3
  5. package/dist/src/commands/deploy.js.map +1 -1
  6. package/dist/src/commands/init.js +3 -3
  7. package/dist/src/commands/module/add.js +53 -22
  8. package/dist/src/commands/module/add.js.map +1 -1
  9. package/dist/src/commands/module/list.d.ts +1 -0
  10. package/dist/src/commands/module/list.js +54 -45
  11. package/dist/src/commands/module/list.js.map +1 -1
  12. package/dist/src/commands/module/remove.js +37 -12
  13. package/dist/src/commands/module/remove.js.map +1 -1
  14. package/dist/src/commands/module/update.js +15 -3
  15. package/dist/src/commands/module/update.js.map +1 -1
  16. package/dist/src/commands/run.js +18 -1
  17. package/dist/src/commands/run.js.map +1 -1
  18. package/package.json +2 -2
  19. package/src/commands/deploy.ts +3 -3
  20. package/src/commands/module/add.ts +74 -31
  21. package/src/commands/module/list.ts +80 -57
  22. package/src/commands/module/remove.ts +50 -14
  23. package/src/commands/module/update.ts +19 -5
  24. package/src/commands/run.ts +21 -1
  25. package/test/e2e/lifecycle.e2e.test.ts +3 -2
  26. package/test/integration/commands/deploy.integration.test.ts +102 -0
  27. package/test/integration/commands/init.integration.test.ts +16 -1
  28. package/test/integration/commands/module.integration.test.ts +81 -55
  29. package/test/integration/commands/run.integration.test.ts +69 -74
  30. package/test/integration/commands/setup.integration.test.ts +53 -0
  31. package/test/unit/commands/deploy.test.ts +285 -0
  32. package/test/unit/commands/init.test.ts +15 -0
  33. package/test/unit/commands/module/add.test.ts +363 -254
  34. package/test/unit/commands/module/list.test.ts +100 -99
  35. package/test/unit/commands/module/remove.test.ts +143 -58
  36. package/test/unit/commands/module/update.test.ts +45 -62
  37. package/test/unit/commands/run.test.ts +16 -1
  38. package/test/unit/commands/setup.test.ts +25 -66
  39. package/test/unit/deploy/config-manager.test.ts +65 -0
  40. package/test/unit/deploy/providers/cloudflare.test.ts +210 -0
  41. package/test/unit/deploy/providers/github.test.ts +139 -0
  42. package/test/unit/deploy/providers/railway.test.ts +328 -0
  43. package/test/unit/deploy/registry.test.ts +227 -0
  44. package/test/unit/deploy/utils.test.ts +30 -0
  45. package/test/unit/utils/command-discovery.test.ts +145 -142
  46. package/test/unit/utils/git_utils.test.ts +49 -0
@@ -0,0 +1,285 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import dotenv from 'dotenv';
3
+ import DeployCommand from '../../../src/commands/deploy.js';
4
+
5
+ // Fully mock the modules to return classes
6
+ vi.mock('../../../src/deploy/config-manager.js', () => {
7
+ return {
8
+ ConfigManager: vi.fn().mockImplementation(function () {
9
+ return {
10
+ load: vi.fn().mockResolvedValue({
11
+ deploy: {
12
+ backend: { provider: 'railway' },
13
+ frontend: { provider: 'cloudflare' },
14
+ repository: { provider: 'github' },
15
+ },
16
+ }),
17
+ };
18
+ }),
19
+ };
20
+ });
21
+
22
+ vi.mock('../../../src/deploy/registry.js', () => {
23
+ return {
24
+ ProviderRegistry: vi.fn().mockImplementation(function () {
25
+ return {
26
+ loadCoreProviders: vi.fn(),
27
+ loadLocalProviders: vi.fn(),
28
+ getDeploymentProvider: vi.fn(),
29
+ getRepositoryProvider: vi.fn(),
30
+ };
31
+ }),
32
+ };
33
+ });
34
+
35
+ vi.mock('dotenv');
36
+ vi.mock('@nexical/cli-core', async (importOriginal) => {
37
+ const mod = await importOriginal<typeof import('@nexical/cli-core')>();
38
+ return {
39
+ ...mod,
40
+ logger: {
41
+ info: vi.fn(),
42
+ error: vi.fn(),
43
+ success: vi.fn(),
44
+ warn: vi.fn(),
45
+ debug: vi.fn(),
46
+ },
47
+ };
48
+ });
49
+
50
+ // Import them after vi.mock to get the mocked versions
51
+ import { ConfigManager } from '../../../src/deploy/config-manager.js';
52
+ import { ProviderRegistry } from '../../../src/deploy/registry.js';
53
+
54
+ describe('DeployCommand', () => {
55
+ let command: DeployCommand;
56
+ let mockRegistry: {
57
+ loadCoreProviders: any;
58
+ loadLocalProviders: any;
59
+ getDeploymentProvider: any;
60
+ getRepositoryProvider: any;
61
+ };
62
+ let mockConfigManager: { load: any };
63
+
64
+ beforeEach(() => {
65
+ vi.clearAllMocks();
66
+
67
+ command = new DeployCommand({} as unknown as any, { rootDir: '/mock/root' });
68
+ (command as unknown as { projectRoot: string }).projectRoot = '/mock/root';
69
+
70
+ // Get the instance that will be returned by the constructor
71
+ mockConfigManager = {
72
+ load: vi.fn().mockResolvedValue({
73
+ deploy: {
74
+ backend: { provider: 'railway' },
75
+ frontend: { provider: 'cloudflare' },
76
+ repository: { provider: 'github' },
77
+ },
78
+ }),
79
+ };
80
+ vi.mocked(ConfigManager).mockImplementation(function () {
81
+ return mockConfigManager;
82
+ });
83
+
84
+ mockRegistry = {
85
+ loadCoreProviders: vi.fn(),
86
+ loadLocalProviders: vi.fn(),
87
+ getDeploymentProvider: vi.fn(),
88
+ getRepositoryProvider: vi.fn(),
89
+ };
90
+ vi.mocked(ProviderRegistry).mockImplementation(function () {
91
+ return mockRegistry;
92
+ });
93
+
94
+ vi.spyOn(command, 'info').mockImplementation(() => {});
95
+ vi.spyOn(command, 'error').mockImplementation(() => {
96
+ throw new Error('CLI ERROR');
97
+ });
98
+ vi.spyOn(command, 'success').mockImplementation(() => {});
99
+ });
100
+
101
+ afterEach(() => {
102
+ vi.restoreAllMocks();
103
+ });
104
+
105
+ it('should run a full deployment successfully', async () => {
106
+ const mockBackend = {
107
+ name: 'railway',
108
+ provision: vi.fn().mockResolvedValue(undefined),
109
+ getSecrets: vi.fn().mockResolvedValue({ B_SEC: 'val' }),
110
+ getVariables: vi.fn().mockResolvedValue({ B_VAR: 'val' }),
111
+ };
112
+ const mockFrontend = {
113
+ name: 'cloudflare',
114
+ provision: vi.fn().mockResolvedValue(undefined),
115
+ getSecrets: vi.fn().mockResolvedValue({ F_SEC: 'val' }),
116
+ getVariables: vi.fn().mockResolvedValue({ F_VAR: 'val' }),
117
+ };
118
+ const mockRepo = {
119
+ name: 'github',
120
+ configureSecrets: vi.fn().mockResolvedValue(undefined),
121
+ configureVariables: vi.fn().mockResolvedValue(undefined),
122
+ generateWorkflow: vi.fn().mockResolvedValue(undefined),
123
+ };
124
+
125
+ mockRegistry.getDeploymentProvider.mockImplementation((name: string) => {
126
+ if (name === 'railway') return mockBackend;
127
+ if (name === 'cloudflare') return mockFrontend;
128
+ return undefined;
129
+ });
130
+ mockRegistry.getRepositoryProvider.mockReturnValue(mockRepo);
131
+
132
+ await command.run({ env: 'production' });
133
+
134
+ expect(dotenv.config).toHaveBeenCalled();
135
+ expect(mockBackend.provision).toHaveBeenCalled();
136
+ expect(mockFrontend.provision).toHaveBeenCalled();
137
+ expect(mockRepo.configureSecrets).toHaveBeenCalled();
138
+ expect(mockRepo.configureVariables).toHaveBeenCalled();
139
+ expect(mockRepo.generateWorkflow).toHaveBeenCalled();
140
+ expect(command.success).toHaveBeenCalledWith('Deployment configuration complete!');
141
+ });
142
+
143
+ it('should error if backend provider is missing', async () => {
144
+ mockConfigManager.load.mockResolvedValue({ deploy: { frontend: { provider: 'cf' } } });
145
+ await expect(command.run({})).rejects.toThrow('CLI ERROR');
146
+ });
147
+
148
+ it('should error if frontend provider is missing', async () => {
149
+ mockConfigManager.load.mockResolvedValue({ deploy: { backend: { provider: 'rw' } } });
150
+ await expect(command.run({})).rejects.toThrow('CLI ERROR');
151
+ });
152
+
153
+ it('should error if repo provider is missing', async () => {
154
+ mockConfigManager.load.mockResolvedValue({
155
+ deploy: { backend: { provider: 'rw' }, frontend: { provider: 'cf' } },
156
+ });
157
+ await expect(command.run({})).rejects.toThrow('CLI ERROR');
158
+ });
159
+
160
+ it('should throw if provider is not found in registry', async () => {
161
+ mockRegistry.getDeploymentProvider.mockReturnValue(undefined);
162
+ await expect(command.run({ backend: 'unknown' })).rejects.toThrow(
163
+ "Backend provider 'unknown' not found.",
164
+ );
165
+ });
166
+
167
+ it('should handle errors during secret resolution', async () => {
168
+ const mockBackend = {
169
+ name: 'railway',
170
+ provision: vi.fn(),
171
+ getSecrets: vi.fn().mockRejectedValue(new Error('Secret fail')),
172
+ getVariables: vi.fn().mockResolvedValue({}),
173
+ };
174
+ mockRegistry.getDeploymentProvider.mockReturnValue(mockBackend);
175
+ mockRegistry.getRepositoryProvider.mockReturnValue({
176
+ configureSecrets: vi.fn(),
177
+ configureVariables: vi.fn(),
178
+ generateWorkflow: vi.fn(),
179
+ });
180
+
181
+ await expect(command.run({})).rejects.toThrow('CLI ERROR');
182
+ });
183
+
184
+ it('should handle non-Error exceptions during secret resolution', async () => {
185
+ const mockBackend = {
186
+ name: 'railway',
187
+ provision: vi.fn(),
188
+ getSecrets: vi.fn().mockRejectedValue('String error'),
189
+ getVariables: vi.fn().mockResolvedValue({}),
190
+ };
191
+ mockRegistry.getDeploymentProvider.mockReturnValue(mockBackend);
192
+ mockRegistry.getRepositoryProvider.mockReturnValue({
193
+ configureSecrets: vi.fn(),
194
+ configureVariables: vi.fn(),
195
+ generateWorkflow: vi.fn(),
196
+ });
197
+
198
+ await expect(command.run({})).rejects.toThrow('CLI ERROR');
199
+ });
200
+
201
+ it('should handle errors during variable resolution', async () => {
202
+ const mockBackend = {
203
+ name: 'railway',
204
+ provision: vi.fn(),
205
+ getSecrets: vi.fn().mockResolvedValue({}),
206
+ getVariables: vi.fn().mockRejectedValue(new Error('Var fail')),
207
+ };
208
+ mockRegistry.getDeploymentProvider.mockReturnValue(mockBackend);
209
+ mockRegistry.getRepositoryProvider.mockReturnValue({
210
+ configureSecrets: vi.fn(),
211
+ configureVariables: vi.fn(),
212
+ generateWorkflow: vi.fn(),
213
+ });
214
+
215
+ await expect(command.run({})).rejects.toThrow('CLI ERROR');
216
+ });
217
+
218
+ it('should handle non-Error exceptions during variable resolution', async () => {
219
+ const mockBackend = {
220
+ name: 'railway',
221
+ provision: vi.fn(),
222
+ getSecrets: vi.fn().mockResolvedValue({}),
223
+ getVariables: vi.fn().mockRejectedValue('String var fail'),
224
+ };
225
+ mockRegistry.getDeploymentProvider.mockReturnValue(mockBackend);
226
+ mockRegistry.getRepositoryProvider.mockReturnValue({
227
+ configureSecrets: vi.fn(),
228
+ configureVariables: vi.fn(),
229
+ generateWorkflow: vi.fn(),
230
+ });
231
+
232
+ await expect(command.run({})).rejects.toThrow('CLI ERROR');
233
+ });
234
+ it('should handle errors during frontend secret resolution', async () => {
235
+ const mockBackend = {
236
+ name: 'railway',
237
+ provision: vi.fn(),
238
+ getSecrets: vi.fn().mockResolvedValue({}),
239
+ getVariables: vi.fn().mockResolvedValue({}),
240
+ };
241
+ const mockFrontend = {
242
+ name: 'cloudflare',
243
+ provision: vi.fn(),
244
+ getSecrets: vi.fn().mockRejectedValue(new Error('Front secret fail')),
245
+ getVariables: vi.fn().mockResolvedValue({}),
246
+ };
247
+ mockRegistry.getDeploymentProvider.mockImplementation((name: string) => {
248
+ if (name === 'railway') return mockBackend;
249
+ if (name === 'cloudflare') return mockFrontend;
250
+ });
251
+ mockRegistry.getRepositoryProvider.mockReturnValue({
252
+ configureSecrets: vi.fn(),
253
+ configureVariables: vi.fn(),
254
+ generateWorkflow: vi.fn(),
255
+ });
256
+
257
+ await expect(command.run({})).rejects.toThrow('CLI ERROR');
258
+ });
259
+
260
+ it('should handle errors during frontend variable resolution', async () => {
261
+ const mockBackend = {
262
+ name: 'railway',
263
+ provision: vi.fn(),
264
+ getSecrets: vi.fn().mockResolvedValue({}),
265
+ getVariables: vi.fn().mockResolvedValue({}),
266
+ };
267
+ const mockFrontend = {
268
+ name: 'cloudflare',
269
+ provision: vi.fn(),
270
+ getSecrets: vi.fn().mockResolvedValue({}),
271
+ getVariables: vi.fn().mockRejectedValue(new Error('Front var fail')),
272
+ };
273
+ mockRegistry.getDeploymentProvider.mockImplementation((name: string) => {
274
+ if (name === 'railway') return mockBackend;
275
+ if (name === 'cloudflare') return mockFrontend;
276
+ });
277
+ mockRegistry.getRepositoryProvider.mockReturnValue({
278
+ configureSecrets: vi.fn(),
279
+ configureVariables: vi.fn(),
280
+ generateWorkflow: vi.fn(),
281
+ });
282
+
283
+ await expect(command.run({})).rejects.toThrow('CLI ERROR');
284
+ });
285
+ });
@@ -184,5 +184,20 @@ describe('InitCommand', () => {
184
184
  expect(command.error).toHaveBeenCalledWith(
185
185
  expect.stringContaining('Failed to initialize project'),
186
186
  );
187
+ expect(command.error).toHaveBeenCalledWith(
188
+ expect.stringContaining('Failed to initialize project'),
189
+ );
190
+ });
191
+
192
+ it('should handle non-Error objects in catch', async () => {
193
+ vi.mocked(git.clone).mockRejectedValueOnce('String error');
194
+
195
+ await expect(command.run({ directory: 'fail-project', repo: 'foo' })).rejects.toThrow(
196
+ 'Process.exit(1)',
197
+ );
198
+
199
+ expect(command.error).toHaveBeenCalledWith(
200
+ expect.stringContaining('Failed to initialize project: String error'),
201
+ );
187
202
  });
188
203
  });