@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.
- package/README.md +90 -235
- package/dist/{chunk-OYFWMYPG.js → chunk-6DE5Q66O.js} +6 -1
- package/dist/{chunk-OYFWMYPG.js.map → chunk-6DE5Q66O.js.map} +1 -1
- package/dist/chunk-G66GMEFE.js +31 -0
- package/dist/chunk-G66GMEFE.js.map +1 -0
- package/dist/{chunk-2FKDEDDE.js → chunk-HOVS7SCD.js} +16 -3
- package/dist/chunk-HOVS7SCD.js.map +1 -0
- package/dist/{chunk-GUUPSHWC.js → chunk-JEMIKBGX.js} +3 -3
- package/dist/chunk-JGAMEJTL.js +4101 -0
- package/dist/chunk-JGAMEJTL.js.map +1 -0
- package/dist/{chunk-OUGA4CB4.js → chunk-JS6WL5NS.js} +2 -2
- package/dist/{chunk-GEESHGE4.js → chunk-L2RUXOL4.js} +2 -2
- package/dist/{chunk-54HY52LH.js → chunk-QTJIGPQ3.js} +2 -2
- package/dist/{chunk-EKCOW7FM.js → chunk-USP2MI63.js} +41 -23
- package/dist/chunk-USP2MI63.js.map +1 -0
- package/dist/{chunk-2JW5BYZW.js → chunk-VKE7R2EZ.js} +2 -2
- package/dist/{chunk-AC4B3HPJ.js → chunk-XONR27KC.js} +2 -2
- package/dist/{chunk-PJIOCW2A.js → chunk-ZWNIZB3Q.js} +2 -2
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/src/commands/deploy.d.ts +3 -3
- package/dist/src/commands/deploy.js +134 -78
- package/dist/src/commands/deploy.js.map +1 -1
- package/dist/src/commands/init.js +5 -5
- package/dist/src/commands/module/add.js +4 -4
- package/dist/src/commands/module/list.js +2 -2
- package/dist/src/commands/module/remove.js +2 -2
- package/dist/src/commands/module/update.js +2 -2
- package/dist/src/commands/prompt.js +2 -2
- package/dist/src/commands/run.js +2 -2
- package/dist/src/commands/setup.js +3 -3
- package/dist/src/deploy/config-manager.js +3 -2
- package/dist/src/deploy/providers/cloudflare.d.ts +13 -8
- package/dist/src/deploy/providers/cloudflare.js +161 -52
- package/dist/src/deploy/providers/cloudflare.js.map +1 -1
- package/dist/src/deploy/providers/dns-cloudflare.d.ts +9 -0
- package/dist/src/deploy/providers/dns-cloudflare.js +123 -0
- package/dist/src/deploy/providers/dns-cloudflare.js.map +1 -0
- package/dist/src/deploy/providers/github.d.ts +6 -2
- package/dist/src/deploy/providers/github.js +37 -45
- package/dist/src/deploy/providers/github.js.map +1 -1
- package/dist/src/deploy/providers/railway.d.ts +17 -8
- package/dist/src/deploy/providers/railway.js +106 -45
- package/dist/src/deploy/providers/railway.js.map +1 -1
- package/dist/src/deploy/registry.d.ts +7 -4
- package/dist/src/deploy/registry.js +2 -2
- package/dist/src/deploy/schema.d.ts +188 -0
- package/dist/src/deploy/schema.js +11 -0
- package/dist/src/deploy/schema.js.map +1 -0
- package/dist/src/deploy/template-manager.d.ts +12 -0
- package/dist/src/deploy/template-manager.js +9 -0
- package/dist/src/deploy/template-manager.js.map +1 -0
- package/dist/src/deploy/types.d.ts +42 -17
- package/dist/src/deploy/types.js +1 -1
- package/dist/src/deploy/types.js.map +1 -1
- package/dist/src/deploy/utils.js +2 -2
- package/dist/src/utils/discovery.js +2 -2
- package/dist/src/utils/filter.js +2 -2
- package/dist/src/utils/git.js +2 -2
- package/dist/src/utils/url-resolver.js +2 -2
- package/dist/templates/github-workflow.yaml +23 -0
- package/package.json +2 -2
- package/src/commands/deploy.ts +157 -93
- package/src/deploy/config-manager.ts +14 -1
- package/src/deploy/providers/cloudflare.ts +203 -80
- package/src/deploy/providers/dns-cloudflare.ts +134 -0
- package/src/deploy/providers/github.ts +44 -47
- package/src/deploy/providers/railway.ts +135 -55
- package/src/deploy/registry.ts +49 -28
- package/src/deploy/schema.ts +39 -0
- package/src/deploy/template-manager.ts +32 -0
- package/src/deploy/templates/github-workflow.yaml +23 -0
- package/src/deploy/types.ts +48 -16
- package/test/integration/commands/deploy.integration.test.ts +79 -3
- package/test/unit/commands/deploy.test.ts +96 -198
- package/test/unit/deploy/config-manager.test.ts +9 -5
- package/test/unit/deploy/providers/cloudflare.test.ts +95 -96
- package/test/unit/deploy/providers/dns-cloudflare.test.ts +148 -0
- package/test/unit/deploy/providers/github.test.ts +43 -47
- package/test/unit/deploy/providers/railway.test.ts +50 -261
- package/test/unit/deploy/registry.test.ts +20 -17
- package/tsup.config.ts +3 -0
- package/dist/chunk-2FKDEDDE.js.map +0 -1
- package/dist/chunk-EKCOW7FM.js.map +0 -1
- /package/dist/{chunk-GUUPSHWC.js.map → chunk-JEMIKBGX.js.map} +0 -0
- /package/dist/{chunk-OUGA4CB4.js.map → chunk-JS6WL5NS.js.map} +0 -0
- /package/dist/{chunk-GEESHGE4.js.map → chunk-L2RUXOL4.js.map} +0 -0
- /package/dist/{chunk-54HY52LH.js.map → chunk-QTJIGPQ3.js.map} +0 -0
- /package/dist/{chunk-2JW5BYZW.js.map → chunk-VKE7R2EZ.js.map} +0 -0
- /package/dist/{chunk-AC4B3HPJ.js.map → chunk-XONR27KC.js.map} +0 -0
- /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:
|
|
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: {},
|
|
26
|
-
config: { deploy: {
|
|
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
|
|
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
|
|
75
|
-
|
|
76
|
-
(execAsync as
|
|
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
|
-
|
|
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
|
|
64
|
+
expect.stringContaining('railway status'),
|
|
109
65
|
expect.anything(),
|
|
110
66
|
);
|
|
111
67
|
});
|
|
112
68
|
|
|
113
|
-
it('should
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|
294
|
-
process.env.
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
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('
|
|
326
|
-
it('should
|
|
327
|
-
|
|
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('
|
|
38
|
+
describe('getHostingProvider', () => {
|
|
39
39
|
it('should return undefined for non-existent provider', () => {
|
|
40
|
-
expect(registry.
|
|
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:
|
|
52
|
+
registry as unknown as { registerProviderFromModule: (mod: unknown, name: string) => void }
|
|
53
53
|
).registerProviderFromModule({ default: MockProvider }, 'test');
|
|
54
|
-
expect(registry.
|
|
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:
|
|
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:
|
|
76
|
+
registry as unknown as { registerProviderFromModule: (mod: unknown, name: string) => void }
|
|
77
77
|
).registerProviderFromModule({ Named: MockProvider }, 'test');
|
|
78
|
-
expect(registry.
|
|
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:
|
|
85
|
+
registerProviderFromModule: (mod: unknown, name: string) => Promise<void>;
|
|
86
86
|
}
|
|
87
87
|
).registerProviderFromModule(mockModule, 'test');
|
|
88
|
-
expect(registry.
|
|
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:
|
|
98
|
+
registerProviderFromModule: (mod: unknown, name: string) => Promise<void>;
|
|
99
99
|
}
|
|
100
100
|
).registerProviderFromModule(mockModule, 'fail');
|
|
101
|
-
expect(registry.
|
|
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:
|
|
108
|
+
registerProviderFromModule: (mod: unknown, name: string) => Promise<void>;
|
|
109
109
|
}
|
|
110
110
|
).registerProviderFromModule(mockModule, 'static');
|
|
111
|
-
expect(registry.
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|