@capawesome/cli 3.11.0 → 4.0.1

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 (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/commands/apps/builds/cancel.js +1 -1
  3. package/dist/commands/apps/builds/create.js +58 -50
  4. package/dist/commands/apps/builds/download.js +27 -3
  5. package/dist/commands/apps/bundles/create.js +5 -449
  6. package/dist/commands/apps/bundles/delete.js +3 -68
  7. package/dist/commands/apps/bundles/update.js +3 -66
  8. package/dist/commands/apps/channels/create.js +5 -8
  9. package/dist/commands/apps/channels/create.test.js +6 -9
  10. package/dist/commands/apps/channels/delete.js +3 -2
  11. package/dist/commands/apps/channels/get.js +2 -12
  12. package/dist/commands/apps/channels/get.test.js +1 -2
  13. package/dist/commands/apps/channels/list.js +2 -10
  14. package/dist/commands/apps/channels/list.test.js +2 -3
  15. package/dist/commands/apps/channels/pause.js +85 -0
  16. package/dist/commands/apps/channels/resume.js +85 -0
  17. package/dist/commands/apps/channels/update.js +4 -7
  18. package/dist/commands/apps/channels/update.test.js +2 -4
  19. package/dist/commands/apps/create.js +1 -1
  20. package/dist/commands/apps/delete.js +3 -2
  21. package/dist/commands/apps/deployments/cancel.js +1 -1
  22. package/dist/commands/apps/deployments/create.js +82 -31
  23. package/dist/commands/apps/devices/delete.js +3 -2
  24. package/dist/commands/apps/environments/create.js +1 -1
  25. package/dist/commands/apps/environments/delete.js +3 -2
  26. package/dist/commands/apps/liveupdates/bundle.js +117 -0
  27. package/dist/commands/apps/liveupdates/generate-manifest.js +39 -0
  28. package/dist/commands/{manifests/generate.test.js → apps/liveupdates/generate-manifest.test.js} +6 -6
  29. package/dist/commands/apps/liveupdates/register.js +291 -0
  30. package/dist/commands/apps/{bundles/create.test.js → liveupdates/register.test.js} +123 -111
  31. package/dist/commands/apps/liveupdates/rollback.js +171 -0
  32. package/dist/commands/apps/liveupdates/rollout.js +147 -0
  33. package/dist/commands/apps/liveupdates/upload.js +420 -0
  34. package/dist/commands/apps/liveupdates/upload.test.js +325 -0
  35. package/dist/commands/manifests/generate.js +2 -27
  36. package/dist/commands/organizations/create.js +1 -1
  37. package/dist/index.js +8 -0
  38. package/dist/services/app-builds.js +9 -2
  39. package/dist/services/app-channels.js +19 -0
  40. package/dist/services/app-deployments.js +24 -14
  41. package/dist/services/config.js +2 -0
  42. package/dist/utils/app-environments.js +2 -1
  43. package/dist/utils/time-format.js +26 -0
  44. package/package.json +3 -3
  45. package/dist/commands/apps/bundles/delete.test.js +0 -142
  46. package/dist/commands/apps/bundles/update.test.js +0 -144
  47. package/dist/utils/capacitor-config.js +0 -96
  48. 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
- let path = options.path;
15
- if (!path) {
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(1);
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,6 +34,8 @@ 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),
38
40
  'apps:environments:create': await import('./commands/apps/environments/create.js').then((mod) => mod.default),
39
41
  'apps:environments:delete': await import('./commands/apps/environments/delete.js').then((mod) => mod.default),
@@ -44,7 +46,13 @@ const config = defineConfig({
44
46
  'apps:deployments:cancel': await import('./commands/apps/deployments/cancel.js').then((mod) => mod.default),
45
47
  'apps:deployments:logs': await import('./commands/apps/deployments/logs.js').then((mod) => mod.default),
46
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),
47
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),
48
56
  'manifests:generate': await import('./commands/manifests/generate.js').then((mod) => mod.default),
49
57
  'organizations:create': await import('./commands/organizations/create.js').then((mod) => mod.default),
50
58
  },
@@ -15,11 +15,18 @@ class AppBuildsServiceImpl {
15
15
  return response.data;
16
16
  }
17
17
  async findAll(dto) {
18
- const { appId } = dto;
19
- const response = await this.httpClient.get(`/v1/apps/${appId}/builds`, {
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 { appId, appBuildId, appDestinationName } = dto;
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 { appId } = dto;
25
- const response = await this.httpClient.get(`/v1/apps/${appId}/deployments`, {
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;
@@ -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
  }
@@ -8,6 +8,7 @@
8
8
  * - Keys and values are trimmed
9
9
  * - Values can contain = characters
10
10
  * - Lines with empty keys are skipped
11
+ * - Lines with empty values are skipped
11
12
  *
12
13
  * @param content - Content string to parse
13
14
  * @returns Array of key-value pairs
@@ -26,7 +27,7 @@ export function parseKeyValuePairs(content) {
26
27
  }
27
28
  const key = trimmed.slice(0, separatorIndex).trim();
28
29
  const value = trimmed.slice(separatorIndex + 1).trim();
29
- if (key) {
30
+ if (key && value) {
30
31
  pairs.push({ key, value });
31
32
  }
32
33
  }
@@ -0,0 +1,26 @@
1
+ export function formatTimeAgo(timestamp) {
2
+ const now = Date.now();
3
+ const diffInMs = now - timestamp;
4
+ const diffInSeconds = Math.floor(diffInMs / 1000);
5
+ if (diffInSeconds < 60) {
6
+ return 'just now';
7
+ }
8
+ const diffInMinutes = Math.floor(diffInSeconds / 60);
9
+ if (diffInMinutes < 60) {
10
+ return diffInMinutes === 1 ? '1 minute ago' : `${diffInMinutes} minutes ago`;
11
+ }
12
+ const diffInHours = Math.floor(diffInMinutes / 60);
13
+ if (diffInHours < 24) {
14
+ return diffInHours === 1 ? '1 hour ago' : `${diffInHours} hours ago`;
15
+ }
16
+ const diffInDays = Math.floor(diffInHours / 24);
17
+ if (diffInDays < 7) {
18
+ return diffInDays === 1 ? '1 day ago' : `${diffInDays} days ago`;
19
+ }
20
+ const diffInWeeks = Math.floor(diffInDays / 7);
21
+ if (diffInWeeks < 52) {
22
+ return diffInWeeks === 1 ? '1 week ago' : `${diffInWeeks} weeks ago`;
23
+ }
24
+ const diffInYears = Math.floor(diffInDays / 365);
25
+ return diffInYears === 1 ? '1 year ago' : `${diffInYears} years ago`;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "3.11.0",
3
+ "version": "4.0.1",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -20,8 +20,8 @@
20
20
  "postpublish": "npm run sentry:releases:finalize"
21
21
  },
22
22
  "engines": {
23
- "npm": ">=8.0.0",
24
- "node": ">=18.0.0"
23
+ "npm": ">=10.0.0",
24
+ "node": ">=20.0.0"
25
25
  },
26
26
  "bin": {
27
27
  "capawesome": "./dist/index.js"