@capawesome/cli 4.3.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 +24 -0
- package/dist/commands/apps/builds/create.js +29 -4
- package/dist/commands/apps/certificates/create.js +186 -0
- package/dist/commands/apps/certificates/delete.js +79 -0
- package/dist/commands/apps/certificates/get.js +80 -0
- package/dist/commands/apps/certificates/list.js +43 -0
- package/dist/commands/apps/certificates/update.js +50 -0
- package/dist/commands/apps/channels/create.js +1 -6
- package/dist/commands/apps/channels/create.test.js +6 -11
- package/dist/commands/apps/destinations/create.js +312 -0
- package/dist/commands/apps/destinations/delete.js +75 -0
- package/dist/commands/apps/destinations/get.js +76 -0
- package/dist/commands/apps/destinations/list.js +41 -0
- package/dist/commands/apps/destinations/update.js +69 -0
- package/dist/commands/apps/devices/forcechannel.js +66 -0
- package/dist/commands/apps/devices/unforcechannel.js +40 -0
- package/dist/commands/apps/liveupdates/generate-signing-key.js +3 -0
- package/dist/commands/apps/liveupdates/register.js +1 -6
- package/dist/commands/apps/liveupdates/upload.js +2 -6
- package/dist/index.js +16 -4
- package/dist/services/app-apple-api-keys.js +22 -0
- package/dist/services/app-certificates.js +65 -4
- package/dist/services/app-destinations.js +46 -4
- package/dist/services/app-devices.js +7 -0
- package/dist/services/app-google-service-account-keys.js +22 -0
- package/dist/services/app-provisioning-profiles.js +33 -0
- package/dist/services/apps.js +3 -0
- package/dist/services/update.js +7 -2
- package/dist/types/app-apple-api-key.js +1 -0
- package/dist/types/app-google-service-account-key.js +1 -0
- package/dist/types/app-provisioning-profile.js +1 -0
- package/dist/types/index.js +5 -0
- package/dist/utils/prompt.js +1 -1
- package/package.json +1 -1
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.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
|
+
|
|
20
|
+
## [4.4.0](https://github.com/capawesome-team/cli/compare/v4.3.0...v4.4.0) (2026-03-13)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* 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))
|
|
26
|
+
* 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))
|
|
27
|
+
* add warning when using manifest artifact type ([42a6b2b](https://github.com/capawesome-team/cli/commit/42a6b2bfae0cc958127d4ff60562000eb9713943))
|
|
28
|
+
|
|
5
29
|
## [4.3.0](https://github.com/capawesome-team/cli/compare/v4.2.1...v4.3.0) (2026-03-07)
|
|
6
30
|
|
|
7
31
|
|
|
@@ -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
|
|
158
|
-
if (!certificate && !options.yes && isInteractive()
|
|
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
|
-
|
|
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
|
+
});
|
|
@@ -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
|
});
|