@capawesome/cli 4.5.0 → 4.6.0-dev.80c962a.1774597304

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,13 @@
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.6.0](https://github.com/capawesome-team/cli/compare/v4.5.0...v4.6.0) (2026-03-18)
6
+
7
+
8
+ ### Features
9
+
10
+ * add `apps:devices:probe` command ([#129](https://github.com/capawesome-team/cli/issues/129)) ([33607cd](https://github.com/capawesome-team/cli/commit/33607cdb2e65e26c48d114d694b876db3762b8ec))
11
+
5
12
  ## [4.5.0](https://github.com/capawesome-team/cli/compare/v4.4.0...v4.5.0) (2026-03-15)
6
13
 
7
14
 
@@ -3,6 +3,7 @@ import appBuildsService from '../../../services/app-builds.js';
3
3
  import appCertificatesService from '../../../services/app-certificates.js';
4
4
  import appEnvironmentsService from '../../../services/app-environments.js';
5
5
  import { unescapeAnsi } from '../../../utils/ansi.js';
6
+ import { parseKeyValuePairs } from '../../../utils/app-environments.js';
6
7
  import { withAuth } from '../../../utils/auth.js';
7
8
  import { isInteractive } from '../../../utils/environment.js';
8
9
  import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
@@ -61,6 +62,14 @@ export default defineCommand({
61
62
  .string()
62
63
  .optional()
63
64
  .describe('The type of build. For iOS, supported values are `simulator`, `development`, `ad-hoc`, `app-store`, and `enterprise`. For Android, supported values are `debug` and `release`. For Web, no type is required.'),
65
+ variable: z
66
+ .array(z.string())
67
+ .optional()
68
+ .describe('Ad hoc environment variable in key=value format. Can be specified multiple times.'),
69
+ variableFile: z
70
+ .string()
71
+ .optional()
72
+ .describe('Path to a file containing ad hoc environment variables in .env format.'),
64
73
  zip: z
65
74
  .union([z.boolean(), z.string()])
66
75
  .optional()
@@ -197,9 +206,22 @@ export default defineCommand({
197
206
  }
198
207
  }
199
208
  }
209
+ // Parse ad hoc environment variables from inline and file
210
+ const variablesMap = new Map();
211
+ if (options.variableFile) {
212
+ const fileContent = await fs.readFile(options.variableFile, 'utf-8');
213
+ const fileVariables = parseKeyValuePairs(fileContent);
214
+ fileVariables.forEach((v) => variablesMap.set(v.key, v.value));
215
+ }
216
+ if (options.variable) {
217
+ const inlineVariables = parseKeyValuePairs(options.variable.join('\n'));
218
+ inlineVariables.forEach((v) => variablesMap.set(v.key, v.value));
219
+ }
220
+ const adHocEnvironmentVariables = variablesMap.size > 0 ? Object.fromEntries(variablesMap) : undefined;
200
221
  // Create the app build
201
222
  consola.start('Creating build...');
202
223
  const response = await appBuildsService.create({
224
+ adHocEnvironmentVariables,
203
225
  appCertificateName: certificate,
204
226
  appEnvironmentName: environment,
205
227
  appId,
@@ -74,24 +74,9 @@ export default defineCommand({
74
74
  process.exit(1);
75
75
  }
76
76
  }
77
- // 4. Select certificate type
78
- if (!type) {
79
- if (!isInteractive()) {
80
- consola.error('You must provide the certificate type when running in non-interactive environment.');
81
- process.exit(1);
82
- }
83
- // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
84
- type = await prompt('Select the certificate type:', {
85
- type: 'select',
86
- options: [
87
- { label: 'Development', value: 'development' },
88
- { label: 'Production', value: 'production' },
89
- ],
90
- });
91
- if (!type) {
92
- consola.error('You must select a certificate type.');
93
- process.exit(1);
94
- }
77
+ // 4. Warn if deprecated --type option is used
78
+ if (type) {
79
+ consola.warn('The --type option is deprecated and will be removed in a future version. The certificate type is now detected automatically.');
95
80
  }
96
81
  // 5. Enter certificate file path
97
82
  if (!file) {
@@ -167,7 +152,6 @@ export default defineCommand({
167
152
  fileName,
168
153
  name,
169
154
  platform: platform,
170
- type: type,
171
155
  password,
172
156
  keyAlias,
173
157
  keyPassword,
@@ -36,11 +36,13 @@ export default defineCommand({
36
36
  }
37
37
  certificateId = await prompt('Enter the certificate ID:', { type: 'text' });
38
38
  }
39
+ if (type) {
40
+ consola.warn('The --type option is deprecated and will be removed in a future version. The certificate type is now detected automatically.');
41
+ }
39
42
  await appCertificatesService.update({
40
43
  appId,
41
44
  certificateId,
42
45
  name,
43
- type,
44
46
  password,
45
47
  keyAlias,
46
48
  keyPassword,
@@ -10,11 +10,11 @@ export default defineCommand({
10
10
  description: 'Force a device to use a specific channel.',
11
11
  options: defineOptions(z.object({
12
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.'),
13
+ deviceId: z.array(z.string()).optional().describe('ID of the device. Can be specified multiple times.'),
14
14
  channel: z.string().optional().describe('Name of the channel to force.'),
15
15
  })),
16
16
  action: withAuth(async (options, args) => {
17
- let { appId, deviceId, channel } = options;
17
+ let { appId, deviceId: deviceIds, channel } = options;
18
18
  if (!appId) {
19
19
  if (!isInteractive()) {
20
20
  consola.error('You must provide an app ID when running in non-interactive environment.');
@@ -23,14 +23,15 @@ export default defineCommand({
23
23
  const organizationId = await promptOrganizationSelection();
24
24
  appId = await promptAppSelection(organizationId);
25
25
  }
26
- if (!deviceId) {
26
+ if (!deviceIds || deviceIds.length === 0) {
27
27
  if (!isInteractive()) {
28
28
  consola.error('You must provide the device ID when running in non-interactive environment.');
29
29
  process.exit(1);
30
30
  }
31
- deviceId = await prompt('Enter the device ID:', {
31
+ const deviceId = await prompt('Enter the device ID:', {
32
32
  type: 'text',
33
33
  });
34
+ deviceIds = [deviceId];
34
35
  }
35
36
  if (!channel) {
36
37
  if (!isInteractive()) {
@@ -56,11 +57,12 @@ export default defineCommand({
56
57
  consola.error('Channel ID not found.');
57
58
  process.exit(1);
58
59
  }
59
- await appDevicesService.update({
60
+ await appDevicesService.updateMany({
60
61
  appId,
61
- deviceId,
62
+ deviceIds,
62
63
  forcedAppChannelId: channelId,
63
64
  });
64
- consola.success('Device forced to channel successfully.');
65
+ const deviceCount = deviceIds.length;
66
+ consola.success(`${deviceCount === 1 ? 'Device' : `${deviceCount} devices`} forced to channel successfully.`);
65
67
  }),
66
68
  });
@@ -0,0 +1,70 @@
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 { AxiosError } from 'axios';
7
+ import consola from 'consola';
8
+ import { z } from 'zod';
9
+ export default defineCommand({
10
+ description: 'Check whether a device would receive a live update.',
11
+ options: defineOptions(z.object({
12
+ appId: z.string().optional().describe('ID of the app.'),
13
+ deviceId: z.string().optional().describe('ID of the device.'),
14
+ json: z.boolean().optional().describe('Output in JSON format.'),
15
+ })),
16
+ action: withAuth(async (options) => {
17
+ let { appId, deviceId, json } = 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
+ const device = await appDevicesService.findOneById({ appId, deviceId });
36
+ try {
37
+ const result = await appDevicesService.probe({
38
+ appId,
39
+ appVersionCode: device.appVersionCode,
40
+ appVersionName: device.appVersionName,
41
+ channelName: device.appChannel?.name,
42
+ customId: device.customId ?? undefined,
43
+ deviceId: device.id,
44
+ osVersion: device.osVersion,
45
+ platform: device.platform,
46
+ pluginVersion: device.pluginVersion,
47
+ });
48
+ if (json) {
49
+ console.log(JSON.stringify(result, null, 2));
50
+ }
51
+ else {
52
+ console.table(result);
53
+ consola.success('Update available for this device.');
54
+ }
55
+ }
56
+ catch (error) {
57
+ if (error instanceof AxiosError && error.response?.status === 404) {
58
+ if (json) {
59
+ console.log(JSON.stringify({ bundleId: null }, null, 2));
60
+ }
61
+ else {
62
+ consola.info('No update available for this device.');
63
+ }
64
+ }
65
+ else {
66
+ throw error;
67
+ }
68
+ }
69
+ }),
70
+ });
@@ -9,10 +9,10 @@ export default defineCommand({
9
9
  description: 'Remove the forced channel from a device.',
10
10
  options: defineOptions(z.object({
11
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.'),
12
+ deviceId: z.array(z.string()).optional().describe('ID of the device. Can be specified multiple times.'),
13
13
  })),
14
14
  action: withAuth(async (options, args) => {
15
- let { appId, deviceId } = options;
15
+ let { appId, deviceId: deviceIds } = options;
16
16
  if (!appId) {
17
17
  if (!isInteractive()) {
18
18
  consola.error('You must provide an app ID when running in non-interactive environment.');
@@ -21,20 +21,22 @@ export default defineCommand({
21
21
  const organizationId = await promptOrganizationSelection();
22
22
  appId = await promptAppSelection(organizationId);
23
23
  }
24
- if (!deviceId) {
24
+ if (!deviceIds || deviceIds.length === 0) {
25
25
  if (!isInteractive()) {
26
26
  consola.error('You must provide the device ID when running in non-interactive environment.');
27
27
  process.exit(1);
28
28
  }
29
- deviceId = await prompt('Enter the device ID:', {
29
+ const deviceId = await prompt('Enter the device ID:', {
30
30
  type: 'text',
31
31
  });
32
+ deviceIds = [deviceId];
32
33
  }
33
- await appDevicesService.update({
34
+ await appDevicesService.updateMany({
34
35
  appId,
35
- deviceId,
36
+ deviceIds,
36
37
  forcedAppChannelId: null,
37
38
  });
38
- consola.success('Forced channel removed from device successfully.');
39
+ const deviceCount = deviceIds.length;
40
+ consola.success(`Forced channel removed from ${deviceCount === 1 ? 'device' : `${deviceCount} devices`} successfully.`);
39
41
  }),
40
42
  });
@@ -1,5 +1,5 @@
1
1
  import { isInteractive } from '../../../utils/environment.js';
2
- import { fileExistsAtPath, isDirectory } from '../../../utils/file.js';
2
+ import { directoryContainsSourceMaps, fileExistsAtPath, isDirectory } from '../../../utils/file.js';
3
3
  import { generateManifestJson } from '../../../utils/manifest.js';
4
4
  import { prompt } from '../../../utils/prompt.js';
5
5
  import zip from '../../../utils/zip.js';
@@ -62,6 +62,11 @@ export default defineCommand({
62
62
  consola.error(`Directory must contain an index.html file: ${inputPath}`);
63
63
  process.exit(1);
64
64
  }
65
+ // Check for source maps
66
+ const containsSourceMaps = await directoryContainsSourceMaps(inputPath);
67
+ if (containsSourceMaps) {
68
+ consola.warn('Source map files were detected in the specified path. Source maps should not be distributed to end users as they expose your original source code and increase the download size. Consider excluding source map files from your build output.');
69
+ }
65
70
  // 2. Output path resolution
66
71
  if (!outputPath) {
67
72
  outputPath = './bundle.zip';
@@ -1,5 +1,5 @@
1
1
  import { isInteractive } from '../../../utils/environment.js';
2
- import { fileExistsAtPath } from '../../../utils/file.js';
2
+ import { directoryContainsSourceMaps, fileExistsAtPath } from '../../../utils/file.js';
3
3
  import { generateManifestJson } from '../../../utils/manifest.js';
4
4
  import { prompt } from '../../../utils/prompt.js';
5
5
  import { defineCommand, defineOptions } from '@robingenz/zli';
@@ -32,6 +32,11 @@ export default defineCommand({
32
32
  consola.error(`The path does not exist.`);
33
33
  process.exit(1);
34
34
  }
35
+ // Check for source maps
36
+ const containsSourceMaps = await directoryContainsSourceMaps(path);
37
+ if (containsSourceMaps) {
38
+ consola.warn('Source map files were detected in the specified path. Source maps should not be distributed to end users as they expose your original source code and increase the download size. Consider excluding source map files from your build output.');
39
+ }
35
40
  // Generate the manifest file
36
41
  await generateManifestJson(path);
37
42
  consola.success('Manifest file generated.');
@@ -5,7 +5,7 @@ import appsService from '../../../services/apps.js';
5
5
  import { withAuth } from '../../../utils/auth.js';
6
6
  import { createBufferFromPath, createBufferFromReadStream, createBufferFromString, isPrivateKeyContent, } from '../../../utils/buffer.js';
7
7
  import { isInteractive } from '../../../utils/environment.js';
8
- import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
8
+ import { directoryContainsSourceMaps, fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory, } from '../../../utils/file.js';
9
9
  import { createHash } from '../../../utils/hash.js';
10
10
  import { generateManifestJson } from '../../../utils/manifest.js';
11
11
  import { formatPrivateKey } from '../../../utils/private-key.js';
@@ -156,6 +156,13 @@ export default defineCommand({
156
156
  consola.error('The path must be either a folder or a zip file.');
157
157
  process.exit(1);
158
158
  }
159
+ // Check for source maps
160
+ if (pathIsDirectory) {
161
+ const containsSourceMaps = await directoryContainsSourceMaps(path);
162
+ if (containsSourceMaps) {
163
+ consola.warn('Source map files were detected in the specified path. Source maps should not be distributed to end users as they expose your original source code and increase the download size. Consider excluding source map files from your build output.');
164
+ }
165
+ }
159
166
  // Check that the path is a directory when creating a bundle with an artifact type of manifest
160
167
  if (artifactType === 'manifest') {
161
168
  const pathIsDirectory = await isDirectory(path);
package/dist/index.js CHANGED
@@ -52,6 +52,7 @@ const config = defineConfig({
52
52
  'apps:destinations:update': await import('./commands/apps/destinations/update.js').then((mod) => mod.default),
53
53
  'apps:devices:delete': await import('./commands/apps/devices/delete.js').then((mod) => mod.default),
54
54
  'apps:devices:forcechannel': await import('./commands/apps/devices/forcechannel.js').then((mod) => mod.default),
55
+ 'apps:devices:probe': await import('./commands/apps/devices/probe.js').then((mod) => mod.default),
55
56
  'apps:devices:unforcechannel': await import('./commands/apps/devices/unforcechannel.js').then((mod) => mod.default),
56
57
  'apps:environments:create': await import('./commands/apps/environments/create.js').then((mod) => mod.default),
57
58
  'apps:environments:delete': await import('./commands/apps/environments/delete.js').then((mod) => mod.default),
@@ -11,7 +11,6 @@ class AppCertificatesServiceImpl {
11
11
  formData.append('file', dto.buffer, { filename: dto.fileName });
12
12
  formData.append('name', dto.name);
13
13
  formData.append('platform', dto.platform);
14
- formData.append('type', dto.type);
15
14
  if (dto.password) {
16
15
  formData.append('password', dto.password);
17
16
  }
@@ -12,6 +12,42 @@ class AppDevicesServiceImpl {
12
12
  },
13
13
  });
14
14
  }
15
+ async findOneById(data) {
16
+ const response = await this.httpClient.get(`/v1/apps/${data.appId}/devices/${data.deviceId}`, {
17
+ headers: {
18
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
19
+ },
20
+ params: {
21
+ relations: 'appChannel',
22
+ },
23
+ });
24
+ return response.data;
25
+ }
26
+ async probe(data) {
27
+ const params = {
28
+ appVersionCode: data.appVersionCode,
29
+ appVersionName: data.appVersionName,
30
+ osVersion: data.osVersion,
31
+ platform: data.platform.toString(),
32
+ pluginVersion: data.pluginVersion,
33
+ };
34
+ if (data.channelName) {
35
+ params.channelName = data.channelName;
36
+ }
37
+ if (data.customId) {
38
+ params.customId = data.customId;
39
+ }
40
+ if (data.deviceId) {
41
+ params.deviceId = data.deviceId;
42
+ }
43
+ const response = await this.httpClient.get(`/v1/apps/${data.appId}/bundles/latest`, {
44
+ headers: {
45
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
46
+ },
47
+ params,
48
+ });
49
+ return response.data;
50
+ }
15
51
  async update(data) {
16
52
  await this.httpClient.patch(`/v1/apps/${data.appId}/devices/${data.deviceId}`, { forcedAppChannelId: data.forcedAppChannelId }, {
17
53
  headers: {
@@ -19,6 +55,14 @@ class AppDevicesServiceImpl {
19
55
  },
20
56
  });
21
57
  }
58
+ async updateMany(data) {
59
+ const ids = data.deviceIds.join(',');
60
+ await this.httpClient.patch(`/v1/apps/${data.appId}/devices?ids=${ids}`, { forcedAppChannelId: data.forcedAppChannelId }, {
61
+ headers: {
62
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
63
+ },
64
+ });
65
+ }
22
66
  }
23
67
  const appDevicesService = new AppDevicesServiceImpl(httpClient);
24
68
  export default appDevicesService;
@@ -35,6 +35,10 @@ export const getFilesInDirectoryAndSubdirectories = async (path) => {
35
35
  await walk(path);
36
36
  return files;
37
37
  };
38
+ export const directoryContainsSourceMaps = async (path) => {
39
+ const files = await getFilesInDirectoryAndSubdirectories(path);
40
+ return files.some((file) => file.name.endsWith('.js.map') || file.name.endsWith('.css.map'));
41
+ };
38
42
  export const fileExistsAtPath = async (path) => {
39
43
  return new Promise((resolve) => {
40
44
  fs.access(path, fs.constants.F_OK, (err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "4.5.0",
3
+ "version": "4.6.0-dev.80c962a.1774597304",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {