@nexical/cli 0.11.23 → 0.12.0

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 (91) hide show
  1. package/README.md +90 -235
  2. package/dist/{chunk-OYFWMYPG.js → chunk-6DE5Q66O.js} +6 -1
  3. package/dist/{chunk-OYFWMYPG.js.map → chunk-6DE5Q66O.js.map} +1 -1
  4. package/dist/chunk-G66GMEFE.js +31 -0
  5. package/dist/chunk-G66GMEFE.js.map +1 -0
  6. package/dist/{chunk-2FKDEDDE.js → chunk-HOVS7SCD.js} +16 -3
  7. package/dist/chunk-HOVS7SCD.js.map +1 -0
  8. package/dist/{chunk-GUUPSHWC.js → chunk-JEMIKBGX.js} +3 -3
  9. package/dist/chunk-JGAMEJTL.js +4101 -0
  10. package/dist/chunk-JGAMEJTL.js.map +1 -0
  11. package/dist/{chunk-OUGA4CB4.js → chunk-JS6WL5NS.js} +2 -2
  12. package/dist/{chunk-GEESHGE4.js → chunk-L2RUXOL4.js} +2 -2
  13. package/dist/{chunk-54HY52LH.js → chunk-QTJIGPQ3.js} +2 -2
  14. package/dist/{chunk-EKCOW7FM.js → chunk-USP2MI63.js} +41 -23
  15. package/dist/chunk-USP2MI63.js.map +1 -0
  16. package/dist/{chunk-2JW5BYZW.js → chunk-VKE7R2EZ.js} +2 -2
  17. package/dist/{chunk-AC4B3HPJ.js → chunk-XONR27KC.js} +2 -2
  18. package/dist/{chunk-PJIOCW2A.js → chunk-ZWNIZB3Q.js} +2 -2
  19. package/dist/index.js +5 -5
  20. package/dist/index.js.map +1 -1
  21. package/dist/src/commands/deploy.d.ts +3 -3
  22. package/dist/src/commands/deploy.js +134 -78
  23. package/dist/src/commands/deploy.js.map +1 -1
  24. package/dist/src/commands/init.js +5 -5
  25. package/dist/src/commands/module/add.js +4 -4
  26. package/dist/src/commands/module/list.js +2 -2
  27. package/dist/src/commands/module/remove.js +2 -2
  28. package/dist/src/commands/module/update.js +2 -2
  29. package/dist/src/commands/prompt.js +2 -2
  30. package/dist/src/commands/run.js +2 -2
  31. package/dist/src/commands/setup.js +3 -3
  32. package/dist/src/deploy/config-manager.js +3 -2
  33. package/dist/src/deploy/providers/cloudflare.d.ts +13 -8
  34. package/dist/src/deploy/providers/cloudflare.js +161 -52
  35. package/dist/src/deploy/providers/cloudflare.js.map +1 -1
  36. package/dist/src/deploy/providers/dns-cloudflare.d.ts +9 -0
  37. package/dist/src/deploy/providers/dns-cloudflare.js +123 -0
  38. package/dist/src/deploy/providers/dns-cloudflare.js.map +1 -0
  39. package/dist/src/deploy/providers/github.d.ts +6 -2
  40. package/dist/src/deploy/providers/github.js +37 -45
  41. package/dist/src/deploy/providers/github.js.map +1 -1
  42. package/dist/src/deploy/providers/railway.d.ts +17 -8
  43. package/dist/src/deploy/providers/railway.js +106 -45
  44. package/dist/src/deploy/providers/railway.js.map +1 -1
  45. package/dist/src/deploy/registry.d.ts +7 -4
  46. package/dist/src/deploy/registry.js +2 -2
  47. package/dist/src/deploy/schema.d.ts +188 -0
  48. package/dist/src/deploy/schema.js +11 -0
  49. package/dist/src/deploy/schema.js.map +1 -0
  50. package/dist/src/deploy/template-manager.d.ts +12 -0
  51. package/dist/src/deploy/template-manager.js +9 -0
  52. package/dist/src/deploy/template-manager.js.map +1 -0
  53. package/dist/src/deploy/types.d.ts +42 -17
  54. package/dist/src/deploy/types.js +1 -1
  55. package/dist/src/deploy/types.js.map +1 -1
  56. package/dist/src/deploy/utils.js +2 -2
  57. package/dist/src/utils/discovery.js +2 -2
  58. package/dist/src/utils/filter.js +2 -2
  59. package/dist/src/utils/git.js +2 -2
  60. package/dist/src/utils/url-resolver.js +2 -2
  61. package/dist/templates/github-workflow.yaml +23 -0
  62. package/package.json +2 -2
  63. package/src/commands/deploy.ts +157 -93
  64. package/src/deploy/config-manager.ts +14 -1
  65. package/src/deploy/providers/cloudflare.ts +203 -80
  66. package/src/deploy/providers/dns-cloudflare.ts +134 -0
  67. package/src/deploy/providers/github.ts +44 -47
  68. package/src/deploy/providers/railway.ts +135 -55
  69. package/src/deploy/registry.ts +49 -28
  70. package/src/deploy/schema.ts +39 -0
  71. package/src/deploy/template-manager.ts +32 -0
  72. package/src/deploy/templates/github-workflow.yaml +23 -0
  73. package/src/deploy/types.ts +48 -16
  74. package/test/integration/commands/deploy.integration.test.ts +79 -3
  75. package/test/unit/commands/deploy.test.ts +96 -198
  76. package/test/unit/deploy/config-manager.test.ts +9 -5
  77. package/test/unit/deploy/providers/cloudflare.test.ts +95 -96
  78. package/test/unit/deploy/providers/dns-cloudflare.test.ts +148 -0
  79. package/test/unit/deploy/providers/github.test.ts +43 -47
  80. package/test/unit/deploy/providers/railway.test.ts +50 -261
  81. package/test/unit/deploy/registry.test.ts +20 -17
  82. package/tsup.config.ts +3 -0
  83. package/dist/chunk-2FKDEDDE.js.map +0 -1
  84. package/dist/chunk-EKCOW7FM.js.map +0 -1
  85. /package/dist/{chunk-GUUPSHWC.js.map → chunk-JEMIKBGX.js.map} +0 -0
  86. /package/dist/{chunk-OUGA4CB4.js.map → chunk-JS6WL5NS.js.map} +0 -0
  87. /package/dist/{chunk-GEESHGE4.js.map → chunk-L2RUXOL4.js.map} +0 -0
  88. /package/dist/{chunk-54HY52LH.js.map → chunk-QTJIGPQ3.js.map} +0 -0
  89. /package/dist/{chunk-2JW5BYZW.js.map → chunk-VKE7R2EZ.js.map} +0 -0
  90. /package/dist/{chunk-AC4B3HPJ.js.map → chunk-XONR27KC.js.map} +0 -0
  91. /package/dist/{chunk-PJIOCW2A.js.map → chunk-ZWNIZB3Q.js.map} +0 -0
@@ -1,7 +1,8 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
2
2
  import { RailwayProvider } from '../../../../src/deploy/providers/railway.js';
3
3
  import { execAsync } from '../../../../src/deploy/utils.js';
4
4
  import { logger } from '@nexical/cli-core';
5
+ import { DeploymentContext, AppConfig } from '../../../../src/deploy/types.js';
5
6
 
6
7
  vi.mock('../../../../src/deploy/utils.js');
7
8
  vi.mock('@nexical/cli-core', () => ({
@@ -10,22 +11,30 @@ vi.mock('@nexical/cli-core', () => ({
10
11
  error: vi.fn(),
11
12
  warn: vi.fn(),
12
13
  debug: vi.fn(),
14
+ success: vi.fn(),
13
15
  },
14
16
  }));
15
17
 
16
18
  describe('RailwayProvider', () => {
17
19
  let provider: RailwayProvider;
18
- let mockContext: { cwd: string; options: Record<string, unknown>; config: any };
20
+ let mockContext: DeploymentContext;
21
+ let mockApp: AppConfig;
19
22
 
20
23
  beforeEach(() => {
21
24
  vi.resetAllMocks();
22
25
  provider = new RailwayProvider();
23
26
  mockContext = {
24
27
  cwd: '/mock',
25
- options: {}, // Env undefined by default
26
- config: { deploy: { backend: { projectName: 'my-proj' } } },
28
+ options: {},
29
+ config: { deploy: { repository: { provider: 'github' }, apps: {} } },
30
+ } as unknown as DeploymentContext;
31
+ mockApp = {
32
+ name: 'backend',
33
+ provider: 'railway',
34
+ projectName: 'my-proj',
35
+ target: 'apps/backend',
27
36
  };
28
- (execAsync as unknown as { mockResolvedValue: any }).mockResolvedValue({
37
+ (execAsync as Mock).mockResolvedValue({
29
38
  stdout: '',
30
39
  stderr: '',
31
40
  });
@@ -35,296 +44,76 @@ describe('RailwayProvider', () => {
35
44
  vi.clearAllMocks();
36
45
  delete process.env.RAILWAY_API_TOKEN;
37
46
  delete process.env.RAILWAY_TOKEN;
38
- delete process.env.CUSTOM_RW_TOKEN;
39
47
  });
40
48
 
41
49
  describe('provision', () => {
42
- it('should default to production environment if options.env is missing', async () => {
43
- // mockContext.options.env is undefined
44
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
45
- stdout: 'Linked',
46
- }); // status
47
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
48
- stdout: 'postgres',
49
- }); // postgres check
50
-
51
- await provider.provision(mockContext);
52
-
53
- // Should stick to baseProjectName 'my-proj'
54
- expect(execAsync).not.toHaveBeenCalledWith(
55
- expect.stringContaining('my-proj-production'),
56
- expect.anything(),
57
- );
58
- });
59
-
60
- it('should error if project name missing', async () => {
61
- mockContext.config.deploy.backend.projectName = undefined;
62
- await expect(provider.provision(mockContext)).rejects.toThrow(
63
- 'Railway project name not found',
64
- );
65
- });
66
-
67
50
  it('should handle dry run', async () => {
68
51
  mockContext.options.dryRun = true;
69
- await provider.provision(mockContext);
52
+ await provider.provision(mockContext, mockApp);
70
53
  expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('[Dry Run]'));
71
54
  expect(execAsync).not.toHaveBeenCalled();
72
55
  });
73
56
 
74
- it('should handle deleted project', async () => {
75
- // Use real Error to trigger 'instanceof Error' branch
76
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
77
- Object.assign(new Error('Error'), { stderr: 'Project is deleted' }),
78
- );
79
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
80
- stdout: 'Linked',
81
- }); // init
82
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
83
- stdout: 'postgres',
84
- }); // postgres check
85
-
86
- await provider.provision(mockContext);
57
+ it('should provision successfully', async () => {
58
+ process.env.RAILWAY_TOKEN = 'tok';
59
+ (execAsync as Mock).mockResolvedValueOnce({ stdout: 'ok' }); // status
87
60
 
88
- expect(execAsync).toHaveBeenCalledWith('railway unlink', expect.anything());
89
- });
61
+ await provider.provision(mockContext, mockApp);
90
62
 
91
- it('should handle unlink failure (deleted project)', async () => {
92
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
93
- Object.assign(new Error('Error'), { stderr: 'Project is deleted' }),
94
- );
95
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
96
- new Error('Unlink failed'),
97
- ); // unlink fail
98
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
99
- stdout: 'Linked',
100
- }); // init
101
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
102
- stdout: 'postgres',
103
- }); // postgres check
104
-
105
- await provider.provision(mockContext);
106
- // Should proceed to init despite unlink fail
107
63
  expect(execAsync).toHaveBeenCalledWith(
108
- expect.stringContaining('railway init'),
64
+ expect.stringContaining('railway status'),
109
65
  expect.anything(),
110
66
  );
111
67
  });
112
68
 
113
- it('should check status and init if not linked', async () => {
114
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
115
- Object.assign(new Error('Error'), { stderr: 'Project not found' }),
116
- );
117
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
118
- stdout: 'Linked',
119
- });
120
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
121
- stdout: 'postgres',
122
- });
69
+ it('should warn if auto-add service fails', async () => {
70
+ process.env.RAILWAY_TOKEN = 'tok';
71
+ mockApp.railway = {
72
+ services: [{ type: 'database', name: 'postgres' }],
73
+ };
123
74
 
124
- await provider.provision(mockContext);
75
+ (execAsync as Mock).mockResolvedValueOnce({ stdout: 'ok' }); // status
76
+ (execAsync as Mock).mockResolvedValueOnce({ stdout: 'empty' }); // list
77
+ (execAsync as Mock).mockRejectedValueOnce(new Error('Add failed')); // add
125
78
 
126
- expect(execAsync).toHaveBeenCalledWith(
127
- expect.stringContaining('railway init --name my-proj'),
128
- expect.anything(),
129
- );
130
- });
131
-
132
- it('should throw on auth failure', async () => {
133
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
134
- Object.assign(new Error('Error'), { stderr: 'Unauthorized' }),
135
- );
136
-
137
- await expect(provider.provision(mockContext)).rejects.toThrow(
138
- 'Railway authentication failed',
139
- );
140
- });
141
-
142
- it('should warn on generic status failure with Error object', async () => {
143
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
144
- new Error('Timeout'),
145
- ); // generic
146
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
147
- stdout: 'postgres',
148
- }); // postgres check pass
149
-
150
- await provider.provision(mockContext);
79
+ await provider.provision(mockContext, mockApp);
151
80
 
152
81
  expect(logger.warn).toHaveBeenCalledWith(
153
- expect.stringContaining('Railway status check failed'),
154
- );
155
- });
156
-
157
- it('should warn on generic status failure with string error', async () => {
158
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
159
- 'Timeout String',
160
- );
161
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
162
- stdout: 'postgres',
163
- });
164
-
165
- await provider.provision(mockContext);
166
-
167
- expect(logger.warn).toHaveBeenCalledWith(
168
- expect.stringContaining('Railway status check failed: Timeout String'),
169
- );
170
- });
171
-
172
- it('should add postgres if missing', async () => {
173
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
174
- stdout: 'ok',
175
- }); // status ok
176
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
177
- stdout: 'empty',
178
- }); // postgres missing
179
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
180
- stdout: 'db added',
181
- }); // add db
182
-
183
- await provider.provision(mockContext);
184
-
185
- expect(execAsync).toHaveBeenCalledWith(
186
- expect.stringContaining('railway add --database postgres'),
187
- expect.anything(),
188
- );
189
- });
190
-
191
- it('should handle second status check failure (swallow error)', async () => {
192
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
193
- stdout: 'ok',
194
- }); // status ok
195
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
196
- new Error('Status2 fail'),
197
- ); // status2 fail -> catch -> { stdout: '' }
198
- // If stdout is empty, it proceeds to add postgres?
199
- // Line 78: if (!status.includes('postgres')) -> '' doesn't include it -> true.
200
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
201
- stdout: 'db added',
202
- }); // add db
203
-
204
- await provider.provision(mockContext);
205
-
206
- expect(execAsync).toHaveBeenCalledWith(
207
- expect.stringContaining('railway add --database postgres'),
208
- expect.anything(),
209
- );
210
- });
211
-
212
- it('should handle postgres status check failure (explicit throw in add logic check)', async () => {
213
- // This tests the logic flow, but "Status2 fail" catch block return value triggers "add"
214
- // My previous test above covers the catch block.
215
- });
216
-
217
- it('should warn if auto-add postgres fails', async () => {
218
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
219
- stdout: 'ok',
220
- }); // status ok
221
- (execAsync as unknown as { mockResolvedValueOnce: any }).mockResolvedValueOnce({
222
- stdout: 'empty',
223
- }); // postgres missing
224
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
225
- new Error('Add failed'),
226
- ); // add db fail
227
-
228
- await provider.provision(mockContext);
229
-
230
- expect(logger.warn).toHaveBeenCalledWith('Failed to auto-add PostgreSQL.');
231
- });
232
-
233
- it('should log generic setup failures', async () => {
234
- const err = new Error('Generic failure') as any;
235
- err.stderr = 'some stderr';
236
- err.stdout = 'some stdout';
237
-
238
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
239
- Object.assign(new Error('Error'), { stderr: 'Project not found' }),
240
- );
241
- (execAsync as any).mockRejectedValueOnce(err);
242
-
243
- await provider.provision(mockContext);
244
-
245
- expect(logger.error).toHaveBeenCalledWith(
246
- expect.stringContaining('Railway setup failed with error: Generic failure'),
247
- );
248
- });
249
-
250
- it('should handle setup failure with empty output', async () => {
251
- const errEmpty = new Error('Empty fail') as any;
252
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
253
- Object.assign(new Error('Error'), { stderr: 'Project not found' }),
254
- );
255
- (execAsync as any).mockRejectedValueOnce(errEmpty);
256
-
257
- await provider.provision(mockContext);
258
-
259
- expect(logger.error).toHaveBeenCalledWith(
260
- expect.stringContaining('Railway setup failed with error: Empty fail'),
261
- );
262
- });
263
-
264
- it('should handle non-Error exceptions in outer catch', async () => {
265
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
266
- Object.assign(new Error('Error'), { stderr: 'Project not found' }),
267
- );
268
- (execAsync as any).mockRejectedValueOnce('String setup error');
269
-
270
- await provider.provision(mockContext);
271
-
272
- expect(logger.error).toHaveBeenCalledWith(
273
- expect.stringContaining('Railway setup failed with error: String setup error'),
274
- );
275
- });
276
-
277
- it('should handle non-production environment', async () => {
278
- mockContext.options.env = 'staging';
279
- (execAsync as unknown as { mockRejectedValueOnce: any }).mockRejectedValueOnce(
280
- Object.assign(new Error('Error'), { stderr: 'Project not found' }),
281
- );
282
-
283
- await provider.provision(mockContext);
284
-
285
- expect(execAsync).toHaveBeenCalledWith(
286
- expect.stringContaining('railway init --name my-proj-staging'),
287
- expect.anything(),
82
+ expect.stringContaining('Failed to auto-add postgres database'),
288
83
  );
289
84
  });
290
85
  });
291
86
 
292
87
  describe('getSecrets', () => {
293
- it('should resolve token from env', async () => {
294
- process.env.RAILWAY_API_TOKEN = 'tok';
295
- const secrets = await provider.getSecrets(mockContext);
296
- expect(secrets['RAILWAY_API_TOKEN']).toBe('tok');
297
- delete process.env.RAILWAY_API_TOKEN;
298
- });
299
-
300
- it('should resolve token from configured env var', async () => {
301
- mockContext.config.deploy.backend.options = { tokenEnvVar: 'CUSTOM_RW_TOKEN' };
302
- process.env.CUSTOM_RW_TOKEN = 'custom-rw-tok';
303
-
304
- const secrets = await provider.getSecrets(mockContext);
305
- expect(secrets['RAILWAY_API_TOKEN']).toBe('custom-rw-tok');
88
+ it('should resolve secrets', async () => {
89
+ process.env.RAILWAY_TOKEN = 'tok';
90
+ mockApp.secrets = { KEY: 'SOME_ENV' };
91
+ process.env.SOME_ENV = 'VALUE';
306
92
 
307
- delete process.env.CUSTOM_RW_TOKEN;
308
- });
93
+ const secrets = await provider.getSecrets(mockContext, mockApp);
94
+ expect(secrets['RAILWAY_API_TOKEN']).toBe('tok');
95
+ expect(secrets['KEY']).toBe('VALUE');
309
96
 
310
- it('should error if token missing', async () => {
311
- delete process.env.RAILWAY_API_TOKEN;
312
- delete process.env.RAILWAY_TOKEN;
313
- await expect(provider.getSecrets(mockContext)).rejects.toThrow('Railway Token not found');
97
+ delete process.env.SOME_ENV;
314
98
  });
315
99
  });
316
100
 
317
101
  describe('getVariables', () => {
318
- it('should return project name', async () => {
319
- expect(await provider.getVariables(mockContext)).toEqual({
320
- RAILWAY_PROJECT_NAME: 'my-proj',
321
- });
102
+ it('should return project and environment variables', async () => {
103
+ const vars = await provider.getVariables(mockContext, mockApp);
104
+ expect(vars['RAILWAY_PROJECT_NAME']).toBe('my-proj');
105
+ expect(vars['RAILWAY_ENVIRONMENT']).toBe('production');
322
106
  });
323
107
  });
324
108
 
325
- describe('getCIConfig', () => {
326
- it('should return config', () => {
327
- expect(provider.getCIConfig()).toHaveProperty('deploySteps');
109
+ describe('deploy', () => {
110
+ it('should run railway up', async () => {
111
+ process.env.RAILWAY_TOKEN = 'tok';
112
+ await provider.deploy(mockContext, mockApp);
113
+ expect(execAsync).toHaveBeenCalledWith(
114
+ expect.stringContaining('railway up'),
115
+ expect.anything(),
116
+ );
328
117
  });
329
118
  });
330
119
  });
@@ -35,9 +35,9 @@ describe('ProviderRegistry', () => {
35
35
  vi.restoreAllMocks();
36
36
  });
37
37
 
38
- describe('getDeploymentProvider', () => {
38
+ describe('getHostingProvider', () => {
39
39
  it('should return undefined for non-existent provider', () => {
40
- expect(registry.getDeploymentProvider('missing')).toBeUndefined();
40
+ expect(registry.getHostingProvider('missing')).toBeUndefined();
41
41
  });
42
42
  });
43
43
 
@@ -49,9 +49,9 @@ describe('ProviderRegistry', () => {
49
49
  getCIConfig() {}
50
50
  };
51
51
  (
52
- registry as unknown as { registerProviderFromModule: (mod: any, name: string) => void }
52
+ registry as unknown as { registerProviderFromModule: (mod: unknown, name: string) => void }
53
53
  ).registerProviderFromModule({ default: MockProvider }, 'test');
54
- expect(registry.getDeploymentProvider('valid-deploy')).toBeDefined();
54
+ expect(registry.getHostingProvider('valid-deploy')).toBeDefined();
55
55
  });
56
56
 
57
57
  it('should register a valid repository provider', () => {
@@ -61,7 +61,7 @@ describe('ProviderRegistry', () => {
61
61
  generateWorkflow() {}
62
62
  };
63
63
  (
64
- registry as unknown as { registerProviderFromModule: (mod: any, name: string) => void }
64
+ registry as unknown as { registerProviderFromModule: (mod: unknown, name: string) => void }
65
65
  ).registerProviderFromModule({ default: MockProvider }, 'test');
66
66
  expect(registry.getRepositoryProvider('valid-repo')).toBeDefined();
67
67
  });
@@ -73,19 +73,19 @@ describe('ProviderRegistry', () => {
73
73
  getCIConfig() {}
74
74
  };
75
75
  (
76
- registry as unknown as { registerProviderFromModule: (mod: any, name: string) => void }
76
+ registry as unknown as { registerProviderFromModule: (mod: unknown, name: string) => void }
77
77
  ).registerProviderFromModule({ Named: MockProvider }, 'test');
78
- expect(registry.getDeploymentProvider('named-export')).toBeDefined();
78
+ expect(registry.getHostingProvider('named-export')).toBeDefined();
79
79
  });
80
80
 
81
81
  it('should handle missing exported provider', async () => {
82
82
  const mockModule = {};
83
83
  await (
84
84
  registry as unknown as {
85
- registerProviderFromModule: (mod: any, name: string) => Promise<void>;
85
+ registerProviderFromModule: (mod: unknown, name: string) => Promise<void>;
86
86
  }
87
87
  ).registerProviderFromModule(mockModule, 'test');
88
- expect(registry.getDeploymentProvider('test')).toBeUndefined();
88
+ expect(registry.getHostingProvider('test')).toBeUndefined();
89
89
  });
90
90
 
91
91
  it('should handle instantiation failure', async () => {
@@ -95,20 +95,20 @@ describe('ProviderRegistry', () => {
95
95
  const mockModule = { Provider: MockProvider };
96
96
  await (
97
97
  registry as unknown as {
98
- registerProviderFromModule: (mod: any, name: string) => Promise<void>;
98
+ registerProviderFromModule: (mod: unknown, name: string) => Promise<void>;
99
99
  }
100
100
  ).registerProviderFromModule(mockModule, 'fail');
101
- expect(registry.getDeploymentProvider('fail')).toBeUndefined();
101
+ expect(registry.getHostingProvider('fail')).toBeUndefined();
102
102
  });
103
103
 
104
104
  it('should handle non-class provider', async () => {
105
105
  const mockModule = { Provider: { name: 'static' } };
106
106
  await (
107
107
  registry as unknown as {
108
- registerProviderFromModule: (mod: any, name: string) => Promise<void>;
108
+ registerProviderFromModule: (mod: unknown, name: string) => Promise<void>;
109
109
  }
110
110
  ).registerProviderFromModule(mockModule, 'static');
111
- expect(registry.getDeploymentProvider('static')).toBeUndefined();
111
+ expect(registry.getHostingProvider('static')).toBeUndefined();
112
112
  });
113
113
  });
114
114
 
@@ -136,7 +136,7 @@ describe('ProviderRegistry', () => {
136
136
 
137
137
  // Assert
138
138
  expect(fs.readdir).toHaveBeenCalled();
139
- expect(registry.getDeploymentProvider('core-mock')).toBeDefined();
139
+ expect(registry.getHostingProvider('core-mock')).toBeDefined();
140
140
  });
141
141
 
142
142
  it('should warn if no providers directory found', async () => {
@@ -196,7 +196,7 @@ describe('ProviderRegistry', () => {
196
196
  await registry.loadLocalProviders(mockRoot);
197
197
 
198
198
  expect(mockJitiRequest).toHaveBeenCalledWith(path.join(deployDir, 'custom.ts'));
199
- expect(registry.getDeploymentProvider('local-custom')).toBeDefined();
199
+ expect(registry.getHostingProvider('local-custom')).toBeDefined();
200
200
  });
201
201
 
202
202
  it('should ignore non-js/ts files', async () => {
@@ -246,11 +246,14 @@ describe('ProviderRegistry', () => {
246
246
 
247
247
  it('should handle non-Error during registration in loadCoreProviders', async () => {
248
248
  vi.spyOn(fs, 'access').mockResolvedValue(undefined);
249
- vi.spyOn(fs, 'readdir').mockResolvedValue(['fail-registration.js'] as any);
249
+ vi.spyOn(fs, 'readdir').mockResolvedValue(['fail-registration.js'] as unknown as never);
250
250
 
251
251
  // Mock registerProviderFromModule to throw a string
252
252
  const regSpy = vi
253
- .spyOn(registry as any, 'registerProviderFromModule')
253
+ .spyOn(
254
+ registry as unknown as { registerProviderFromModule: (m: unknown, s: string) => void },
255
+ 'registerProviderFromModule',
256
+ )
254
257
  .mockImplementation(() => {
255
258
  throw 'Registration string fail';
256
259
  });
package/tsup.config.ts CHANGED
@@ -12,6 +12,9 @@ export default <Options>{
12
12
  splitting: true,
13
13
  outDir: 'dist',
14
14
  shims: true, // Enable shims (including __require shim for legacy deps)
15
+ loader: {
16
+ '.yaml': 'copy',
17
+ },
15
18
  banner: {
16
19
  js: 'import { createRequire } from "module"; const require = createRequire(import.meta.url);',
17
20
  },
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/deploy/config-manager.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport YAML from 'yaml';\nimport { NexicalConfig } from './types';\n\nexport class ConfigManager {\n private configPath: string;\n\n constructor(cwd: string) {\n this.configPath = path.join(cwd, 'nexical.yaml');\n }\n\n async load(): Promise<NexicalConfig> {\n try {\n const content = await fs.readFile(this.configPath, 'utf-8');\n return YAML.parse(content) as NexicalConfig;\n } catch (error: unknown) {\n if (\n error &&\n typeof error === 'object' &&\n 'code' in error &&\n (error as { code: unknown }).code === 'ENOENT'\n ) {\n return {};\n }\n throw error;\n }\n }\n\n async save(config: NexicalConfig): Promise<void> {\n const content = YAML.stringify(config);\n await fs.writeFile(this.configPath, content, 'utf-8');\n }\n\n exists(): Promise<boolean> {\n return fs\n .access(this.configPath)\n .then(() => true)\n .catch(() => false);\n }\n}\n"],"mappings":";;;;;;AAAA;AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,UAAU;AAGV,IAAM,gBAAN,MAAoB;AAAA,EACjB;AAAA,EAER,YAAY,KAAa;AACvB,SAAK,aAAa,KAAK,KAAK,KAAK,cAAc;AAAA,EACjD;AAAA,EAEA,MAAM,OAA+B;AACnC,QAAI;AACF,YAAM,UAAU,MAAM,GAAG,SAAS,KAAK,YAAY,OAAO;AAC1D,aAAO,KAAK,MAAM,OAAO;AAAA,IAC3B,SAAS,OAAgB;AACvB,UACE,SACA,OAAO,UAAU,YACjB,UAAU,SACT,MAA4B,SAAS,UACtC;AACA,eAAO,CAAC;AAAA,MACV;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,QAAsC;AAC/C,UAAM,UAAU,KAAK,UAAU,MAAM;AACrC,UAAM,GAAG,UAAU,KAAK,YAAY,SAAS,OAAO;AAAA,EACtD;AAAA,EAEA,SAA2B;AACzB,WAAO,GACJ,OAAO,KAAK,UAAU,EACtB,KAAK,MAAM,IAAI,EACf,MAAM,MAAM,KAAK;AAAA,EACtB;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/deploy/registry.ts"],"sourcesContent":["import path from 'node:path';\nimport fs from 'node:fs/promises';\nimport { logger } from '@nexical/cli-core';\nimport { DeploymentProvider, RepositoryProvider } from './types';\n\nexport class ProviderRegistry {\n private deploymentProviders: Map<string, DeploymentProvider> = new Map();\n private repositoryProviders: Map<string, RepositoryProvider> = new Map();\n\n registerDeploymentProvider(provider: DeploymentProvider) {\n this.deploymentProviders.set(provider.name, provider);\n }\n\n registerRepositoryProvider(provider: RepositoryProvider) {\n this.repositoryProviders.set(provider.name, provider);\n }\n\n getDeploymentProvider(name: string): DeploymentProvider | undefined {\n return this.deploymentProviders.get(name);\n }\n\n getRepositoryProvider(name: string): RepositoryProvider | undefined {\n return this.repositoryProviders.get(name);\n }\n\n private registerProviderFromModule(module: unknown, source: string) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const moduleAny = module as any;\n let provider = moduleAny.default;\n\n // Handle named exports if default is missing (fallback)\n if (!provider && Object.keys(moduleAny).length > 0) {\n // Try to find a class export that looks like a provider\n for (const key of Object.keys(moduleAny)) {\n if (typeof moduleAny[key] === 'function') {\n provider = moduleAny[key];\n break;\n }\n }\n }\n\n // If it's a class, instantiate it\n if (typeof provider === 'function') {\n try {\n provider = new provider();\n } catch {\n // Not a constructor or failed\n }\n }\n\n if (provider) {\n if (typeof provider.provision === 'function' && typeof provider.getCIConfig === 'function') {\n logger.info(`[Registry] Loaded ${source} deployment provider: ${provider.name}`);\n this.registerDeploymentProvider(provider as DeploymentProvider);\n } else if (\n typeof provider.configureSecrets === 'function' &&\n typeof provider.generateWorkflow === 'function'\n ) {\n logger.info(`[Registry] Loaded ${source} repository provider: ${provider.name}`);\n this.registerRepositoryProvider(provider as RepositoryProvider);\n }\n }\n }\n\n async loadCoreProviders() {\n const dirname = path.dirname(new URL(import.meta.url).pathname);\n\n // Try multiple paths to find the providers directory\n // 1. 'providers' - Standard source structure / flattened dist\n // 2. 'src/deploy/providers' - tsup output (chunk in root, files in src/...)\n const candidates = [\n path.join(dirname, 'providers'),\n path.join(dirname, 'src/deploy/providers'),\n ];\n\n let providersDir = '';\n for (const candidate of candidates) {\n try {\n await fs.access(candidate);\n providersDir = candidate;\n break;\n } catch {\n // Ignore missing dir\n }\n }\n\n if (!providersDir) {\n logger.warn(\n `[Registry] Could not locate core providers directory. Checked: ${candidates.join(', ')}`,\n );\n return;\n }\n\n try {\n const files = await fs.readdir(providersDir);\n for (const file of files) {\n if (file.endsWith('.js') || (file.endsWith('.ts') && !file.endsWith('.d.ts'))) {\n try {\n const providerPath = path.join(providersDir, file);\n const module = await import(providerPath);\n this.registerProviderFromModule(module, 'core');\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n logger.warn(`Failed to load core provider from ${file}: ${message}`);\n }\n }\n }\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n logger.warn(`Failed to scan core providers at ${providersDir}: ${message}`);\n }\n }\n\n async loadLocalProviders(cwd: string) {\n const deployDir = path.join(cwd, 'deploy');\n try {\n const files = await fs.readdir(deployDir);\n for (const file of files) {\n if (file.endsWith('.ts') || file.endsWith('.js')) {\n try {\n const providerPath = path.join(deployDir, file);\n // Use jiti to load TS/JS files dynamically\n const jiti = (await import('jiti')).createJiti(import.meta.url);\n const module = (await jiti.import(providerPath)) as unknown;\n this.registerProviderFromModule(module, 'local');\n } catch (e: unknown) {\n const message = e instanceof Error ? e.message : String(e);\n logger.warn(`Failed to load local provider from ${file}: ${message}`);\n }\n }\n }\n } catch {\n // Ignore if deploy dir doesn't exist\n }\n }\n}\n"],"mappings":"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","names":[]}