@capawesome/cli 1.14.0 → 2.0.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 +27 -0
- package/README.md +7 -3
- package/dist/commands/apps/bundles/create.js +206 -239
- package/dist/commands/apps/bundles/create.test.js +276 -0
- package/dist/commands/apps/bundles/delete.js +35 -60
- package/dist/commands/apps/bundles/delete.test.js +139 -0
- package/dist/commands/apps/bundles/update.js +61 -89
- package/dist/commands/apps/bundles/update.test.js +141 -0
- package/dist/commands/apps/channels/create.js +45 -75
- package/dist/commands/apps/channels/create.test.js +119 -0
- package/dist/commands/apps/channels/delete.js +46 -69
- package/dist/commands/apps/channels/delete.test.js +141 -0
- package/dist/commands/apps/channels/get.js +52 -94
- package/dist/commands/apps/channels/get.test.js +135 -0
- package/dist/commands/apps/channels/list.js +37 -82
- package/dist/commands/apps/channels/list.test.js +121 -0
- package/dist/commands/apps/channels/update.js +39 -83
- package/dist/commands/apps/channels/update.test.js +138 -0
- package/dist/commands/apps/create.js +28 -53
- package/dist/commands/apps/create.test.js +117 -0
- package/dist/commands/apps/delete.js +29 -50
- package/dist/commands/apps/delete.test.js +120 -0
- package/dist/commands/apps/devices/delete.js +35 -60
- package/dist/commands/apps/devices/delete.test.js +139 -0
- package/dist/commands/doctor.js +12 -29
- package/dist/commands/doctor.test.js +52 -0
- package/dist/commands/login.js +50 -71
- package/dist/commands/login.test.js +116 -0
- package/dist/commands/logout.js +13 -31
- package/dist/commands/logout.test.js +47 -0
- package/dist/commands/manifests/generate.js +20 -38
- package/dist/commands/manifests/generate.test.js +60 -0
- package/dist/commands/organizations/create.js +25 -0
- package/dist/commands/organizations/create.test.js +80 -0
- package/dist/commands/whoami.js +20 -31
- package/dist/commands/whoami.test.js +30 -0
- package/dist/config/consts.js +4 -5
- package/dist/config/index.js +1 -17
- package/dist/index.js +54 -80
- package/dist/services/app-bundle-files.js +117 -136
- package/dist/services/app-bundles.js +22 -41
- package/dist/services/app-channels.js +54 -77
- package/dist/services/app-devices.js +10 -25
- package/dist/services/apps.js +25 -43
- package/dist/services/authorization-service.js +4 -8
- package/dist/services/config.js +15 -28
- package/dist/services/organizations.js +19 -26
- package/dist/services/session-code.js +7 -22
- package/dist/services/sessions.js +13 -30
- package/dist/services/update.js +17 -55
- package/dist/services/users.js +11 -26
- package/dist/types/app-bundle-file.js +1 -2
- package/dist/types/app-bundle.js +1 -2
- package/dist/types/app-channel.js +1 -2
- package/dist/types/app-device.js +1 -2
- package/dist/types/app.js +1 -2
- package/dist/types/index.js +8 -24
- package/dist/types/npm-package.js +1 -2
- package/dist/types/organization.js +1 -2
- package/dist/types/session-code.js +1 -2
- package/dist/types/session.js +1 -2
- package/dist/types/user.js +1 -2
- package/dist/utils/buffer.js +12 -43
- package/dist/utils/error.js +24 -14
- package/dist/utils/file.js +22 -41
- package/dist/utils/hash.js +3 -39
- package/dist/utils/http-client.js +27 -53
- package/dist/utils/manifest.js +11 -24
- package/dist/utils/private-key.js +23 -0
- package/dist/utils/prompt.js +9 -26
- package/dist/utils/signature.js +3 -39
- package/dist/utils/user-config.js +12 -0
- package/dist/utils/zip.js +11 -27
- package/package.json +22 -9
- package/dist/utils/ci.js +0 -7
- package/dist/utils/userConfig.js +0 -16
|
@@ -1,119 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
bundleId
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
},
|
|
44
|
-
rollout: {
|
|
45
|
-
type: 'string',
|
|
46
|
-
description: 'The percentage of devices to deploy the bundle to. Must be a number between 0 and 1 (e.g. 0.5).',
|
|
47
|
-
},
|
|
48
|
-
iosMax: {
|
|
49
|
-
type: 'string',
|
|
50
|
-
description: 'The maximum iOS bundle version (`CFBundleVersion`) that the bundle supports.',
|
|
51
|
-
},
|
|
52
|
-
iosMin: {
|
|
53
|
-
type: 'string',
|
|
54
|
-
description: 'The minimum iOS bundle version (`CFBundleVersion`) that the bundle supports.',
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
run: (ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
|
-
if (!authorization_service_1.default.hasAuthorizationToken()) {
|
|
59
|
-
consola_1.default.error('You must be logged in to run this command.');
|
|
1
|
+
import appBundlesService from '../../../services/app-bundles.js';
|
|
2
|
+
import appsService from '../../../services/apps.js';
|
|
3
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
4
|
+
import organizationsService from '../../../services/organizations.js';
|
|
5
|
+
import { prompt } 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: 'Update an app bundle.',
|
|
11
|
+
options: defineOptions(z.object({
|
|
12
|
+
androidMax: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('The maximum Android version code (`versionCode`) that the bundle supports.'),
|
|
16
|
+
androidMin: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('The minimum Android version code (`versionCode`) that the bundle supports.'),
|
|
20
|
+
appId: z.string().optional().describe('ID of the app.'),
|
|
21
|
+
bundleId: z.string().optional().describe('ID of the bundle.'),
|
|
22
|
+
rollout: z.coerce
|
|
23
|
+
.number()
|
|
24
|
+
.min(0)
|
|
25
|
+
.max(1, {
|
|
26
|
+
message: 'Rollout percentage must be a number between 0 and 1 (e.g. 0.5).',
|
|
27
|
+
})
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('The percentage of devices to deploy the bundle to. Must be a number between 0 and 1 (e.g. 0.5).'),
|
|
30
|
+
iosMax: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('The maximum iOS bundle version (`CFBundleVersion`) that the bundle supports.'),
|
|
34
|
+
iosMin: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('The minimum iOS bundle version (`CFBundleVersion`) that the bundle supports.'),
|
|
38
|
+
})),
|
|
39
|
+
action: async (options, args) => {
|
|
40
|
+
let { androidMax, androidMin, appId, bundleId, rollout, iosMax, iosMin } = options;
|
|
41
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
42
|
+
consola.error('You must be logged in to run this command.');
|
|
60
43
|
process.exit(1);
|
|
61
44
|
}
|
|
62
45
|
// Prompt for missing arguments
|
|
63
|
-
const { androidMax, androidMin, rollout, iosMax, iosMin } = ctx.args;
|
|
64
|
-
let appId = ctx.args.appId;
|
|
65
|
-
let bundleId = ctx.args.bundleId;
|
|
66
46
|
if (!appId) {
|
|
67
|
-
const organizations =
|
|
47
|
+
const organizations = await organizationsService.findAll();
|
|
68
48
|
if (organizations.length === 0) {
|
|
69
|
-
|
|
49
|
+
consola.error('You must create an organization before updating a bundle.');
|
|
70
50
|
process.exit(1);
|
|
71
51
|
}
|
|
72
52
|
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
73
|
-
const organizationId =
|
|
53
|
+
const organizationId = await prompt('Select the organization of the app for which you want to update a bundle.', {
|
|
74
54
|
type: 'select',
|
|
75
55
|
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
|
|
76
56
|
});
|
|
77
57
|
if (!organizationId) {
|
|
78
|
-
|
|
58
|
+
consola.error('You must select the organization of an app for which you want to update a bundle.');
|
|
79
59
|
process.exit(1);
|
|
80
60
|
}
|
|
81
|
-
const apps =
|
|
61
|
+
const apps = await appsService.findAll({
|
|
82
62
|
organizationId,
|
|
83
63
|
});
|
|
84
64
|
if (!apps.length) {
|
|
85
|
-
|
|
65
|
+
consola.error('You must create an app before updating a bundle.');
|
|
86
66
|
process.exit(1);
|
|
87
67
|
}
|
|
88
68
|
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
89
|
-
appId =
|
|
69
|
+
appId = await prompt('Which app do you want to update the bundle for?', {
|
|
90
70
|
type: 'select',
|
|
91
71
|
options: apps.map((app) => ({ label: app.name, value: app.id })),
|
|
92
72
|
});
|
|
93
73
|
}
|
|
94
74
|
if (!bundleId) {
|
|
95
|
-
bundleId =
|
|
75
|
+
bundleId = await prompt('Enter the bundle ID:', {
|
|
96
76
|
type: 'text',
|
|
97
77
|
});
|
|
98
78
|
}
|
|
99
79
|
// Update bundle
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
consola_1.default.success('Bundle updated successfully.');
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
const message = (0, error_1.getMessageFromUnknownError)(error);
|
|
115
|
-
consola_1.default.error(message);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
}),
|
|
80
|
+
await appBundlesService.update({
|
|
81
|
+
appId,
|
|
82
|
+
appBundleId: bundleId,
|
|
83
|
+
maxAndroidAppVersionCode: androidMax,
|
|
84
|
+
maxIosAppVersionCode: iosMax,
|
|
85
|
+
minAndroidAppVersionCode: androidMin,
|
|
86
|
+
minIosAppVersionCode: iosMin,
|
|
87
|
+
rolloutPercentage: rollout,
|
|
88
|
+
});
|
|
89
|
+
consola.success('Bundle updated successfully.');
|
|
90
|
+
},
|
|
119
91
|
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
3
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
4
|
+
import userConfig from '../../../utils/user-config.js';
|
|
5
|
+
import consola from 'consola';
|
|
6
|
+
import nock from 'nock';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import updateBundleCommand from './update.js';
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@/utils/user-config.js');
|
|
11
|
+
vi.mock('@/utils/prompt.js');
|
|
12
|
+
vi.mock('@/services/authorization-service.js');
|
|
13
|
+
vi.mock('consola');
|
|
14
|
+
describe('apps-bundles-update', () => {
|
|
15
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
16
|
+
const mockPrompt = vi.mocked(prompt);
|
|
17
|
+
const mockConsola = vi.mocked(consola);
|
|
18
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
22
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
23
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
24
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
25
|
+
throw new Error(`Process exited with code ${code}`);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
nock.cleanAll();
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
it('should require authentication', async () => {
|
|
33
|
+
const appId = 'app-123';
|
|
34
|
+
const bundleId = 'bundle-456';
|
|
35
|
+
const options = { appId, bundleId };
|
|
36
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
|
|
37
|
+
await expect(updateBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
38
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command.');
|
|
39
|
+
});
|
|
40
|
+
it('should update bundle with provided options', async () => {
|
|
41
|
+
const appId = 'app-123';
|
|
42
|
+
const bundleId = 'bundle-456';
|
|
43
|
+
const rollout = 0.5;
|
|
44
|
+
const androidMax = '1000';
|
|
45
|
+
const iosMin = '2.0.0';
|
|
46
|
+
const testToken = 'test-token';
|
|
47
|
+
const options = { appId, bundleId, rollout, androidMax, iosMin };
|
|
48
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
49
|
+
.patch(`/v1/apps/${appId}/bundles/${bundleId}`, {
|
|
50
|
+
appId,
|
|
51
|
+
appBundleId: bundleId,
|
|
52
|
+
maxAndroidAppVersionCode: androidMax,
|
|
53
|
+
maxIosAppVersionCode: undefined,
|
|
54
|
+
minAndroidAppVersionCode: undefined,
|
|
55
|
+
minIosAppVersionCode: iosMin,
|
|
56
|
+
rolloutPercentage: rollout,
|
|
57
|
+
})
|
|
58
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
59
|
+
.reply(200, { id: bundleId });
|
|
60
|
+
await updateBundleCommand.action(options, undefined);
|
|
61
|
+
expect(scope.isDone()).toBe(true);
|
|
62
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Bundle updated successfully.');
|
|
63
|
+
});
|
|
64
|
+
it('should prompt for app selection when appId not provided', async () => {
|
|
65
|
+
const orgId = 'org-1';
|
|
66
|
+
const appId = 'app-1';
|
|
67
|
+
const bundleId = 'bundle-456';
|
|
68
|
+
const testToken = 'test-token';
|
|
69
|
+
const organization = { id: orgId, name: 'Org 1' };
|
|
70
|
+
const app = { id: appId, name: 'App 1' };
|
|
71
|
+
const options = { bundleId, rollout: 1 };
|
|
72
|
+
const orgsScope = nock(DEFAULT_API_BASE_URL)
|
|
73
|
+
.get('/v1/organizations')
|
|
74
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
75
|
+
.reply(200, [organization]);
|
|
76
|
+
const appsScope = nock(DEFAULT_API_BASE_URL)
|
|
77
|
+
.get('/v1/apps')
|
|
78
|
+
.query({ organizationId: orgId })
|
|
79
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
80
|
+
.reply(200, [app]);
|
|
81
|
+
const updateScope = nock(DEFAULT_API_BASE_URL)
|
|
82
|
+
.patch(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
83
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
84
|
+
.reply(200, { id: bundleId });
|
|
85
|
+
mockPrompt
|
|
86
|
+
.mockResolvedValueOnce(orgId) // organization selection
|
|
87
|
+
.mockResolvedValueOnce(appId); // app selection
|
|
88
|
+
await updateBundleCommand.action(options, undefined);
|
|
89
|
+
expect(orgsScope.isDone()).toBe(true);
|
|
90
|
+
expect(appsScope.isDone()).toBe(true);
|
|
91
|
+
expect(updateScope.isDone()).toBe(true);
|
|
92
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Bundle updated successfully.');
|
|
93
|
+
});
|
|
94
|
+
it('should prompt for bundleId when not provided', async () => {
|
|
95
|
+
const appId = 'app-123';
|
|
96
|
+
const bundleId = 'bundle-456';
|
|
97
|
+
const testToken = 'test-token';
|
|
98
|
+
const options = { appId };
|
|
99
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
100
|
+
.patch(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
101
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
102
|
+
.reply(200, { id: bundleId });
|
|
103
|
+
mockPrompt.mockResolvedValueOnce(bundleId); // bundle ID input
|
|
104
|
+
await updateBundleCommand.action(options, undefined);
|
|
105
|
+
expect(scope.isDone()).toBe(true);
|
|
106
|
+
expect(mockPrompt).toHaveBeenCalledWith('Enter the bundle ID:', { type: 'text' });
|
|
107
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Bundle updated successfully.');
|
|
108
|
+
});
|
|
109
|
+
it('should handle API error during update', async () => {
|
|
110
|
+
const appId = 'app-123';
|
|
111
|
+
const bundleId = 'bundle-456';
|
|
112
|
+
const testToken = 'test-token';
|
|
113
|
+
const options = { appId, bundleId };
|
|
114
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
115
|
+
.patch(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
116
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
117
|
+
.reply(404, { message: 'Bundle not found' });
|
|
118
|
+
await expect(updateBundleCommand.action(options, undefined)).rejects.toThrow();
|
|
119
|
+
expect(scope.isDone()).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
it('should handle error when no organizations exist', async () => {
|
|
122
|
+
const testToken = 'test-token';
|
|
123
|
+
const options = {};
|
|
124
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
125
|
+
.get('/v1/organizations')
|
|
126
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
127
|
+
.reply(200, []);
|
|
128
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
129
|
+
throw new Error(`process.exit called with code ${code}`);
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
await updateBundleCommand.action(options, undefined);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
expect(error.message).toBe('process.exit called with code 1');
|
|
136
|
+
}
|
|
137
|
+
expect(scope.isDone()).toBe(true);
|
|
138
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must create an organization before updating a bundle.');
|
|
139
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -1,110 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
args: {
|
|
27
|
-
appId: {
|
|
28
|
-
type: 'string',
|
|
29
|
-
description: 'ID of the app.',
|
|
30
|
-
},
|
|
31
|
-
bundleLimit: {
|
|
32
|
-
type: 'string',
|
|
33
|
-
description: 'Maximum number of bundles that can be assigned to the channel. If more bundles are assigned, the oldest bundles will be automatically deleted.',
|
|
34
|
-
},
|
|
35
|
-
ignoreErrors: {
|
|
36
|
-
type: 'boolean',
|
|
37
|
-
description: 'Whether to ignore errors or not.',
|
|
38
|
-
},
|
|
39
|
-
name: {
|
|
40
|
-
type: 'string',
|
|
41
|
-
description: 'Name of the channel.',
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
run: (ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
-
let appId = ctx.args.appId;
|
|
46
|
-
let bundleLimitAsString = ctx.args.bundleLimit;
|
|
47
|
-
let ignoreErrors = ctx.args.ignoreErrors;
|
|
48
|
-
let name = ctx.args.name;
|
|
49
|
-
// Convert ignoreErrors to boolean
|
|
50
|
-
if (typeof ignoreErrors === 'string') {
|
|
51
|
-
ignoreErrors = ignoreErrors.toLowerCase() === 'true';
|
|
1
|
+
import appChannelsService from '../../../services/app-channels.js';
|
|
2
|
+
import appsService from '../../../services/apps.js';
|
|
3
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
4
|
+
import organizationsService from '../../../services/organizations.js';
|
|
5
|
+
import { getMessageFromUnknownError } from '../../../utils/error.js';
|
|
6
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
7
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
8
|
+
import consola from 'consola';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
export default defineCommand({
|
|
11
|
+
description: 'Create a new app channel.',
|
|
12
|
+
options: defineOptions(z.object({
|
|
13
|
+
appId: z.string().optional().describe('ID of the app.'),
|
|
14
|
+
bundleLimit: z.coerce
|
|
15
|
+
.number()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Maximum number of bundles that can be assigned to the channel. If more bundles are assigned, the oldest bundles will be automatically deleted.'),
|
|
18
|
+
ignoreErrors: z.boolean().optional().describe('Whether to ignore errors or not.'),
|
|
19
|
+
name: z.string().optional().describe('Name of the channel.'),
|
|
20
|
+
})),
|
|
21
|
+
action: async (options, args) => {
|
|
22
|
+
let { appId, bundleLimit, ignoreErrors, name } = options;
|
|
23
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
24
|
+
consola.error('You must be logged in to run this command.');
|
|
25
|
+
process.exit(1);
|
|
52
26
|
}
|
|
53
27
|
// Validate the app ID
|
|
54
28
|
if (!appId) {
|
|
55
|
-
const organizations =
|
|
29
|
+
const organizations = await organizationsService.findAll();
|
|
56
30
|
if (organizations.length === 0) {
|
|
57
|
-
|
|
31
|
+
consola.error('You must create an organization before creating a channel.');
|
|
58
32
|
process.exit(1);
|
|
59
33
|
}
|
|
60
34
|
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
61
|
-
const organizationId =
|
|
35
|
+
const organizationId = await prompt('Select the organization of the app for which you want to create a channel.', {
|
|
62
36
|
type: 'select',
|
|
63
37
|
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
|
|
64
38
|
});
|
|
65
39
|
if (!organizationId) {
|
|
66
|
-
|
|
40
|
+
consola.error('You must select the organization of an app for which you want to create a channel.');
|
|
67
41
|
process.exit(1);
|
|
68
42
|
}
|
|
69
|
-
const apps =
|
|
43
|
+
const apps = await appsService.findAll({
|
|
70
44
|
organizationId,
|
|
71
45
|
});
|
|
72
46
|
if (!apps.length) {
|
|
73
|
-
|
|
47
|
+
consola.error('You must create an app before creating a channel.');
|
|
74
48
|
process.exit(1);
|
|
75
49
|
}
|
|
76
50
|
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
77
|
-
appId =
|
|
51
|
+
appId = await prompt('Which app do you want to create the channel for?', {
|
|
78
52
|
type: 'select',
|
|
79
53
|
options: apps.map((app) => ({ label: app.name, value: app.id })),
|
|
80
54
|
});
|
|
81
55
|
}
|
|
82
|
-
// Validate the bundle limit
|
|
83
|
-
let bundleLimit;
|
|
84
|
-
if (bundleLimitAsString) {
|
|
85
|
-
bundleLimit = parseInt(bundleLimitAsString, 10);
|
|
86
|
-
if (isNaN(bundleLimit)) {
|
|
87
|
-
consola_1.default.error('The bundle limit must be a number.');
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
56
|
// Validate the channel name
|
|
92
57
|
if (!name) {
|
|
93
|
-
name =
|
|
58
|
+
name = await prompt('Enter the name of the channel:', { type: 'text' });
|
|
94
59
|
}
|
|
95
60
|
try {
|
|
96
|
-
const response =
|
|
61
|
+
const response = await appChannelsService.create({
|
|
97
62
|
appId,
|
|
98
63
|
name,
|
|
99
64
|
totalAppBundleLimit: bundleLimit,
|
|
100
65
|
});
|
|
101
|
-
|
|
102
|
-
|
|
66
|
+
consola.success('Channel created successfully.');
|
|
67
|
+
consola.info(`Channel ID: ${response.id}`);
|
|
103
68
|
}
|
|
104
69
|
catch (error) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
70
|
+
if (ignoreErrors) {
|
|
71
|
+
const message = getMessageFromUnknownError(error);
|
|
72
|
+
consola.error(message);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
108
78
|
}
|
|
109
|
-
}
|
|
79
|
+
},
|
|
110
80
|
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
3
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
4
|
+
import userConfig from '../../../utils/user-config.js';
|
|
5
|
+
import consola from 'consola';
|
|
6
|
+
import nock from 'nock';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import createChannelCommand from './create.js';
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@/utils/user-config.js');
|
|
11
|
+
vi.mock('@/utils/prompt.js');
|
|
12
|
+
vi.mock('@/services/authorization-service.js');
|
|
13
|
+
vi.mock('consola');
|
|
14
|
+
describe('apps-channels-create', () => {
|
|
15
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
16
|
+
const mockPrompt = vi.mocked(prompt);
|
|
17
|
+
const mockConsola = vi.mocked(consola);
|
|
18
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
22
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
23
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
24
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
25
|
+
throw new Error(`Process exited with code ${code}`);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
nock.cleanAll();
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
it('should create channel with provided options', async () => {
|
|
33
|
+
const appId = 'app-123';
|
|
34
|
+
const channelName = 'production';
|
|
35
|
+
const bundleLimit = 5;
|
|
36
|
+
const channelId = 'channel-456';
|
|
37
|
+
const testToken = 'test-token';
|
|
38
|
+
const options = { appId, name: channelName, bundleLimit };
|
|
39
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
40
|
+
.post(`/v1/apps/${appId}/channels`, {
|
|
41
|
+
appId,
|
|
42
|
+
name: channelName,
|
|
43
|
+
totalAppBundleLimit: bundleLimit,
|
|
44
|
+
})
|
|
45
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
46
|
+
.reply(201, { id: channelId, name: channelName });
|
|
47
|
+
await createChannelCommand.action(options, undefined);
|
|
48
|
+
expect(scope.isDone()).toBe(true);
|
|
49
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Channel created successfully.');
|
|
50
|
+
expect(mockConsola.info).toHaveBeenCalledWith(`Channel ID: ${channelId}`);
|
|
51
|
+
});
|
|
52
|
+
it('should prompt for app when not provided', async () => {
|
|
53
|
+
const channelName = 'staging';
|
|
54
|
+
const orgId = 'org-1';
|
|
55
|
+
const appId = 'app-1';
|
|
56
|
+
const channelId = 'channel-456';
|
|
57
|
+
const testToken = 'test-token';
|
|
58
|
+
const options = { name: channelName };
|
|
59
|
+
const orgsScope = nock(DEFAULT_API_BASE_URL)
|
|
60
|
+
.get('/v1/organizations')
|
|
61
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
62
|
+
.reply(200, [{ id: orgId, name: 'Org 1' }]);
|
|
63
|
+
const appsScope = nock(DEFAULT_API_BASE_URL)
|
|
64
|
+
.get('/v1/apps')
|
|
65
|
+
.query({ organizationId: orgId })
|
|
66
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
67
|
+
.reply(200, [{ id: appId, name: 'App 1' }]);
|
|
68
|
+
const createScope = nock(DEFAULT_API_BASE_URL)
|
|
69
|
+
.post(`/v1/apps/${appId}/channels`, {
|
|
70
|
+
appId,
|
|
71
|
+
name: channelName,
|
|
72
|
+
totalAppBundleLimit: undefined,
|
|
73
|
+
})
|
|
74
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
75
|
+
.reply(201, { id: channelId, name: channelName });
|
|
76
|
+
mockPrompt
|
|
77
|
+
.mockResolvedValueOnce(orgId) // organization selection
|
|
78
|
+
.mockResolvedValueOnce(appId); // app selection
|
|
79
|
+
await createChannelCommand.action(options, undefined);
|
|
80
|
+
expect(orgsScope.isDone()).toBe(true);
|
|
81
|
+
expect(appsScope.isDone()).toBe(true);
|
|
82
|
+
expect(createScope.isDone()).toBe(true);
|
|
83
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Channel created successfully.');
|
|
84
|
+
});
|
|
85
|
+
it('should prompt for channel name when not provided', async () => {
|
|
86
|
+
const options = { appId: 'app-123' };
|
|
87
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
88
|
+
.post('/v1/apps/app-123/channels', {
|
|
89
|
+
appId: 'app-123',
|
|
90
|
+
name: 'development',
|
|
91
|
+
totalAppBundleLimit: undefined,
|
|
92
|
+
})
|
|
93
|
+
.matchHeader('Authorization', 'Bearer test-token')
|
|
94
|
+
.reply(201, { id: 'channel-456', name: 'development' });
|
|
95
|
+
mockPrompt.mockResolvedValueOnce('development');
|
|
96
|
+
await createChannelCommand.action(options, undefined);
|
|
97
|
+
expect(scope.isDone()).toBe(true);
|
|
98
|
+
expect(mockPrompt).toHaveBeenCalledWith('Enter the name of the channel:', { type: 'text' });
|
|
99
|
+
});
|
|
100
|
+
it('should handle error with ignoreErrors flag', async () => {
|
|
101
|
+
const options = { appId: 'app-123', name: 'production', ignoreErrors: true };
|
|
102
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
103
|
+
.post('/v1/apps/app-123/channels')
|
|
104
|
+
.matchHeader('Authorization', 'Bearer test-token')
|
|
105
|
+
.reply(400, { message: 'Channel name already exists' });
|
|
106
|
+
await expect(createChannelCommand.action(options, undefined)).rejects.toThrow('Process exited with code 0');
|
|
107
|
+
expect(scope.isDone()).toBe(true);
|
|
108
|
+
expect(mockConsola.error).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
it('should handle error without ignoreErrors flag', async () => {
|
|
111
|
+
const options = { appId: 'app-123', name: 'production' };
|
|
112
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
113
|
+
.post('/v1/apps/app-123/channels')
|
|
114
|
+
.matchHeader('Authorization', 'Bearer test-token')
|
|
115
|
+
.reply(400, { message: 'Channel name already exists' });
|
|
116
|
+
await expect(createChannelCommand.action(options, undefined)).rejects.toThrow();
|
|
117
|
+
expect(scope.isDone()).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|