@capawesome/cli 4.4.0 → 4.5.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,21 @@
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.5.0](https://github.com/capawesome-team/cli/compare/v4.4.0...v4.5.0) (2026-03-15)
6
+
7
+
8
+ ### Features
9
+
10
+ * add certificate and destination CRUD commands ([#126](https://github.com/capawesome-team/cli/issues/126)) ([2d5e3cf](https://github.com/capawesome-team/cli/commit/2d5e3cfc80518fc1237dbc926a57c4a2eddf2b31))
11
+ * **apps:builds:create:** add `--channel` and `--destination` options ([#127](https://github.com/capawesome-team/cli/issues/127)) ([baab841](https://github.com/capawesome-team/cli/commit/baab841d330c7bf230dbbf8e0819f3aaf248867b))
12
+ * **update:** add dev version detection in update check ([#128](https://github.com/capawesome-team/cli/issues/128)) ([5004095](https://github.com/capawesome-team/cli/commit/50040958cd2a873ff296981b0950b3a799505a93))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **apps:builds:create:** allow certificate selection for web platform ([a038251](https://github.com/capawesome-team/cli/commit/a03825128b8380f86dcf85e8d3507b3a41d9f775))
18
+ * create parent directories before writing signing key files ([#125](https://github.com/capawesome-team/cli/issues/125)) ([e152606](https://github.com/capawesome-team/cli/commit/e152606d1d9c1758e922379e5bbda0764261c711))
19
+
5
20
  ## [4.4.0](https://github.com/capawesome-team/cli/compare/v4.3.0...v4.4.0) (2026-03-13)
6
21
 
7
22
 
@@ -32,6 +32,8 @@ export default defineCommand({
32
32
  .optional()
33
33
  .describe('App ID to create the build for.'),
34
34
  certificate: z.string().optional().describe('The name of the certificate to use for the build.'),
35
+ channel: z.string().optional().describe('The name of the channel to deploy to (Web only).'),
36
+ destination: z.string().optional().describe('The name of the destination to deploy to (Android/iOS only).'),
35
37
  detached: z
36
38
  .boolean()
37
39
  .optional()
@@ -72,6 +74,16 @@ export default defineCommand({
72
74
  consola.error('The --detached flag cannot be used with --apk, --aab, --ipa, or --zip flags.');
73
75
  process.exit(1);
74
76
  }
77
+ // Validate that detached flag cannot be used with channel or destination
78
+ if (options.detached && (options.channel || options.destination)) {
79
+ consola.error('The --detached flag cannot be used with --channel or --destination flags.');
80
+ process.exit(1);
81
+ }
82
+ // Validate that channel and destination cannot be used together
83
+ if (options.channel && options.destination) {
84
+ consola.error('The --channel and --destination flags cannot be used together.');
85
+ process.exit(1);
86
+ }
75
87
  // Prompt for app ID if not provided
76
88
  if (!appId) {
77
89
  if (!isInteractive()) {
@@ -133,6 +145,16 @@ export default defineCommand({
133
145
  consola.error(`Invalid build type for Android. Supported values are: ${ANDROID_BUILD_TYPES.map((t) => `\`${t}\``).join(', ')}.`);
134
146
  process.exit(1);
135
147
  }
148
+ // Validate that channel is only used with web platform
149
+ if (options.channel && platform !== 'web') {
150
+ consola.error('The --channel flag can only be used with the web platform.');
151
+ process.exit(1);
152
+ }
153
+ // Validate that destination is only used with non-web platforms
154
+ if (options.destination && platform === 'web') {
155
+ consola.error('The --destination flag cannot be used with the web platform.');
156
+ process.exit(1);
157
+ }
136
158
  // Prompt for environment if not provided
137
159
  if (!environment && !options.yes && isInteractive()) {
138
160
  // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
@@ -154,8 +176,8 @@ export default defineCommand({
154
176
  }
155
177
  }
156
178
  }
157
- // Prompt for certificate if not provided (skip for web platform)
158
- if (!certificate && !options.yes && isInteractive() && platform !== 'web') {
179
+ // Prompt for certificate if not provided
180
+ if (!certificate && !options.yes && isInteractive()) {
159
181
  // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
160
182
  const selectCertificate = await prompt('Do you want to select a certificate?', {
161
183
  type: 'confirm',
@@ -288,8 +310,7 @@ export default defineCommand({
288
310
  numberAsString: response.numberAsString,
289
311
  }, null, 2));
290
312
  }
291
- // Exit successfully
292
- process.exit(0);
313
+ break;
293
314
  }
294
315
  else if (jobStatus === 'failed') {
295
316
  consola.error('Build failed.');
@@ -331,6 +352,10 @@ export default defineCommand({
331
352
  consola.success(`Build completed successfully.`);
332
353
  }
333
354
  }
355
+ // Create deployment if channel or destination is set
356
+ if (options.channel || options.destination) {
357
+ await (await import('../../../commands/apps/deployments/create.js').then((mod) => mod.default)).action({ appId, buildId: response.id, channel: options.channel, destination: options.destination }, undefined);
358
+ }
334
359
  }),
335
360
  });
336
361
  /**
@@ -0,0 +1,186 @@
1
+ import appCertificatesService from '../../../services/app-certificates.js';
2
+ import appProvisioningProfilesService from '../../../services/app-provisioning-profiles.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 fs from 'fs';
9
+ import path from 'path';
10
+ import { z } from 'zod';
11
+ export default defineCommand({
12
+ description: 'Create a new app certificate.',
13
+ options: defineOptions(z.object({
14
+ appId: z.string().optional().describe('ID of the app.'),
15
+ file: z.string().optional().describe('Path to the certificate file.'),
16
+ keyAlias: z.string().optional().describe('Key alias for the certificate.'),
17
+ keyPassword: z.string().optional().describe('Key password for the certificate.'),
18
+ name: z.string().optional().describe('Name of the certificate.'),
19
+ password: z.string().optional().describe('Password for the certificate.'),
20
+ platform: z
21
+ .enum(['android', 'ios', 'web'])
22
+ .optional()
23
+ .describe('Platform of the certificate (android, ios, web).'),
24
+ provisioningProfile: z
25
+ .array(z.string())
26
+ .optional()
27
+ .describe('Paths to provisioning profile files to upload and link.'),
28
+ type: z
29
+ .enum(['development', 'production'])
30
+ .optional()
31
+ .describe('Type of the certificate (development, production).'),
32
+ yes: z.boolean().optional().describe('Skip confirmation prompts.'),
33
+ }), { y: 'yes' }),
34
+ action: withAuth(async (options, args) => {
35
+ let { appId, file, keyAlias, keyPassword, name, password, platform, provisioningProfile, type } = options;
36
+ // 1. Select organization and app
37
+ if (!appId) {
38
+ if (!isInteractive()) {
39
+ consola.error('You must provide an app ID when running in non-interactive environment.');
40
+ process.exit(1);
41
+ }
42
+ const organizationId = await promptOrganizationSelection();
43
+ appId = await promptAppSelection(organizationId);
44
+ }
45
+ // 2. Enter certificate name
46
+ if (!name) {
47
+ if (!isInteractive()) {
48
+ consola.error('You must provide the certificate name when running in non-interactive environment.');
49
+ process.exit(1);
50
+ }
51
+ name = await prompt('Enter the name of the certificate:', { type: 'text' });
52
+ if (!name) {
53
+ consola.error('You must provide a certificate name.');
54
+ process.exit(1);
55
+ }
56
+ }
57
+ // 3. Select platform
58
+ if (!platform) {
59
+ if (!isInteractive()) {
60
+ consola.error('You must provide the platform when running in non-interactive environment.');
61
+ process.exit(1);
62
+ }
63
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
64
+ platform = await prompt('Select the platform:', {
65
+ type: 'select',
66
+ options: [
67
+ { label: 'Android', value: 'android' },
68
+ { label: 'iOS', value: 'ios' },
69
+ { label: 'Web', value: 'web' },
70
+ ],
71
+ });
72
+ if (!platform) {
73
+ consola.error('You must select a platform.');
74
+ process.exit(1);
75
+ }
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
+ }
95
+ }
96
+ // 5. Enter certificate file path
97
+ if (!file) {
98
+ if (!isInteractive()) {
99
+ consola.error('You must provide a certificate file path when running in non-interactive environment.');
100
+ process.exit(1);
101
+ }
102
+ file = await prompt('Enter the path to the certificate file:', { type: 'text' });
103
+ if (!file) {
104
+ consola.error('You must provide a certificate file path.');
105
+ process.exit(1);
106
+ }
107
+ }
108
+ // 6. Enter certificate password (required for android/ios)
109
+ if (!password && (platform === 'android' || platform === 'ios')) {
110
+ if (!isInteractive()) {
111
+ consola.error('You must provide the certificate password when running in non-interactive environment.');
112
+ process.exit(1);
113
+ }
114
+ password = await prompt('Enter the certificate password:', { type: 'text' });
115
+ if (!password) {
116
+ consola.error('You must provide a certificate password.');
117
+ process.exit(1);
118
+ }
119
+ }
120
+ // 7. If Android, ask for key alias (required)
121
+ if (!keyAlias && platform === 'android') {
122
+ if (!isInteractive()) {
123
+ consola.error('You must provide the key alias when running in non-interactive environment.');
124
+ process.exit(1);
125
+ }
126
+ keyAlias = await prompt('Enter the key alias:', { type: 'text' });
127
+ if (!keyAlias) {
128
+ consola.error('You must provide a key alias.');
129
+ process.exit(1);
130
+ }
131
+ }
132
+ // 8. If Android, ask for key password (optional, skip with --yes)
133
+ if (!keyPassword && platform === 'android' && !options.yes && isInteractive()) {
134
+ keyPassword = await prompt('Enter the key password (leave empty if none):', { type: 'text' });
135
+ if (!keyPassword) {
136
+ keyPassword = undefined;
137
+ }
138
+ }
139
+ // 9. If iOS, ask for provisioning profile file path (optional, skip with --yes)
140
+ if (!provisioningProfile && platform === 'ios' && !options.yes && isInteractive()) {
141
+ const profilePath = await prompt('Enter the path to the provisioning profile file (leave empty to skip):', {
142
+ type: 'text',
143
+ });
144
+ if (profilePath) {
145
+ provisioningProfile = [profilePath];
146
+ }
147
+ }
148
+ const buffer = fs.readFileSync(file);
149
+ const fileName = path.basename(file);
150
+ // Upload provisioning profiles first
151
+ const provisioningProfileIds = [];
152
+ if (provisioningProfile && provisioningProfile.length > 0) {
153
+ for (const profilePath of provisioningProfile) {
154
+ const profileBuffer = fs.readFileSync(profilePath);
155
+ const profileFileName = path.basename(profilePath);
156
+ const profile = await appProvisioningProfilesService.create({
157
+ appId,
158
+ buffer: profileBuffer,
159
+ fileName: profileFileName,
160
+ });
161
+ provisioningProfileIds.push(profile.id);
162
+ }
163
+ }
164
+ const certificate = await appCertificatesService.create({
165
+ appId,
166
+ buffer,
167
+ fileName,
168
+ name,
169
+ platform: platform,
170
+ type: type,
171
+ password,
172
+ keyAlias,
173
+ keyPassword,
174
+ });
175
+ // Link provisioning profiles to the certificate
176
+ if (provisioningProfileIds.length > 0) {
177
+ await appProvisioningProfilesService.updateMany({
178
+ appId,
179
+ ids: provisioningProfileIds,
180
+ appCertificateId: certificate.id,
181
+ });
182
+ }
183
+ consola.info(`Certificate ID: ${certificate.id}`);
184
+ consola.success('Certificate created successfully.');
185
+ }),
186
+ });
@@ -0,0 +1,79 @@
1
+ import appCertificatesService from '../../../services/app-certificates.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: 'Delete an app certificate.',
10
+ options: defineOptions(z.object({
11
+ appId: z.string().optional().describe('ID of the app.'),
12
+ certificateId: z.string().optional().describe('ID of the certificate.'),
13
+ name: z.string().optional().describe('Name of the certificate.'),
14
+ platform: z
15
+ .enum(['android', 'ios', 'web'])
16
+ .optional()
17
+ .describe('Platform of the certificate (android, ios, web).'),
18
+ yes: z.boolean().optional().describe('Skip confirmation prompt.'),
19
+ }), { y: 'yes' }),
20
+ action: withAuth(async (options, args) => {
21
+ let { appId, certificateId, name, platform } = options;
22
+ if (!appId) {
23
+ if (!isInteractive()) {
24
+ consola.error('You must provide an app ID when running in non-interactive environment.');
25
+ process.exit(1);
26
+ }
27
+ const organizationId = await promptOrganizationSelection();
28
+ appId = await promptAppSelection(organizationId);
29
+ }
30
+ if (!certificateId) {
31
+ if (name && platform) {
32
+ const certificates = await appCertificatesService.findAll({ appId, name, platform });
33
+ const firstCertificate = certificates[0];
34
+ if (!firstCertificate) {
35
+ consola.error(`No certificate found with name '${name}' and platform '${platform}'.`);
36
+ process.exit(1);
37
+ }
38
+ certificateId = firstCertificate.id;
39
+ }
40
+ else if (isInteractive()) {
41
+ if (!platform) {
42
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
43
+ platform = await prompt('Select the platform:', {
44
+ type: 'select',
45
+ options: [
46
+ { label: 'Android', value: 'android' },
47
+ { label: 'iOS', value: 'ios' },
48
+ { label: 'Web', value: 'web' },
49
+ ],
50
+ });
51
+ }
52
+ const certificates = await appCertificatesService.findAll({ appId, platform });
53
+ if (!certificates.length) {
54
+ consola.error('No certificates found for this app. Create one first.');
55
+ process.exit(1);
56
+ }
57
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
58
+ certificateId = await prompt('Select the certificate to delete:', {
59
+ type: 'select',
60
+ options: certificates.map((cert) => ({ label: cert.name, value: cert.id })),
61
+ });
62
+ }
63
+ else {
64
+ consola.error('You must provide the certificate ID or --name and --platform when running in non-interactive environment.');
65
+ process.exit(1);
66
+ }
67
+ }
68
+ if (!options.yes && isInteractive()) {
69
+ const confirmed = await prompt('Are you sure you want to delete this certificate?', {
70
+ type: 'confirm',
71
+ });
72
+ if (!confirmed) {
73
+ return;
74
+ }
75
+ }
76
+ await appCertificatesService.delete({ appId, certificateId });
77
+ consola.success('Certificate deleted successfully.');
78
+ }),
79
+ });
@@ -0,0 +1,80 @@
1
+ import appCertificatesService from '../../../services/app-certificates.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: 'Get an existing app certificate.',
10
+ options: defineOptions(z.object({
11
+ appId: z.string().optional().describe('ID of the app.'),
12
+ certificateId: z.string().optional().describe('ID of the certificate.'),
13
+ json: z.boolean().optional().describe('Output in JSON format.'),
14
+ name: z.string().optional().describe('Name of the certificate.'),
15
+ platform: z
16
+ .enum(['android', 'ios', 'web'])
17
+ .optional()
18
+ .describe('Platform of the certificate (android, ios, web).'),
19
+ })),
20
+ action: withAuth(async (options, args) => {
21
+ let { appId, certificateId, name, platform } = options;
22
+ if (!appId) {
23
+ if (!isInteractive()) {
24
+ consola.error('You must provide an app ID when running in non-interactive environment.');
25
+ process.exit(1);
26
+ }
27
+ const organizationId = await promptOrganizationSelection();
28
+ appId = await promptAppSelection(organizationId);
29
+ }
30
+ if (!certificateId) {
31
+ if (name && platform) {
32
+ const certificates = await appCertificatesService.findAll({ appId, name, platform });
33
+ const firstCertificate = certificates[0];
34
+ if (!firstCertificate) {
35
+ consola.error(`No certificate found with name '${name}' and platform '${platform}'.`);
36
+ process.exit(1);
37
+ }
38
+ certificateId = firstCertificate.id;
39
+ }
40
+ else if (isInteractive()) {
41
+ if (!platform) {
42
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
43
+ platform = await prompt('Select the platform:', {
44
+ type: 'select',
45
+ options: [
46
+ { label: 'Android', value: 'android' },
47
+ { label: 'iOS', value: 'ios' },
48
+ { label: 'Web', value: 'web' },
49
+ ],
50
+ });
51
+ }
52
+ const certificates = await appCertificatesService.findAll({ appId, platform });
53
+ if (!certificates.length) {
54
+ consola.error('No certificates found for this app. Create one first.');
55
+ process.exit(1);
56
+ }
57
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
58
+ certificateId = await prompt('Select the certificate:', {
59
+ type: 'select',
60
+ options: certificates.map((cert) => ({ label: cert.name, value: cert.id })),
61
+ });
62
+ }
63
+ else {
64
+ consola.error('You must provide the certificate ID or --name and --platform when running in non-interactive environment.');
65
+ process.exit(1);
66
+ }
67
+ }
68
+ const certificate = await appCertificatesService.findOneById({
69
+ appId,
70
+ certificateId,
71
+ });
72
+ if (options.json) {
73
+ console.log(JSON.stringify(certificate, null, 2));
74
+ }
75
+ else {
76
+ console.table(certificate);
77
+ consola.success('Certificate retrieved successfully.');
78
+ }
79
+ }),
80
+ });
@@ -0,0 +1,43 @@
1
+ import appCertificatesService from '../../../services/app-certificates.js';
2
+ import { withAuth } from '../../../utils/auth.js';
3
+ import { isInteractive } from '../../../utils/environment.js';
4
+ import { 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: 'Retrieve a list of existing app certificates.',
10
+ options: defineOptions(z.object({
11
+ appId: z.string().optional().describe('ID of the app.'),
12
+ json: z.boolean().optional().describe('Output in JSON format.'),
13
+ limit: z.coerce.number().optional().describe('Limit for pagination.'),
14
+ offset: z.coerce.number().optional().describe('Offset for pagination.'),
15
+ platform: z.enum(['android', 'ios', 'web']).optional().describe('Filter by platform.'),
16
+ type: z.enum(['development', 'production']).optional().describe('Filter by type.'),
17
+ })),
18
+ action: withAuth(async (options, args) => {
19
+ let { appId, json, limit, offset, platform, type } = options;
20
+ if (!appId) {
21
+ if (!isInteractive()) {
22
+ consola.error('You must provide an app ID when running in non-interactive environment.');
23
+ process.exit(1);
24
+ }
25
+ const organizationId = await promptOrganizationSelection();
26
+ appId = await promptAppSelection(organizationId);
27
+ }
28
+ const certificates = await appCertificatesService.findAll({
29
+ appId,
30
+ limit,
31
+ offset,
32
+ platform,
33
+ type,
34
+ });
35
+ if (json) {
36
+ console.log(JSON.stringify(certificates, null, 2));
37
+ }
38
+ else {
39
+ console.table(certificates);
40
+ consola.success('Certificates retrieved successfully.');
41
+ }
42
+ }),
43
+ });
@@ -0,0 +1,50 @@
1
+ import appCertificatesService from '../../../services/app-certificates.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: 'Update an existing app certificate.',
10
+ options: defineOptions(z.object({
11
+ appId: z.string().optional().describe('ID of the app.'),
12
+ certificateId: z.string().optional().describe('ID of the certificate.'),
13
+ keyAlias: z.string().optional().describe('Key alias for the certificate.'),
14
+ keyPassword: z.string().optional().describe('Key password for the certificate.'),
15
+ name: z.string().optional().describe('Name of the certificate.'),
16
+ password: z.string().optional().describe('Password for the certificate.'),
17
+ type: z
18
+ .enum(['development', 'production'])
19
+ .optional()
20
+ .describe('Type of the certificate (development, production).'),
21
+ })),
22
+ action: withAuth(async (options, args) => {
23
+ let { appId, certificateId, keyAlias, keyPassword, name, password, type } = options;
24
+ if (!appId) {
25
+ if (!isInteractive()) {
26
+ consola.error('You must provide an app ID when running in non-interactive environment.');
27
+ process.exit(1);
28
+ }
29
+ const organizationId = await promptOrganizationSelection();
30
+ appId = await promptAppSelection(organizationId);
31
+ }
32
+ if (!certificateId) {
33
+ if (!isInteractive()) {
34
+ consola.error('You must provide the certificate ID when running in non-interactive environment.');
35
+ process.exit(1);
36
+ }
37
+ certificateId = await prompt('Enter the certificate ID:', { type: 'text' });
38
+ }
39
+ await appCertificatesService.update({
40
+ appId,
41
+ certificateId,
42
+ name,
43
+ type,
44
+ password,
45
+ keyAlias,
46
+ keyPassword,
47
+ });
48
+ consola.success('Certificate updated successfully.');
49
+ }),
50
+ });