@capawesome/cli 3.10.2 → 4.0.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/CHANGELOG.md +34 -0
- package/dist/commands/apps/builds/cancel.js +1 -1
- package/dist/commands/apps/builds/create.js +58 -50
- package/dist/commands/apps/builds/download.js +27 -3
- package/dist/commands/apps/bundles/create.js +5 -449
- package/dist/commands/apps/bundles/delete.js +3 -68
- package/dist/commands/apps/bundles/update.js +3 -66
- package/dist/commands/apps/channels/create.js +5 -8
- package/dist/commands/apps/channels/create.test.js +6 -9
- package/dist/commands/apps/channels/delete.js +13 -4
- package/dist/commands/apps/channels/delete.test.js +22 -7
- package/dist/commands/apps/channels/get.js +2 -12
- package/dist/commands/apps/channels/get.test.js +1 -2
- package/dist/commands/apps/channels/list.js +36 -12
- package/dist/commands/apps/channels/list.test.js +3 -4
- package/dist/commands/apps/channels/pause.js +85 -0
- package/dist/commands/apps/channels/resume.js +85 -0
- package/dist/commands/apps/channels/update.js +4 -7
- package/dist/commands/apps/channels/update.test.js +2 -4
- package/dist/commands/apps/create.js +1 -1
- package/dist/commands/apps/delete.js +3 -2
- package/dist/commands/apps/deployments/cancel.js +1 -1
- package/dist/commands/apps/deployments/create.js +82 -31
- package/dist/commands/apps/devices/delete.js +3 -2
- package/dist/commands/apps/environments/create.js +68 -0
- package/dist/commands/apps/environments/delete.js +88 -0
- package/dist/commands/apps/environments/list.js +69 -0
- package/dist/commands/apps/environments/set.js +126 -0
- package/dist/commands/apps/environments/unset.js +98 -0
- package/dist/commands/apps/liveupdates/bundle.js +117 -0
- package/dist/commands/apps/liveupdates/generate-manifest.js +39 -0
- package/dist/commands/{manifests/generate.test.js → apps/liveupdates/generate-manifest.test.js} +6 -6
- package/dist/commands/apps/liveupdates/register.js +291 -0
- package/dist/commands/apps/{bundles/create.test.js → liveupdates/register.test.js} +123 -111
- package/dist/commands/apps/liveupdates/rollback.js +171 -0
- package/dist/commands/apps/liveupdates/rollout.js +147 -0
- package/dist/commands/apps/liveupdates/upload.js +420 -0
- package/dist/commands/apps/liveupdates/upload.test.js +325 -0
- package/dist/commands/manifests/generate.js +2 -27
- package/dist/commands/organizations/create.js +1 -1
- package/dist/index.js +13 -0
- package/dist/services/app-builds.js +9 -2
- package/dist/services/app-channels.js +19 -0
- package/dist/services/app-deployments.js +24 -14
- package/dist/services/app-environments.js +67 -2
- package/dist/services/config.js +2 -0
- package/dist/utils/app-environments.js +35 -0
- package/dist/utils/app-environments.ts.test.js +76 -0
- package/dist/utils/time-format.js +26 -0
- package/package.json +3 -3
- package/dist/commands/apps/bundles/delete.test.js +0 -142
- package/dist/commands/apps/bundles/update.test.js +0 -144
- package/dist/utils/capacitor-config.js +0 -96
- package/dist/utils/package-json.js +0 -58
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
3
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
4
|
+
import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
|
|
5
|
+
import userConfig from '../../../utils/user-config.js';
|
|
6
|
+
import consola from 'consola';
|
|
7
|
+
import nock from 'nock';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import uploadCommand from './upload.js';
|
|
10
|
+
// Mock dependencies
|
|
11
|
+
vi.mock('@/utils/user-config.js');
|
|
12
|
+
vi.mock('@/services/authorization-service.js');
|
|
13
|
+
vi.mock('@/utils/file.js');
|
|
14
|
+
vi.mock('@/utils/zip.js');
|
|
15
|
+
vi.mock('@/utils/buffer.js');
|
|
16
|
+
vi.mock('@/utils/private-key.js');
|
|
17
|
+
vi.mock('@/utils/hash.js');
|
|
18
|
+
vi.mock('@/utils/signature.js');
|
|
19
|
+
vi.mock('@/utils/manifest.js');
|
|
20
|
+
vi.mock('@/utils/environment.js');
|
|
21
|
+
vi.mock('consola');
|
|
22
|
+
describe('apps-liveupdates-upload', () => {
|
|
23
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
24
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
25
|
+
const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
|
|
26
|
+
const mockGetFilesInDirectoryAndSubdirectories = vi.mocked(getFilesInDirectoryAndSubdirectories);
|
|
27
|
+
const mockIsDirectory = vi.mocked(isDirectory);
|
|
28
|
+
const mockIsInteractive = vi.mocked(isInteractive);
|
|
29
|
+
const mockConsola = vi.mocked(consola);
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
33
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
34
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
35
|
+
mockIsInteractive.mockReturnValue(false);
|
|
36
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
37
|
+
throw new Error(`Process exited with code ${code}`);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
nock.cleanAll();
|
|
42
|
+
vi.restoreAllMocks();
|
|
43
|
+
});
|
|
44
|
+
it('should require authentication', async () => {
|
|
45
|
+
const appId = 'app-123';
|
|
46
|
+
const options = { appId, path: './dist', artifactType: 'zip', rollout: 1 };
|
|
47
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
|
|
48
|
+
await expect(uploadCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
49
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command. Please run the `login` command first.');
|
|
50
|
+
});
|
|
51
|
+
it('should handle path validation errors', async () => {
|
|
52
|
+
const appId = 'app-123';
|
|
53
|
+
const nonexistentPath = './nonexistent';
|
|
54
|
+
const options = { appId, path: nonexistentPath, artifactType: 'zip', rollout: 1 };
|
|
55
|
+
mockFileExistsAtPath.mockResolvedValue(false);
|
|
56
|
+
await expect(uploadCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
57
|
+
expect(mockConsola.error).toHaveBeenCalledWith('The path does not exist.');
|
|
58
|
+
});
|
|
59
|
+
it('should validate manifest artifact type requires directory', async () => {
|
|
60
|
+
const appId = 'app-123';
|
|
61
|
+
const bundlePath = './bundle.zip';
|
|
62
|
+
const options = {
|
|
63
|
+
appId,
|
|
64
|
+
path: bundlePath,
|
|
65
|
+
artifactType: 'manifest',
|
|
66
|
+
rollout: 1,
|
|
67
|
+
};
|
|
68
|
+
mockFileExistsAtPath.mockResolvedValue(true);
|
|
69
|
+
mockIsDirectory.mockResolvedValue(false);
|
|
70
|
+
// Mock zip utility to return true so path validation passes
|
|
71
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
72
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
|
|
73
|
+
await expect(uploadCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
74
|
+
expect(mockConsola.error).toHaveBeenCalledWith('The path must be a folder when creating a bundle with an artifact type of `manifest`.');
|
|
75
|
+
});
|
|
76
|
+
it('should upload bundle successfully', async () => {
|
|
77
|
+
const appId = 'app-123';
|
|
78
|
+
const bundlePath = './dist';
|
|
79
|
+
const bundleId = 'bundle-456';
|
|
80
|
+
const testToken = 'test-token';
|
|
81
|
+
const testBuffer = Buffer.from('test');
|
|
82
|
+
const options = {
|
|
83
|
+
appId,
|
|
84
|
+
path: bundlePath,
|
|
85
|
+
artifactType: 'zip',
|
|
86
|
+
rollout: 1,
|
|
87
|
+
};
|
|
88
|
+
mockFileExistsAtPath.mockResolvedValue(true);
|
|
89
|
+
mockIsDirectory.mockResolvedValue(true);
|
|
90
|
+
mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
|
|
91
|
+
{ href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
|
|
92
|
+
]);
|
|
93
|
+
// Mock utility functions
|
|
94
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
95
|
+
const mockBuffer = await import('../../../utils/buffer.js');
|
|
96
|
+
const mockHash = await import('../../../utils/hash.js');
|
|
97
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(false);
|
|
98
|
+
vi.mocked(mockZip.default.zipFolder).mockResolvedValue(testBuffer);
|
|
99
|
+
vi.mocked(mockHash.createHash).mockResolvedValue('test-hash');
|
|
100
|
+
const appScope = nock(DEFAULT_API_BASE_URL)
|
|
101
|
+
.get(`/v1/apps/${appId}`)
|
|
102
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
103
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
104
|
+
const bundleScope = nock(DEFAULT_API_BASE_URL)
|
|
105
|
+
.post(`/v1/apps/${appId}/bundles`, {
|
|
106
|
+
appId,
|
|
107
|
+
artifactType: 'zip',
|
|
108
|
+
rolloutPercentage: 1,
|
|
109
|
+
})
|
|
110
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
111
|
+
.reply(201, { id: bundleId, appBuildId: 'build-789' });
|
|
112
|
+
const uploadScope = nock(DEFAULT_API_BASE_URL)
|
|
113
|
+
.post(`/v1/apps/${appId}/bundles/${bundleId}/files`)
|
|
114
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
115
|
+
.reply(201, { id: 'file-123' });
|
|
116
|
+
const updateScope = nock(DEFAULT_API_BASE_URL)
|
|
117
|
+
.patch(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
118
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
119
|
+
.reply(200, { id: bundleId });
|
|
120
|
+
await uploadCommand.action(options, undefined);
|
|
121
|
+
expect(appScope.isDone()).toBe(true);
|
|
122
|
+
expect(bundleScope.isDone()).toBe(true);
|
|
123
|
+
expect(uploadScope.isDone()).toBe(true);
|
|
124
|
+
expect(updateScope.isDone()).toBe(true);
|
|
125
|
+
expect(mockConsola.info).toHaveBeenCalledWith(`Build Artifact ID: ${bundleId}`);
|
|
126
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Live Update successfully uploaded.');
|
|
127
|
+
});
|
|
128
|
+
it('should pass gitRef to API when provided', async () => {
|
|
129
|
+
const appId = 'app-123';
|
|
130
|
+
const bundlePath = './dist';
|
|
131
|
+
const bundleId = 'bundle-456';
|
|
132
|
+
const testToken = 'test-token';
|
|
133
|
+
const testBuffer = Buffer.from('test');
|
|
134
|
+
const gitRef = 'main';
|
|
135
|
+
const options = {
|
|
136
|
+
appId,
|
|
137
|
+
path: bundlePath,
|
|
138
|
+
artifactType: 'zip',
|
|
139
|
+
rollout: 1,
|
|
140
|
+
gitRef,
|
|
141
|
+
};
|
|
142
|
+
mockFileExistsAtPath.mockResolvedValue(true);
|
|
143
|
+
mockIsDirectory.mockResolvedValue(true);
|
|
144
|
+
mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
|
|
145
|
+
{ href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
|
|
146
|
+
]);
|
|
147
|
+
// Mock utility functions
|
|
148
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
149
|
+
const mockHash = await import('../../../utils/hash.js');
|
|
150
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(false);
|
|
151
|
+
vi.mocked(mockZip.default.zipFolder).mockResolvedValue(testBuffer);
|
|
152
|
+
vi.mocked(mockHash.createHash).mockResolvedValue('test-hash');
|
|
153
|
+
const appScope = nock(DEFAULT_API_BASE_URL)
|
|
154
|
+
.get(`/v1/apps/${appId}`)
|
|
155
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
156
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
157
|
+
const bundleScope = nock(DEFAULT_API_BASE_URL)
|
|
158
|
+
.post(`/v1/apps/${appId}/bundles`, {
|
|
159
|
+
appId,
|
|
160
|
+
artifactType: 'zip',
|
|
161
|
+
gitRef,
|
|
162
|
+
rolloutPercentage: 1,
|
|
163
|
+
})
|
|
164
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
165
|
+
.reply(201, { id: bundleId, appBuildId: 'build-789' });
|
|
166
|
+
const uploadScope = nock(DEFAULT_API_BASE_URL)
|
|
167
|
+
.post(`/v1/apps/${appId}/bundles/${bundleId}/files`)
|
|
168
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
169
|
+
.reply(201, { id: 'file-123' });
|
|
170
|
+
const updateScope = nock(DEFAULT_API_BASE_URL)
|
|
171
|
+
.patch(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
172
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
173
|
+
.reply(200, { id: bundleId });
|
|
174
|
+
await uploadCommand.action(options, undefined);
|
|
175
|
+
expect(appScope.isDone()).toBe(true);
|
|
176
|
+
expect(bundleScope.isDone()).toBe(true);
|
|
177
|
+
expect(uploadScope.isDone()).toBe(true);
|
|
178
|
+
expect(updateScope.isDone()).toBe(true);
|
|
179
|
+
expect(mockConsola.info).toHaveBeenCalledWith(`Build Artifact ID: ${bundleId}`);
|
|
180
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Live Update successfully uploaded.');
|
|
181
|
+
});
|
|
182
|
+
it('should handle private key file path', async () => {
|
|
183
|
+
const appId = 'app-123';
|
|
184
|
+
const bundlePath = './dist';
|
|
185
|
+
const privateKeyPath = 'private-key.pem';
|
|
186
|
+
const testHash = 'test-hash';
|
|
187
|
+
const testSignature = 'test-signature';
|
|
188
|
+
const bundleId = 'bundle-456';
|
|
189
|
+
const testToken = 'test-token';
|
|
190
|
+
const testBuffer = Buffer.from('test');
|
|
191
|
+
const options = {
|
|
192
|
+
appId,
|
|
193
|
+
path: bundlePath,
|
|
194
|
+
privateKey: privateKeyPath,
|
|
195
|
+
artifactType: 'zip',
|
|
196
|
+
rollout: 1,
|
|
197
|
+
};
|
|
198
|
+
mockFileExistsAtPath.mockImplementation((path) => {
|
|
199
|
+
if (path === privateKeyPath)
|
|
200
|
+
return Promise.resolve(true);
|
|
201
|
+
if (path === bundlePath)
|
|
202
|
+
return Promise.resolve(true);
|
|
203
|
+
return Promise.resolve(false);
|
|
204
|
+
});
|
|
205
|
+
mockIsDirectory.mockResolvedValue(true);
|
|
206
|
+
mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
|
|
207
|
+
{ href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
|
|
208
|
+
]);
|
|
209
|
+
// Mock utility functions
|
|
210
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
211
|
+
const mockBuffer = await import('../../../utils/buffer.js');
|
|
212
|
+
const mockPrivateKey = await import('../../../utils/private-key.js');
|
|
213
|
+
const mockHash = await import('../../../utils/hash.js');
|
|
214
|
+
const mockSignature = await import('../../../utils/signature.js');
|
|
215
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(false);
|
|
216
|
+
vi.mocked(mockZip.default.zipFolder).mockResolvedValue(testBuffer);
|
|
217
|
+
vi.mocked(mockBuffer.createBufferFromPath).mockResolvedValue(testBuffer);
|
|
218
|
+
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
|
|
219
|
+
vi.mocked(mockPrivateKey.formatPrivateKey).mockReturnValue('formatted-private-key');
|
|
220
|
+
vi.mocked(mockBuffer.createBufferFromString).mockReturnValue(testBuffer);
|
|
221
|
+
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
|
|
222
|
+
vi.mocked(mockSignature.createSignature).mockResolvedValue(testSignature);
|
|
223
|
+
const appScope = nock(DEFAULT_API_BASE_URL)
|
|
224
|
+
.get(`/v1/apps/${appId}`)
|
|
225
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
226
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
227
|
+
const bundleScope = nock(DEFAULT_API_BASE_URL)
|
|
228
|
+
.post(`/v1/apps/${appId}/bundles`)
|
|
229
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
230
|
+
.reply(201, { id: bundleId, appBuildId: 'build-789' });
|
|
231
|
+
const uploadScope = nock(DEFAULT_API_BASE_URL)
|
|
232
|
+
.post(`/v1/apps/${appId}/bundles/${bundleId}/files`)
|
|
233
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
234
|
+
.reply(201, { id: 'file-123' });
|
|
235
|
+
const updateScope = nock(DEFAULT_API_BASE_URL)
|
|
236
|
+
.patch(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
237
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
238
|
+
.reply(200, { id: bundleId });
|
|
239
|
+
await uploadCommand.action(options, undefined);
|
|
240
|
+
expect(appScope.isDone()).toBe(true);
|
|
241
|
+
expect(bundleScope.isDone()).toBe(true);
|
|
242
|
+
expect(uploadScope.isDone()).toBe(true);
|
|
243
|
+
expect(updateScope.isDone()).toBe(true);
|
|
244
|
+
expect(mockConsola.info).toHaveBeenCalledWith(`Build Artifact ID: ${bundleId}`);
|
|
245
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Live Update successfully uploaded.');
|
|
246
|
+
});
|
|
247
|
+
it('should handle private key file not found', async () => {
|
|
248
|
+
const appId = 'app-123';
|
|
249
|
+
const privateKeyPath = 'nonexistent-key.pem';
|
|
250
|
+
const options = {
|
|
251
|
+
appId,
|
|
252
|
+
path: './dist',
|
|
253
|
+
privateKey: privateKeyPath,
|
|
254
|
+
artifactType: 'zip',
|
|
255
|
+
rollout: 1,
|
|
256
|
+
};
|
|
257
|
+
mockFileExistsAtPath.mockImplementation((path) => {
|
|
258
|
+
if (path === privateKeyPath)
|
|
259
|
+
return Promise.resolve(false);
|
|
260
|
+
return Promise.resolve(true);
|
|
261
|
+
});
|
|
262
|
+
mockIsDirectory.mockResolvedValue(true);
|
|
263
|
+
mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
|
|
264
|
+
{ href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
|
|
265
|
+
]);
|
|
266
|
+
// Mock utility functions
|
|
267
|
+
const mockBuffer = await import('../../../utils/buffer.js');
|
|
268
|
+
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
|
|
269
|
+
await expect(uploadCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
270
|
+
expect(mockConsola.error).toHaveBeenCalledWith('Private key file not found.');
|
|
271
|
+
});
|
|
272
|
+
it('should handle invalid private key format', async () => {
|
|
273
|
+
const appId = 'app-123';
|
|
274
|
+
const invalidPrivateKey = 'not-a-valid-key';
|
|
275
|
+
const options = {
|
|
276
|
+
appId,
|
|
277
|
+
path: './dist',
|
|
278
|
+
privateKey: invalidPrivateKey,
|
|
279
|
+
artifactType: 'zip',
|
|
280
|
+
rollout: 1,
|
|
281
|
+
};
|
|
282
|
+
mockFileExistsAtPath.mockResolvedValue(true);
|
|
283
|
+
mockIsDirectory.mockResolvedValue(false);
|
|
284
|
+
// Mock zip utility to pass path validation
|
|
285
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
286
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
|
|
287
|
+
// Mock utility functions
|
|
288
|
+
const mockBuffer = await import('../../../utils/buffer.js');
|
|
289
|
+
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
|
|
290
|
+
await expect(uploadCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
291
|
+
expect(mockConsola.error).toHaveBeenCalledWith('Private key must be either a path to a .pem file or the private key content as plain text.');
|
|
292
|
+
});
|
|
293
|
+
it('should handle API error during creation', async () => {
|
|
294
|
+
const appId = 'app-123';
|
|
295
|
+
const bundlePath = './dist';
|
|
296
|
+
const testToken = 'test-token';
|
|
297
|
+
const testBuffer = Buffer.from('test');
|
|
298
|
+
const options = {
|
|
299
|
+
appId,
|
|
300
|
+
path: bundlePath,
|
|
301
|
+
artifactType: 'zip',
|
|
302
|
+
rollout: 1,
|
|
303
|
+
};
|
|
304
|
+
mockFileExistsAtPath.mockResolvedValue(true);
|
|
305
|
+
mockIsDirectory.mockResolvedValue(true);
|
|
306
|
+
mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
|
|
307
|
+
{ href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
|
|
308
|
+
]);
|
|
309
|
+
// Mock utility functions
|
|
310
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
311
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(false);
|
|
312
|
+
vi.mocked(mockZip.default.zipFolder).mockResolvedValue(testBuffer);
|
|
313
|
+
const appScope = nock(DEFAULT_API_BASE_URL)
|
|
314
|
+
.get(`/v1/apps/${appId}`)
|
|
315
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
316
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
317
|
+
const bundleScope = nock(DEFAULT_API_BASE_URL)
|
|
318
|
+
.post(`/v1/apps/${appId}/bundles`)
|
|
319
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
320
|
+
.reply(400, { message: 'Invalid bundle data' });
|
|
321
|
+
await expect(uploadCommand.action(options, undefined)).rejects.toThrow();
|
|
322
|
+
expect(appScope.isDone()).toBe(true);
|
|
323
|
+
expect(bundleScope.isDone()).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { fileExistsAtPath } from '../../utils/file.js';
|
|
2
|
-
import { generateManifestJson } from '../../utils/manifest.js';
|
|
3
|
-
import { prompt } from '../../utils/prompt.js';
|
|
4
1
|
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
5
2
|
import consola from 'consola';
|
|
6
|
-
import { isInteractive } from '../../utils/environment.js';
|
|
7
3
|
import { z } from 'zod';
|
|
8
4
|
export default defineCommand({
|
|
9
5
|
description: 'Generate a manifest file.',
|
|
@@ -11,28 +7,7 @@ export default defineCommand({
|
|
|
11
7
|
path: z.string().optional().describe('Path to the web assets folder (e.g. `www` or `dist`).'),
|
|
12
8
|
})),
|
|
13
9
|
action: async (options, args) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!isInteractive()) {
|
|
17
|
-
consola.error('You must provide the path to the web assets folder when running in non-interactive environment.');
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
path = await prompt('Enter the path to the web assets folder:', {
|
|
21
|
-
type: 'text',
|
|
22
|
-
});
|
|
23
|
-
if (!path) {
|
|
24
|
-
consola.error('You must provide a path to the web assets folder.');
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// Check if the path exists
|
|
29
|
-
const pathExists = await fileExistsAtPath(path);
|
|
30
|
-
if (!pathExists) {
|
|
31
|
-
consola.error(`The path does not exist.`);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
// Generate the manifest file
|
|
35
|
-
await generateManifestJson(path);
|
|
36
|
-
consola.success('Manifest file generated.');
|
|
10
|
+
consola.warn('The `manifests:generate` command has been deprecated. Please use `apps:liveupdates:generatemanifest` instead.');
|
|
11
|
+
process.exit(0);
|
|
37
12
|
},
|
|
38
13
|
});
|
|
@@ -24,7 +24,7 @@ export default defineCommand({
|
|
|
24
24
|
name = await prompt('Enter the name of the organization:', { type: 'text' });
|
|
25
25
|
}
|
|
26
26
|
const response = await organizationsService.create({ name });
|
|
27
|
-
consola.success('Organization created successfully.');
|
|
28
27
|
consola.info(`Organization ID: ${response.id}`);
|
|
28
|
+
consola.success('Organization created successfully.');
|
|
29
29
|
},
|
|
30
30
|
});
|
package/dist/index.js
CHANGED
|
@@ -34,12 +34,25 @@ const config = defineConfig({
|
|
|
34
34
|
'apps:channels:delete': await import('./commands/apps/channels/delete.js').then((mod) => mod.default),
|
|
35
35
|
'apps:channels:get': await import('./commands/apps/channels/get.js').then((mod) => mod.default),
|
|
36
36
|
'apps:channels:list': await import('./commands/apps/channels/list.js').then((mod) => mod.default),
|
|
37
|
+
'apps:channels:pause': await import('./commands/apps/channels/pause.js').then((mod) => mod.default),
|
|
38
|
+
'apps:channels:resume': await import('./commands/apps/channels/resume.js').then((mod) => mod.default),
|
|
37
39
|
'apps:channels:update': await import('./commands/apps/channels/update.js').then((mod) => mod.default),
|
|
40
|
+
'apps:environments:create': await import('./commands/apps/environments/create.js').then((mod) => mod.default),
|
|
41
|
+
'apps:environments:delete': await import('./commands/apps/environments/delete.js').then((mod) => mod.default),
|
|
42
|
+
'apps:environments:list': await import('./commands/apps/environments/list.js').then((mod) => mod.default),
|
|
43
|
+
'apps:environments:set': await import('./commands/apps/environments/set.js').then((mod) => mod.default),
|
|
44
|
+
'apps:environments:unset': await import('./commands/apps/environments/unset.js').then((mod) => mod.default),
|
|
38
45
|
'apps:deployments:create': await import('./commands/apps/deployments/create.js').then((mod) => mod.default),
|
|
39
46
|
'apps:deployments:cancel': await import('./commands/apps/deployments/cancel.js').then((mod) => mod.default),
|
|
40
47
|
'apps:deployments:logs': await import('./commands/apps/deployments/logs.js').then((mod) => mod.default),
|
|
41
48
|
'apps:devices:delete': await import('./commands/apps/devices/delete.js').then((mod) => mod.default),
|
|
49
|
+
'apps:liveupdates:bundle': await import('./commands/apps/liveupdates/bundle.js').then((mod) => mod.default),
|
|
42
50
|
'apps:liveupdates:generatesigningkey': await import('./commands/apps/liveupdates/generate-signing-key.js').then((mod) => mod.default),
|
|
51
|
+
'apps:liveupdates:rollback': await import('./commands/apps/liveupdates/rollback.js').then((mod) => mod.default),
|
|
52
|
+
'apps:liveupdates:rollout': await import('./commands/apps/liveupdates/rollout.js').then((mod) => mod.default),
|
|
53
|
+
'apps:liveupdates:upload': await import('./commands/apps/liveupdates/upload.js').then((mod) => mod.default),
|
|
54
|
+
'apps:liveupdates:register': await import('./commands/apps/liveupdates/register.js').then((mod) => mod.default),
|
|
55
|
+
'apps:liveupdates:generatemanifest': await import('./commands/apps/liveupdates/generate-manifest.js').then((mod) => mod.default),
|
|
43
56
|
'manifests:generate': await import('./commands/manifests/generate.js').then((mod) => mod.default),
|
|
44
57
|
'organizations:create': await import('./commands/organizations/create.js').then((mod) => mod.default),
|
|
45
58
|
},
|
|
@@ -15,11 +15,18 @@ class AppBuildsServiceImpl {
|
|
|
15
15
|
return response.data;
|
|
16
16
|
}
|
|
17
17
|
async findAll(dto) {
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const params = {};
|
|
19
|
+
if (dto.platform) {
|
|
20
|
+
params.platform = dto.platform;
|
|
21
|
+
}
|
|
22
|
+
if (dto.numberAsString) {
|
|
23
|
+
params.numberAsString = dto.numberAsString;
|
|
24
|
+
}
|
|
25
|
+
const response = await this.httpClient.get(`/v1/apps/${dto.appId}/builds`, {
|
|
20
26
|
headers: {
|
|
21
27
|
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
22
28
|
},
|
|
29
|
+
params,
|
|
23
30
|
});
|
|
24
31
|
return response.data;
|
|
25
32
|
}
|
|
@@ -51,13 +51,32 @@ class AppChannelsServiceImpl {
|
|
|
51
51
|
return response.data;
|
|
52
52
|
}
|
|
53
53
|
async findOneById(data) {
|
|
54
|
+
const params = {};
|
|
55
|
+
if (data.relations) {
|
|
56
|
+
params.relations = data.relations;
|
|
57
|
+
}
|
|
54
58
|
const response = await this.httpClient.get(`/v1/apps/${data.appId}/channels/${data.id}`, {
|
|
55
59
|
headers: {
|
|
56
60
|
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
57
61
|
},
|
|
62
|
+
params,
|
|
58
63
|
});
|
|
59
64
|
return response.data;
|
|
60
65
|
}
|
|
66
|
+
async pause(dto) {
|
|
67
|
+
await this.httpClient.post(`/v1/apps/${dto.appId}/channels/${dto.channelId}/pause`, {}, {
|
|
68
|
+
headers: {
|
|
69
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async resume(dto) {
|
|
74
|
+
await this.httpClient.post(`/v1/apps/${dto.appId}/channels/${dto.channelId}/resume`, {}, {
|
|
75
|
+
headers: {
|
|
76
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
61
80
|
async update(dto) {
|
|
62
81
|
const response = await this.httpClient.patch(`/v1/apps/${dto.appId}/channels/${dto.appChannelId}`, dto, {
|
|
63
82
|
headers: {
|
|
@@ -6,14 +6,7 @@ class AppDeploymentsServiceImpl {
|
|
|
6
6
|
this.httpClient = httpClient;
|
|
7
7
|
}
|
|
8
8
|
async create(dto) {
|
|
9
|
-
const
|
|
10
|
-
const bodyData = {
|
|
11
|
-
appBuildId,
|
|
12
|
-
};
|
|
13
|
-
if (appDestinationName) {
|
|
14
|
-
bodyData.appDestinationName = appDestinationName;
|
|
15
|
-
}
|
|
16
|
-
const response = await this.httpClient.post(`/v1/apps/${appId}/deployments`, bodyData, {
|
|
9
|
+
const response = await this.httpClient.post(`/v1/apps/${dto.appId}/deployments`, dto, {
|
|
17
10
|
headers: {
|
|
18
11
|
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
19
12
|
},
|
|
@@ -21,21 +14,30 @@ class AppDeploymentsServiceImpl {
|
|
|
21
14
|
return response.data;
|
|
22
15
|
}
|
|
23
16
|
async findAll(dto) {
|
|
24
|
-
const
|
|
25
|
-
|
|
17
|
+
const params = {};
|
|
18
|
+
if (dto.appChannelId) {
|
|
19
|
+
params.appChannelId = dto.appChannelId;
|
|
20
|
+
}
|
|
21
|
+
if (dto.limit) {
|
|
22
|
+
params.limit = dto.limit.toString();
|
|
23
|
+
}
|
|
24
|
+
if (dto.relations) {
|
|
25
|
+
params.relations = dto.relations;
|
|
26
|
+
}
|
|
27
|
+
const response = await this.httpClient.get(`/v1/apps/${dto.appId}/deployments`, {
|
|
26
28
|
headers: {
|
|
27
29
|
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
28
30
|
},
|
|
31
|
+
params,
|
|
29
32
|
});
|
|
30
33
|
return response.data;
|
|
31
34
|
}
|
|
32
35
|
async findOne(dto) {
|
|
33
|
-
const { appId, appDeploymentId, relations } = dto;
|
|
34
36
|
const params = {};
|
|
35
|
-
if (relations) {
|
|
36
|
-
params.relations = relations;
|
|
37
|
+
if (dto.relations) {
|
|
38
|
+
params.relations = dto.relations;
|
|
37
39
|
}
|
|
38
|
-
const response = await this.httpClient.get(`/v1/apps/${appId}/deployments/${appDeploymentId}`, {
|
|
40
|
+
const response = await this.httpClient.get(`/v1/apps/${dto.appId}/deployments/${dto.appDeploymentId}`, {
|
|
39
41
|
headers: {
|
|
40
42
|
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
41
43
|
},
|
|
@@ -43,6 +45,14 @@ class AppDeploymentsServiceImpl {
|
|
|
43
45
|
});
|
|
44
46
|
return response.data;
|
|
45
47
|
}
|
|
48
|
+
async update(dto) {
|
|
49
|
+
const response = await this.httpClient.patch(`/v1/apps/${dto.appId}/deployments/${dto.appDeploymentId}`, dto, {
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return response.data;
|
|
55
|
+
}
|
|
46
56
|
}
|
|
47
57
|
const appDeploymentsService = new AppDeploymentsServiceImpl(httpClient);
|
|
48
58
|
export default appDeploymentsService;
|
|
@@ -5,15 +5,80 @@ class AppEnvironmentsServiceImpl {
|
|
|
5
5
|
constructor(httpClient) {
|
|
6
6
|
this.httpClient = httpClient;
|
|
7
7
|
}
|
|
8
|
+
async create(dto) {
|
|
9
|
+
const response = await this.httpClient.post(`/v1/apps/${dto.appId}/environments`, dto, {
|
|
10
|
+
headers: {
|
|
11
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
return response.data;
|
|
15
|
+
}
|
|
16
|
+
async delete(dto) {
|
|
17
|
+
if (dto.id) {
|
|
18
|
+
await this.httpClient.delete(`/v1/apps/${dto.appId}/environments/${dto.id}`, {
|
|
19
|
+
headers: {
|
|
20
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
else if (dto.name) {
|
|
25
|
+
await this.httpClient.delete(`/v1/apps/${dto.appId}/environments`, {
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
28
|
+
},
|
|
29
|
+
params: {
|
|
30
|
+
name: dto.name,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
8
35
|
async findAll(dto) {
|
|
9
|
-
const
|
|
10
|
-
|
|
36
|
+
const queryParams = new URLSearchParams();
|
|
37
|
+
if (dto.limit) {
|
|
38
|
+
queryParams.append('limit', dto.limit.toString());
|
|
39
|
+
}
|
|
40
|
+
if (dto.offset) {
|
|
41
|
+
queryParams.append('offset', dto.offset.toString());
|
|
42
|
+
}
|
|
43
|
+
const queryString = queryParams.toString();
|
|
44
|
+
const url = queryString
|
|
45
|
+
? `/v1/apps/${dto.appId}/environments?${queryString}`
|
|
46
|
+
: `/v1/apps/${dto.appId}/environments`;
|
|
47
|
+
const response = await this.httpClient.get(url, {
|
|
11
48
|
headers: {
|
|
12
49
|
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
13
50
|
},
|
|
14
51
|
});
|
|
15
52
|
return response.data;
|
|
16
53
|
}
|
|
54
|
+
async setVariables(dto) {
|
|
55
|
+
await this.httpClient.post(`/v1/apps/${dto.appId}/environments/${dto.environmentId}/variables/set`, dto.variables, {
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async setSecrets(dto) {
|
|
62
|
+
await this.httpClient.post(`/v1/apps/${dto.appId}/environments/${dto.environmentId}/secrets/set`, dto.secrets, {
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async unsetVariables(dto) {
|
|
69
|
+
await this.httpClient.post(`/v1/apps/${dto.appId}/environments/${dto.environmentId}/variables/unset`, dto.keys, {
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async unsetSecrets(dto) {
|
|
76
|
+
await this.httpClient.post(`/v1/apps/${dto.appId}/environments/${dto.environmentId}/secrets/unset`, dto.keys, {
|
|
77
|
+
headers: {
|
|
78
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
17
82
|
}
|
|
18
83
|
const appEnvironmentsService = new AppEnvironmentsServiceImpl(httpClient);
|
|
19
84
|
export default appEnvironmentsService;
|
package/dist/services/config.js
CHANGED
|
@@ -9,6 +9,7 @@ class ConfigServiceImpl {
|
|
|
9
9
|
return (await this.config)[key];
|
|
10
10
|
}
|
|
11
11
|
async loadConfig() {
|
|
12
|
+
const isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true';
|
|
12
13
|
const { config } = await loadConfig({
|
|
13
14
|
defaults: {
|
|
14
15
|
API_BASE_URL: DEFAULT_API_BASE_URL,
|
|
@@ -16,6 +17,7 @@ class ConfigServiceImpl {
|
|
|
16
17
|
ENVIRONMENT: 'production',
|
|
17
18
|
},
|
|
18
19
|
name: 'capawesome',
|
|
20
|
+
rcFile: isTestEnvironment ? false : undefined,
|
|
19
21
|
});
|
|
20
22
|
return config;
|
|
21
23
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse key-value pairs from content string.
|
|
3
|
+
*
|
|
4
|
+
* Format: KEY=value (one per line)
|
|
5
|
+
* - Empty lines are ignored
|
|
6
|
+
* - Lines starting with # are ignored (comments)
|
|
7
|
+
* - Lines without = are skipped
|
|
8
|
+
* - Keys and values are trimmed
|
|
9
|
+
* - Values can contain = characters
|
|
10
|
+
* - Lines with empty keys are skipped
|
|
11
|
+
* - Lines with empty values are skipped
|
|
12
|
+
*
|
|
13
|
+
* @param content - Content string to parse
|
|
14
|
+
* @returns Array of key-value pairs
|
|
15
|
+
*/
|
|
16
|
+
export function parseKeyValuePairs(content) {
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
const pairs = [];
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const separatorIndex = trimmed.indexOf('=');
|
|
25
|
+
if (separatorIndex === -1) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
29
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
30
|
+
if (key && value) {
|
|
31
|
+
pairs.push({ key, value });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return pairs;
|
|
35
|
+
}
|