@capawesome/cli 4.3.0 → 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 CHANGED
@@ -2,6 +2,15 @@
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
+
5
14
  ## [4.3.0](https://github.com/capawesome-team/cli/compare/v4.2.1...v4.3.0) (2026-03-07)
6
15
 
7
16
 
@@ -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
- const expiresAtDate = new Date();
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 create channel with expiresInDays option', async () => {
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`, (body) => {
124
- // Verify the request includes expiresAt and it's approximately correct (within 1 minute)
125
- const actualExpiresAt = new Date(body.expiresAt);
126
- const timeDiff = Math.abs(actualExpiresAt.getTime() - expectedExpiresAt.getTime());
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
+ });
@@ -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
- const expiresAtDate = new Date();
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,
@@ -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
- const expiresAtDate = new Date();
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) {
@@ -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,
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),
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "4.3.0",
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": {