@capawesome/cli 4.2.1 → 4.4.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 +24 -0
- package/dist/commands/apps/channels/create.js +1 -6
- package/dist/commands/apps/channels/create.test.js +6 -11
- package/dist/commands/apps/devices/forcechannel.js +66 -0
- package/dist/commands/apps/devices/unforcechannel.js +40 -0
- package/dist/commands/apps/liveupdates/bundle.js +5 -2
- package/dist/commands/apps/liveupdates/register.js +1 -6
- package/dist/commands/apps/liveupdates/upload.js +18 -19
- package/dist/config/consts.js +2 -1
- package/dist/index.js +2 -0
- package/dist/services/app-bundle-files.js +10 -9
- package/dist/services/app-devices.js +7 -0
- package/dist/utils/time-format.js +1 -1
- package/dist/utils/zip.js +4 -10
- package/package.json +3 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [4.4.0](https://github.com/capawesome-team/cli/compare/v4.3.0...v4.4.0) (2026-03-13)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add `apps:devices:forcechannel` and `apps:devices:unforcechannel` commands ([#124](https://github.com/capawesome-team/cli/issues/124)) ([c47c15f](https://github.com/capawesome-team/cli/commit/c47c15fc8328824973dd9062bfac2bca2e847752))
|
|
11
|
+
* add deprecation warning for `--expires-in-days` option ([#123](https://github.com/capawesome-team/cli/issues/123)) ([db628a2](https://github.com/capawesome-team/cli/commit/db628a21b50b826d59a1b29eb3913c1f6ce225bf))
|
|
12
|
+
* add warning when using manifest artifact type ([42a6b2b](https://github.com/capawesome-team/cli/commit/42a6b2bfae0cc958127d4ff60562000eb9713943))
|
|
13
|
+
|
|
14
|
+
## [4.3.0](https://github.com/capawesome-team/cli/compare/v4.2.1...v4.3.0) (2026-03-07)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* add part-level progress for multipart zip uploads ([#122](https://github.com/capawesome-team/cli/issues/122)) ([f74cda4](https://github.com/capawesome-team/cli/commit/f74cda48f454dfbc786052498ebecccef98e6fc5))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **deps:** remove `glob` dependency ([#120](https://github.com/capawesome-team/cli/issues/120)) ([80709de](https://github.com/capawesome-team/cli/commit/80709deac3292b976f3633f3335004d2cb80fc22))
|
|
25
|
+
* read deployment ID from update response ([#121](https://github.com/capawesome-team/cli/issues/121)) ([9ef9979](https://github.com/capawesome-team/cli/commit/9ef99794a5bb14631fcecf41c1a9885931627dfd))
|
|
26
|
+
* use ISO string timestamps ([#119](https://github.com/capawesome-team/cli/issues/119)) ([7fe8263](https://github.com/capawesome-team/cli/commit/7fe8263956fc83ca7a93d803e6867133fea8c879))
|
|
27
|
+
* validate prompt response in bundle command ([#118](https://github.com/capawesome-team/cli/issues/118)) ([a1541a0](https://github.com/capawesome-team/cli/commit/a1541a074f614b42bf69f2c6a186eeef5d95200b))
|
|
28
|
+
|
|
5
29
|
## [4.2.1](https://github.com/capawesome-team/cli/compare/v4.2.0...v4.2.1) (2026-03-01)
|
|
6
30
|
|
|
7
31
|
## [4.2.0](https://github.com/capawesome-team/cli/compare/v4.1.0...v4.2.0) (2026-02-20)
|
|
@@ -25,12 +25,8 @@ export default defineCommand({
|
|
|
25
25
|
})),
|
|
26
26
|
action: withAuth(async (options, args) => {
|
|
27
27
|
let { appId, expiresInDays, ignoreErrors, name, protected: _protected } = options;
|
|
28
|
-
// Calculate the expiration date
|
|
29
|
-
let expiresAt;
|
|
30
28
|
if (expiresInDays) {
|
|
31
|
-
|
|
32
|
-
expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
|
|
33
|
-
expiresAt = expiresAtDate.toISOString();
|
|
29
|
+
consola.warn('The `--expires-in-days` option is deprecated and will be removed in a future version. Channel expiration is now managed by the data retention policy of your organization billing plan.');
|
|
34
30
|
}
|
|
35
31
|
// Validate the app ID
|
|
36
32
|
if (!appId) {
|
|
@@ -54,7 +50,6 @@ export default defineCommand({
|
|
|
54
50
|
appId,
|
|
55
51
|
protected: _protected,
|
|
56
52
|
name,
|
|
57
|
-
expiresAt,
|
|
58
53
|
});
|
|
59
54
|
consola.info(`Channel ID: ${response.id}`);
|
|
60
55
|
consola.success('Channel created successfully.');
|
|
@@ -109,29 +109,24 @@ describe('apps-channels-create', () => {
|
|
|
109
109
|
await expect(createChannelCommand.action(options, undefined)).rejects.toThrow();
|
|
110
110
|
expect(scope.isDone()).toBe(true);
|
|
111
111
|
});
|
|
112
|
-
it('should
|
|
112
|
+
it('should show warning when expiresInDays option is used', async () => {
|
|
113
113
|
const appId = 'app-123';
|
|
114
114
|
const channelName = 'production';
|
|
115
115
|
const expiresInDays = 30;
|
|
116
116
|
const channelId = 'channel-456';
|
|
117
117
|
const testToken = 'test-token';
|
|
118
118
|
const options = { appId, name: channelName, expiresInDays };
|
|
119
|
-
// Calculate expected expiration date
|
|
120
|
-
const expectedExpiresAt = new Date();
|
|
121
|
-
expectedExpiresAt.setDate(expectedExpiresAt.getDate() + expiresInDays);
|
|
122
119
|
const scope = nock(DEFAULT_API_BASE_URL)
|
|
123
|
-
.post(`/v1/apps/${appId}/channels`,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const oneMinute = 60 * 1000;
|
|
128
|
-
return (body.appId === appId && body.name === channelName && body.protected === undefined && timeDiff < oneMinute);
|
|
120
|
+
.post(`/v1/apps/${appId}/channels`, {
|
|
121
|
+
appId,
|
|
122
|
+
name: channelName,
|
|
123
|
+
protected: undefined,
|
|
129
124
|
})
|
|
130
125
|
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
131
126
|
.reply(201, { id: channelId, name: channelName });
|
|
132
127
|
await createChannelCommand.action(options, undefined);
|
|
133
128
|
expect(scope.isDone()).toBe(true);
|
|
129
|
+
expect(mockConsola.warn).toHaveBeenCalledWith('The `--expires-in-days` option is deprecated and will be removed in a future version. Channel expiration is now managed by the data retention policy of your organization billing plan.');
|
|
134
130
|
expect(mockConsola.success).toHaveBeenCalledWith('Channel created successfully.');
|
|
135
|
-
expect(mockConsola.info).toHaveBeenCalledWith(`Channel ID: ${channelId}`);
|
|
136
131
|
});
|
|
137
132
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import appChannelsService from '../../../services/app-channels.js';
|
|
2
|
+
import appDevicesService from '../../../services/app-devices.js';
|
|
3
|
+
import { withAuth } from '../../../utils/auth.js';
|
|
4
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
5
|
+
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
6
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
7
|
+
import consola from 'consola';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
description: 'Force a device to use a specific channel.',
|
|
11
|
+
options: defineOptions(z.object({
|
|
12
|
+
appId: z.string().uuid({ message: 'App ID must be a UUID.' }).optional().describe('ID of the app.'),
|
|
13
|
+
deviceId: z.string().optional().describe('ID of the device.'),
|
|
14
|
+
channel: z.string().optional().describe('Name of the channel to force.'),
|
|
15
|
+
})),
|
|
16
|
+
action: withAuth(async (options, args) => {
|
|
17
|
+
let { appId, deviceId, channel } = options;
|
|
18
|
+
if (!appId) {
|
|
19
|
+
if (!isInteractive()) {
|
|
20
|
+
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const organizationId = await promptOrganizationSelection();
|
|
24
|
+
appId = await promptAppSelection(organizationId);
|
|
25
|
+
}
|
|
26
|
+
if (!deviceId) {
|
|
27
|
+
if (!isInteractive()) {
|
|
28
|
+
consola.error('You must provide the device ID when running in non-interactive environment.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
deviceId = await prompt('Enter the device ID:', {
|
|
32
|
+
type: 'text',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!channel) {
|
|
36
|
+
if (!isInteractive()) {
|
|
37
|
+
consola.error('You must provide a channel when running in non-interactive environment.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
41
|
+
channel = await prompt('Enter the name of the channel to force:', {
|
|
42
|
+
type: 'text',
|
|
43
|
+
});
|
|
44
|
+
if (!channel) {
|
|
45
|
+
consola.error('You must provide a channel name.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const channels = await appChannelsService.findAll({ appId, name: channel });
|
|
50
|
+
if (channels.length === 0) {
|
|
51
|
+
consola.error('Channel not found.');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const channelId = channels[0]?.id;
|
|
55
|
+
if (!channelId) {
|
|
56
|
+
consola.error('Channel ID not found.');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
await appDevicesService.update({
|
|
60
|
+
appId,
|
|
61
|
+
deviceId,
|
|
62
|
+
forcedAppChannelId: channelId,
|
|
63
|
+
});
|
|
64
|
+
consola.success('Device forced to channel successfully.');
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import appDevicesService from '../../../services/app-devices.js';
|
|
2
|
+
import { withAuth } from '../../../utils/auth.js';
|
|
3
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
4
|
+
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
5
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
6
|
+
import consola from 'consola';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
export default defineCommand({
|
|
9
|
+
description: 'Remove the forced channel from a device.',
|
|
10
|
+
options: defineOptions(z.object({
|
|
11
|
+
appId: z.string().uuid({ message: 'App ID must be a UUID.' }).optional().describe('ID of the app.'),
|
|
12
|
+
deviceId: z.string().optional().describe('ID of the device.'),
|
|
13
|
+
})),
|
|
14
|
+
action: withAuth(async (options, args) => {
|
|
15
|
+
let { appId, deviceId } = options;
|
|
16
|
+
if (!appId) {
|
|
17
|
+
if (!isInteractive()) {
|
|
18
|
+
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const organizationId = await promptOrganizationSelection();
|
|
22
|
+
appId = await promptAppSelection(organizationId);
|
|
23
|
+
}
|
|
24
|
+
if (!deviceId) {
|
|
25
|
+
if (!isInteractive()) {
|
|
26
|
+
consola.error('You must provide the device ID when running in non-interactive environment.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
deviceId = await prompt('Enter the device ID:', {
|
|
30
|
+
type: 'text',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
await appDevicesService.update({
|
|
34
|
+
appId,
|
|
35
|
+
deviceId,
|
|
36
|
+
forcedAppChannelId: null,
|
|
37
|
+
});
|
|
38
|
+
consola.success('Forced channel removed from device successfully.');
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
@@ -33,10 +33,13 @@ export default defineCommand({
|
|
|
33
33
|
process.exit(1);
|
|
34
34
|
}
|
|
35
35
|
consola.warn('Make sure you have built your web assets before creating a bundle (e.g., `npm run build`).');
|
|
36
|
-
|
|
36
|
+
inputPath = await prompt('Enter the path to the web assets directory (e.g., `dist` or `www`):', {
|
|
37
37
|
type: 'text',
|
|
38
38
|
});
|
|
39
|
-
inputPath
|
|
39
|
+
if (!inputPath) {
|
|
40
|
+
consola.error('You must provide an input path.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
40
43
|
}
|
|
41
44
|
// Convert to absolute path
|
|
42
45
|
inputPath = pathModule.resolve(inputPath);
|
|
@@ -102,12 +102,8 @@ export default defineCommand({
|
|
|
102
102
|
}), { y: 'yes' }),
|
|
103
103
|
action: withAuth(async (options, args) => {
|
|
104
104
|
let { androidEq, androidMax, androidMin, appId, channel, commitMessage, commitRef, commitSha, customProperty, expiresInDays, gitRef, iosEq, iosMax, iosMin, path, privateKey, rolloutPercentage, url, } = options;
|
|
105
|
-
// Calculate the expiration date
|
|
106
|
-
let expiresAt;
|
|
107
105
|
if (expiresInDays) {
|
|
108
|
-
|
|
109
|
-
expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
|
|
110
|
-
expiresAt = expiresAtDate.toISOString();
|
|
106
|
+
consola.warn('The `--expires-in-days` option is deprecated and will be removed in a future version. Bundle expiration is now managed by the data retention policy of your organization billing plan.');
|
|
111
107
|
}
|
|
112
108
|
// Prompt for url if not provided
|
|
113
109
|
if (!url) {
|
|
@@ -226,7 +222,6 @@ export default defineCommand({
|
|
|
226
222
|
gitCommitSha: commitSha,
|
|
227
223
|
gitRef,
|
|
228
224
|
customProperties: parseCustomProperties(customProperty),
|
|
229
|
-
expiresAt,
|
|
230
225
|
url,
|
|
231
226
|
maxAndroidAppVersionCode: androidMax,
|
|
232
227
|
maxIosAppVersionCode: iosMax,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_CONSOLE_BASE_URL,
|
|
1
|
+
import { DEFAULT_CONSOLE_BASE_URL, MAX_CONCURRENT_FILE_UPLOADS } from '../../../config/index.js';
|
|
2
2
|
import appBundleFilesService from '../../../services/app-bundle-files.js';
|
|
3
3
|
import appBundlesService from '../../../services/app-bundles.js';
|
|
4
4
|
import appsService from '../../../services/apps.js';
|
|
@@ -115,12 +115,8 @@ export default defineCommand({
|
|
|
115
115
|
}), { y: 'yes' }),
|
|
116
116
|
action: withAuth(async (options, args) => {
|
|
117
117
|
let { androidEq, androidMax, androidMin, appId, artifactType, channel, commitMessage, commitRef, commitSha, customProperty, expiresInDays, gitRef, iosEq, iosMax, iosMin, path, privateKey, rolloutPercentage, } = options;
|
|
118
|
-
// Calculate the expiration date
|
|
119
|
-
let expiresAt;
|
|
120
118
|
if (expiresInDays) {
|
|
121
|
-
|
|
122
|
-
expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
|
|
123
|
-
expiresAt = expiresAtDate.toISOString();
|
|
119
|
+
consola.warn('The `--expires-in-days` option is deprecated and will be removed in a future version. Bundle expiration is now managed by the data retention policy of your organization billing plan.');
|
|
124
120
|
}
|
|
125
121
|
// Prompt for path if not provided
|
|
126
122
|
if (!path) {
|
|
@@ -167,6 +163,7 @@ export default defineCommand({
|
|
|
167
163
|
consola.error('The path must be a folder when creating a bundle with an artifact type of `manifest`.');
|
|
168
164
|
process.exit(1);
|
|
169
165
|
}
|
|
166
|
+
consola.warn('The `zip` artifact type is faster and more efficient for most apps. The `manifest` type can result in longer downloads and more network requests if your files change frequently between builds. Learn more: https://capawesome.io/cloud/live-updates/advanced/delta-updates/');
|
|
170
167
|
}
|
|
171
168
|
// Prompt for appId if not provided
|
|
172
169
|
if (!appId) {
|
|
@@ -236,7 +233,7 @@ export default defineCommand({
|
|
|
236
233
|
}
|
|
237
234
|
// Create the app bundle
|
|
238
235
|
consola.start('Creating bundle...');
|
|
239
|
-
const
|
|
236
|
+
const createBundleResponse = await appBundlesService.create({
|
|
240
237
|
appId,
|
|
241
238
|
artifactType,
|
|
242
239
|
channelName: channel,
|
|
@@ -247,7 +244,6 @@ export default defineCommand({
|
|
|
247
244
|
gitCommitSha: commitSha,
|
|
248
245
|
gitRef,
|
|
249
246
|
customProperties: parseCustomProperties(customProperty),
|
|
250
|
-
expiresAt,
|
|
251
247
|
maxAndroidAppVersionCode: androidMax,
|
|
252
248
|
maxIosAppVersionCode: iosMax,
|
|
253
249
|
minAndroidAppVersionCode: androidMin,
|
|
@@ -258,29 +254,29 @@ export default defineCommand({
|
|
|
258
254
|
let appBundleFileId;
|
|
259
255
|
// Upload the app bundle files
|
|
260
256
|
if (artifactType === 'manifest') {
|
|
261
|
-
await uploadFiles({ appId, appBundleId:
|
|
257
|
+
await uploadFiles({ appId, appBundleId: createBundleResponse.id, path, privateKeyBuffer });
|
|
262
258
|
}
|
|
263
259
|
else {
|
|
264
|
-
const result = await uploadZip({ appId, appBundleId:
|
|
260
|
+
const result = await uploadZip({ appId, appBundleId: createBundleResponse.id, path, privateKeyBuffer });
|
|
265
261
|
appBundleFileId = result.appBundleFileId;
|
|
266
262
|
}
|
|
267
263
|
// Update the app bundle
|
|
268
264
|
consola.start('Updating bundle...');
|
|
269
|
-
await appBundlesService.update({
|
|
265
|
+
const updateBundleResponse = await appBundlesService.update({
|
|
270
266
|
appBundleFileId,
|
|
271
267
|
appId,
|
|
272
268
|
artifactStatus: 'ready',
|
|
273
|
-
appBundleId:
|
|
269
|
+
appBundleId: createBundleResponse.id,
|
|
274
270
|
});
|
|
275
|
-
consola.info(`Build Artifact ID: ${
|
|
276
|
-
if (
|
|
277
|
-
consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${
|
|
271
|
+
consola.info(`Build Artifact ID: ${createBundleResponse.id}`);
|
|
272
|
+
if (updateBundleResponse.appDeploymentId) {
|
|
273
|
+
consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${updateBundleResponse.appDeploymentId}`);
|
|
278
274
|
}
|
|
279
275
|
consola.success('Live Update successfully uploaded.');
|
|
280
276
|
}),
|
|
281
277
|
});
|
|
282
278
|
const uploadFile = async (options) => {
|
|
283
|
-
let { appId, appBundleId, buffer, href, mimeType, name, privateKeyBuffer, retryOnFailure } = options;
|
|
279
|
+
let { appId, appBundleId, buffer, href, mimeType, name, onProgress, privateKeyBuffer, retryOnFailure } = options;
|
|
284
280
|
try {
|
|
285
281
|
// Generate checksum
|
|
286
282
|
const hash = await createHash(buffer);
|
|
@@ -299,7 +295,7 @@ const uploadFile = async (options) => {
|
|
|
299
295
|
mimeType,
|
|
300
296
|
name,
|
|
301
297
|
signature,
|
|
302
|
-
});
|
|
298
|
+
}, onProgress);
|
|
303
299
|
}
|
|
304
300
|
catch (error) {
|
|
305
301
|
if (retryOnFailure) {
|
|
@@ -339,8 +335,8 @@ const uploadFiles = async (options) => {
|
|
|
339
335
|
});
|
|
340
336
|
await uploadNextFile();
|
|
341
337
|
};
|
|
342
|
-
const uploadPromises = Array.from({ length:
|
|
343
|
-
for (let i = 0; i <
|
|
338
|
+
const uploadPromises = Array.from({ length: MAX_CONCURRENT_FILE_UPLOADS });
|
|
339
|
+
for (let i = 0; i < MAX_CONCURRENT_FILE_UPLOADS; i++) {
|
|
344
340
|
uploadPromises[i] = uploadNextFile();
|
|
345
341
|
}
|
|
346
342
|
await Promise.all(uploadPromises);
|
|
@@ -365,6 +361,9 @@ const uploadZip = async (options) => {
|
|
|
365
361
|
buffer: fileBuffer,
|
|
366
362
|
mimeType: 'application/zip',
|
|
367
363
|
name: 'bundle.zip',
|
|
364
|
+
onProgress: (completed, total) => {
|
|
365
|
+
consola.start(`Uploading file (part ${completed}/${total})...`);
|
|
366
|
+
},
|
|
368
367
|
privateKeyBuffer: privateKeyBuffer,
|
|
369
368
|
});
|
|
370
369
|
return {
|
package/dist/config/consts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const DEFAULT_API_BASE_URL = 'https://api.cloud.capawesome.io';
|
|
2
2
|
export const DEFAULT_CONSOLE_BASE_URL = 'https://console.cloud.capawesome.io';
|
|
3
3
|
export const MANIFEST_JSON_FILE_NAME = 'capawesome-live-update-manifest.json'; // Do NOT change this!
|
|
4
|
-
export const
|
|
4
|
+
export const MAX_CONCURRENT_FILE_UPLOADS = 20;
|
|
5
|
+
export const MAX_CONCURRENT_PART_UPLOADS = 4;
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,8 @@ const config = defineConfig({
|
|
|
46
46
|
'apps:deployments:cancel': await import('./commands/apps/deployments/cancel.js').then((mod) => mod.default),
|
|
47
47
|
'apps:deployments:logs': await import('./commands/apps/deployments/logs.js').then((mod) => mod.default),
|
|
48
48
|
'apps:devices:delete': await import('./commands/apps/devices/delete.js').then((mod) => mod.default),
|
|
49
|
+
'apps:devices:forcechannel': await import('./commands/apps/devices/forcechannel.js').then((mod) => mod.default),
|
|
50
|
+
'apps:devices:unforcechannel': await import('./commands/apps/devices/unforcechannel.js').then((mod) => mod.default),
|
|
49
51
|
'apps:liveupdates:bundle': await import('./commands/apps/liveupdates/bundle.js').then((mod) => mod.default),
|
|
50
52
|
'apps:liveupdates:generatesigningkey': await import('./commands/apps/liveupdates/generate-signing-key.js').then((mod) => mod.default),
|
|
51
53
|
'apps:liveupdates:rollback': await import('./commands/apps/liveupdates/rollback.js').then((mod) => mod.default),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MAX_CONCURRENT_PART_UPLOADS } from '../config/index.js';
|
|
2
2
|
import authorizationService from '../services/authorization-service.js';
|
|
3
3
|
import httpClient from '../utils/http-client.js';
|
|
4
4
|
import FormData from 'form-data';
|
|
@@ -7,7 +7,7 @@ class AppBundleFilesServiceImpl {
|
|
|
7
7
|
constructor(httpClient) {
|
|
8
8
|
this.httpClient = httpClient;
|
|
9
9
|
}
|
|
10
|
-
async create(dto) {
|
|
10
|
+
async create(dto, onProgress) {
|
|
11
11
|
const sizeInBytes = dto.buffer.byteLength;
|
|
12
12
|
const useMultipartUpload = sizeInBytes >= 50 * 1024 * 1024; // 50 MB
|
|
13
13
|
const formData = new FormData();
|
|
@@ -38,7 +38,7 @@ class AppBundleFilesServiceImpl {
|
|
|
38
38
|
buffer: dto.buffer,
|
|
39
39
|
name: dto.name,
|
|
40
40
|
checksum: dto.checksum,
|
|
41
|
-
});
|
|
41
|
+
}, onProgress);
|
|
42
42
|
}
|
|
43
43
|
return response.data;
|
|
44
44
|
}
|
|
@@ -74,7 +74,7 @@ class AppBundleFilesServiceImpl {
|
|
|
74
74
|
})
|
|
75
75
|
.then((response) => response.data);
|
|
76
76
|
}
|
|
77
|
-
async createUploadParts(dto) {
|
|
77
|
+
async createUploadParts(dto, onProgress) {
|
|
78
78
|
const uploadedParts = [];
|
|
79
79
|
const partSize = 10 * 1024 * 1024; // 10 MB. 5 MB is the minimum part size except for the last part.
|
|
80
80
|
const totalParts = Math.ceil(dto.buffer.byteLength / partSize);
|
|
@@ -84,6 +84,7 @@ class AppBundleFilesServiceImpl {
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
partNumber++;
|
|
87
|
+
onProgress?.(partNumber, totalParts);
|
|
87
88
|
const start = (partNumber - 1) * partSize;
|
|
88
89
|
const end = Math.min(start + partSize, dto.buffer.byteLength);
|
|
89
90
|
const partBuffer = dto.buffer.subarray(start, end);
|
|
@@ -99,14 +100,14 @@ class AppBundleFilesServiceImpl {
|
|
|
99
100
|
uploadedParts.push(uploadedPart);
|
|
100
101
|
await uploadNextPart();
|
|
101
102
|
};
|
|
102
|
-
const uploadPartPromises = Array.from({ length:
|
|
103
|
-
for (let i = 0; i <
|
|
103
|
+
const uploadPartPromises = Array.from({ length: MAX_CONCURRENT_PART_UPLOADS });
|
|
104
|
+
for (let i = 0; i < MAX_CONCURRENT_PART_UPLOADS; i++) {
|
|
104
105
|
uploadPartPromises[i] = uploadNextPart();
|
|
105
106
|
}
|
|
106
107
|
await Promise.all(uploadPartPromises);
|
|
107
|
-
return uploadedParts;
|
|
108
|
+
return uploadedParts.sort((a, b) => a.partNumber - b.partNumber);
|
|
108
109
|
}
|
|
109
|
-
async upload(dto) {
|
|
110
|
+
async upload(dto, onProgress) {
|
|
110
111
|
// 1. Create a multipart upload
|
|
111
112
|
const { uploadId } = await this.createUpload({
|
|
112
113
|
appBundleFileId: dto.appBundleFileId,
|
|
@@ -121,7 +122,7 @@ class AppBundleFilesServiceImpl {
|
|
|
121
122
|
buffer: dto.buffer,
|
|
122
123
|
name: dto.name,
|
|
123
124
|
uploadId,
|
|
124
|
-
});
|
|
125
|
+
}, onProgress);
|
|
125
126
|
// 3. Complete the upload
|
|
126
127
|
await this.completeUpload({
|
|
127
128
|
appBundleFileId: dto.appBundleFileId,
|
|
@@ -12,6 +12,13 @@ class AppDevicesServiceImpl {
|
|
|
12
12
|
},
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
+
async update(data) {
|
|
16
|
+
await this.httpClient.patch(`/v1/apps/${data.appId}/devices/${data.deviceId}`, { forcedAppChannelId: data.forcedAppChannelId }, {
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
15
22
|
}
|
|
16
23
|
const appDevicesService = new AppDevicesServiceImpl(httpClient);
|
|
17
24
|
export default appDevicesService;
|
package/dist/utils/zip.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import AdmZip from 'adm-zip';
|
|
2
2
|
class ZipImpl {
|
|
3
3
|
async zipFolder(sourceFolder) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
archive.on('data', (data) => buffers.push(data));
|
|
8
|
-
archive.on('error', (err) => reject(err));
|
|
9
|
-
archive.on('end', () => resolve(Buffer.concat(buffers)));
|
|
10
|
-
archive.directory(sourceFolder, false);
|
|
11
|
-
archive.finalize();
|
|
12
|
-
});
|
|
4
|
+
const zip = new AdmZip();
|
|
5
|
+
zip.addLocalFolder(sourceFolder);
|
|
6
|
+
return zip.toBuffer();
|
|
13
7
|
}
|
|
14
8
|
isZipped(path) {
|
|
15
9
|
return path.endsWith('.zip');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capawesome/cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -51,15 +51,11 @@
|
|
|
51
51
|
"url": "https://opencollective.com/capawesome"
|
|
52
52
|
}
|
|
53
53
|
],
|
|
54
|
-
"overrides": {
|
|
55
|
-
"glob": "13.0.6",
|
|
56
|
-
"minimatch": "10.2.4"
|
|
57
|
-
},
|
|
58
54
|
"dependencies": {
|
|
59
55
|
"@clack/prompts": "0.7.0",
|
|
60
56
|
"@robingenz/zli": "0.2.0",
|
|
61
57
|
"@sentry/node": "8.55.0",
|
|
62
|
-
"
|
|
58
|
+
"adm-zip": "0.5.16",
|
|
63
59
|
"axios": "1.13.5",
|
|
64
60
|
"axios-retry": "4.5.0",
|
|
65
61
|
"c12": "3.3.3",
|
|
@@ -77,7 +73,7 @@
|
|
|
77
73
|
"devDependencies": {
|
|
78
74
|
"@ionic/prettier-config": "4.0.0",
|
|
79
75
|
"@sentry/cli": "2.52.0",
|
|
80
|
-
"@types/
|
|
76
|
+
"@types/adm-zip": "0.5.7",
|
|
81
77
|
"@types/mime": "3.0.4",
|
|
82
78
|
"@types/node": "24.2.1",
|
|
83
79
|
"@types/semver": "7.5.8",
|