@capawesome/cli 4.17.2 → 4.18.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/commands/apps/automations/create.js +141 -0
  3. package/dist/commands/apps/automations/create.test.js +162 -0
  4. package/dist/commands/apps/automations/delete.js +66 -0
  5. package/dist/commands/apps/automations/delete.test.js +127 -0
  6. package/dist/commands/apps/automations/get.js +62 -0
  7. package/dist/commands/apps/automations/get.test.js +118 -0
  8. package/dist/commands/apps/automations/list.js +46 -0
  9. package/dist/commands/apps/automations/list.test.js +92 -0
  10. package/dist/commands/apps/automations/update.js +118 -0
  11. package/dist/commands/apps/automations/update.test.js +124 -0
  12. package/dist/commands/apps/builds/create.js +31 -1
  13. package/dist/commands/apps/builds/create.test.js +15 -0
  14. package/dist/commands/apps/builds/run.js +265 -0
  15. package/dist/commands/apps/configurations/create.js +51 -0
  16. package/dist/commands/apps/configurations/create.test.js +120 -0
  17. package/dist/commands/apps/configurations/delete.js +61 -0
  18. package/dist/commands/apps/configurations/delete.test.js +112 -0
  19. package/dist/commands/apps/configurations/get.js +65 -0
  20. package/dist/commands/apps/configurations/get.test.js +119 -0
  21. package/dist/commands/apps/configurations/list.js +39 -0
  22. package/dist/commands/apps/configurations/list.test.js +94 -0
  23. package/dist/commands/apps/configurations/update.js +61 -0
  24. package/dist/commands/apps/configurations/update.test.js +122 -0
  25. package/dist/index.js +11 -0
  26. package/dist/services/app-automations.js +64 -0
  27. package/dist/services/app-configurations.js +77 -0
  28. package/dist/types/app-automation.js +1 -0
  29. package/dist/types/app-configuration.js +1 -0
  30. package/dist/types/index.js +1 -0
  31. package/dist/utils/android-emulator.js +170 -0
  32. package/dist/utils/ios-simulator.js +57 -0
  33. package/dist/utils/zip.js +4 -0
  34. package/package.json +2 -2
@@ -0,0 +1,118 @@
1
+ import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
2
+ import authorizationService from '../../../services/authorization-service.js';
3
+ import { prompt, promptAppSelection, promptOrganizationSelection } 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 getAutomationCommand from './get.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
+ vi.mock('@/utils/environment.js', () => ({
15
+ isInteractive: () => true,
16
+ }));
17
+ describe('apps-automations-get', () => {
18
+ const mockUserConfig = vi.mocked(userConfig);
19
+ const mockPrompt = vi.mocked(prompt);
20
+ const mockPromptOrganizationSelection = vi.mocked(promptOrganizationSelection);
21
+ const mockPromptAppSelection = vi.mocked(promptAppSelection);
22
+ const mockConsola = vi.mocked(consola);
23
+ const mockAuthorizationService = vi.mocked(authorizationService);
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ mockUserConfig.read.mockReturnValue({ token: 'test-token' });
27
+ mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
28
+ mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
29
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
30
+ throw new Error(`Process exited with code ${code}`);
31
+ });
32
+ });
33
+ afterEach(() => {
34
+ nock.cleanAll();
35
+ vi.restoreAllMocks();
36
+ });
37
+ it('should get automation by ID', async () => {
38
+ const appId = 'app-123';
39
+ const automationId = 'automation-456';
40
+ const automation = { id: automationId, appId, name: 'nightly' };
41
+ const options = { appId, automationId };
42
+ const tableSpy = vi.spyOn(console, 'table').mockImplementation(() => { });
43
+ const scope = nock(DEFAULT_API_BASE_URL)
44
+ .get(`/v1/apps/${appId}/automations/${automationId}`)
45
+ .matchHeader('Authorization', 'Bearer test-token')
46
+ .reply(200, automation);
47
+ await getAutomationCommand.action(options, undefined);
48
+ expect(scope.isDone()).toBe(true);
49
+ expect(tableSpy).toHaveBeenCalledWith(automation);
50
+ expect(mockConsola.success).toHaveBeenCalledWith('Automation retrieved successfully.');
51
+ });
52
+ it('should get automation by name', async () => {
53
+ const appId = 'app-123';
54
+ const automation = { id: 'automation-456', appId, name: 'nightly' };
55
+ const options = { appId, name: 'nightly' };
56
+ vi.spyOn(console, 'table').mockImplementation(() => { });
57
+ const scope = nock(DEFAULT_API_BASE_URL)
58
+ .get(`/v1/apps/${appId}/automations`)
59
+ .query({ name: 'nightly' })
60
+ .matchHeader('Authorization', 'Bearer test-token')
61
+ .reply(200, [automation]);
62
+ await getAutomationCommand.action(options, undefined);
63
+ expect(scope.isDone()).toBe(true);
64
+ expect(mockConsola.success).toHaveBeenCalledWith('Automation retrieved successfully.');
65
+ });
66
+ it('should output JSON when json flag is set', async () => {
67
+ const appId = 'app-123';
68
+ const automationId = 'automation-456';
69
+ const automation = { id: automationId, appId, name: 'nightly' };
70
+ const options = { appId, automationId, json: true };
71
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
72
+ const scope = nock(DEFAULT_API_BASE_URL)
73
+ .get(`/v1/apps/${appId}/automations/${automationId}`)
74
+ .matchHeader('Authorization', 'Bearer test-token')
75
+ .reply(200, automation);
76
+ await getAutomationCommand.action(options, undefined);
77
+ expect(scope.isDone()).toBe(true);
78
+ expect(logSpy).toHaveBeenCalledWith(JSON.stringify(automation, null, 2));
79
+ expect(mockConsola.success).not.toHaveBeenCalled();
80
+ });
81
+ it('should prompt for app and automation when not provided', async () => {
82
+ const orgId = 'org-1';
83
+ const appId = 'app-1';
84
+ const automationId = 'automation-456';
85
+ const options = {};
86
+ vi.spyOn(console, 'table').mockImplementation(() => { });
87
+ const listScope = nock(DEFAULT_API_BASE_URL)
88
+ .get(`/v1/apps/${appId}/automations`)
89
+ .matchHeader('Authorization', 'Bearer test-token')
90
+ .reply(200, [{ id: automationId, appId, name: 'nightly' }]);
91
+ const getScope = nock(DEFAULT_API_BASE_URL)
92
+ .get(`/v1/apps/${appId}/automations/${automationId}`)
93
+ .matchHeader('Authorization', 'Bearer test-token')
94
+ .reply(200, { id: automationId, appId, name: 'nightly' });
95
+ mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
96
+ mockPromptAppSelection.mockResolvedValueOnce(appId);
97
+ mockPrompt.mockResolvedValueOnce(automationId);
98
+ await getAutomationCommand.action(options, undefined);
99
+ expect(listScope.isDone()).toBe(true);
100
+ expect(getScope.isDone()).toBe(true);
101
+ expect(mockPrompt).toHaveBeenCalledWith('Select the automation:', {
102
+ type: 'select',
103
+ options: [{ label: 'nightly', value: automationId }],
104
+ });
105
+ });
106
+ it('should exit when no automation matches the name', async () => {
107
+ const appId = 'app-123';
108
+ const options = { appId, name: 'missing' };
109
+ const scope = nock(DEFAULT_API_BASE_URL)
110
+ .get(`/v1/apps/${appId}/automations`)
111
+ .query({ name: 'missing' })
112
+ .matchHeader('Authorization', 'Bearer test-token')
113
+ .reply(200, []);
114
+ await expect(getAutomationCommand.action(options, undefined)).rejects.toThrow();
115
+ expect(scope.isDone()).toBe(true);
116
+ expect(mockConsola.error).toHaveBeenCalledWith('Automation not found.');
117
+ });
118
+ });
@@ -0,0 +1,46 @@
1
+ import appAutomationsService from '../../../services/app-automations.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: 'List all automations for an app.',
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
16
+ .enum(['android', 'ios', 'web'], {
17
+ message: 'Platform must be either `android`, `ios`, or `web`.',
18
+ })
19
+ .optional()
20
+ .describe('Only list automations for this platform.'),
21
+ })),
22
+ action: withAuth(async (options, args) => {
23
+ let { appId, json, limit, offset, platform } = 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
+ const automations = await appAutomationsService.findAll({
33
+ appId,
34
+ limit,
35
+ offset,
36
+ platform,
37
+ });
38
+ if (json) {
39
+ console.log(JSON.stringify(automations, null, 2));
40
+ }
41
+ else {
42
+ console.table(automations);
43
+ consola.success('Automations retrieved successfully.');
44
+ }
45
+ }),
46
+ });
@@ -0,0 +1,92 @@
1
+ import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
2
+ import authorizationService from '../../../services/authorization-service.js';
3
+ import { promptAppSelection, promptOrganizationSelection } 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 listAutomationsCommand from './list.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
+ vi.mock('@/utils/environment.js', () => ({
15
+ isInteractive: () => true,
16
+ }));
17
+ describe('apps-automations-list', () => {
18
+ const mockUserConfig = vi.mocked(userConfig);
19
+ const mockPromptOrganizationSelection = vi.mocked(promptOrganizationSelection);
20
+ const mockPromptAppSelection = vi.mocked(promptAppSelection);
21
+ const mockConsola = vi.mocked(consola);
22
+ const mockAuthorizationService = vi.mocked(authorizationService);
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ mockUserConfig.read.mockReturnValue({ token: 'test-token' });
26
+ mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
27
+ mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
28
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
29
+ throw new Error(`Process exited with code ${code}`);
30
+ });
31
+ });
32
+ afterEach(() => {
33
+ nock.cleanAll();
34
+ vi.restoreAllMocks();
35
+ });
36
+ it('should list automations', async () => {
37
+ const appId = 'app-123';
38
+ const automations = [{ id: 'automation-1', appId, name: 'nightly' }];
39
+ const options = { appId };
40
+ const tableSpy = vi.spyOn(console, 'table').mockImplementation(() => { });
41
+ const scope = nock(DEFAULT_API_BASE_URL)
42
+ .get(`/v1/apps/${appId}/automations`)
43
+ .matchHeader('Authorization', 'Bearer test-token')
44
+ .reply(200, automations);
45
+ await listAutomationsCommand.action(options, undefined);
46
+ expect(scope.isDone()).toBe(true);
47
+ expect(tableSpy).toHaveBeenCalledWith(automations);
48
+ expect(mockConsola.success).toHaveBeenCalledWith('Automations retrieved successfully.');
49
+ });
50
+ it('should pass pagination and platform filters', async () => {
51
+ const appId = 'app-123';
52
+ const options = { appId, limit: 5, offset: 10, platform: 'ios' };
53
+ vi.spyOn(console, 'table').mockImplementation(() => { });
54
+ const scope = nock(DEFAULT_API_BASE_URL)
55
+ .get(`/v1/apps/${appId}/automations`)
56
+ .query({ limit: '5', offset: '10', platform: 'ios' })
57
+ .matchHeader('Authorization', 'Bearer test-token')
58
+ .reply(200, []);
59
+ await listAutomationsCommand.action(options, undefined);
60
+ expect(scope.isDone()).toBe(true);
61
+ });
62
+ it('should output JSON when json flag is set', async () => {
63
+ const appId = 'app-123';
64
+ const automations = [{ id: 'automation-1', appId, name: 'nightly' }];
65
+ const options = { appId, json: true };
66
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
67
+ const scope = nock(DEFAULT_API_BASE_URL)
68
+ .get(`/v1/apps/${appId}/automations`)
69
+ .matchHeader('Authorization', 'Bearer test-token')
70
+ .reply(200, automations);
71
+ await listAutomationsCommand.action(options, undefined);
72
+ expect(scope.isDone()).toBe(true);
73
+ expect(logSpy).toHaveBeenCalledWith(JSON.stringify(automations, null, 2));
74
+ expect(mockConsola.success).not.toHaveBeenCalled();
75
+ });
76
+ it('should prompt for app when not provided', async () => {
77
+ const orgId = 'org-1';
78
+ const appId = 'app-1';
79
+ const options = {};
80
+ vi.spyOn(console, 'table').mockImplementation(() => { });
81
+ const scope = nock(DEFAULT_API_BASE_URL)
82
+ .get(`/v1/apps/${appId}/automations`)
83
+ .matchHeader('Authorization', 'Bearer test-token')
84
+ .reply(200, []);
85
+ mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
86
+ mockPromptAppSelection.mockResolvedValueOnce(appId);
87
+ await listAutomationsCommand.action(options, undefined);
88
+ expect(scope.isDone()).toBe(true);
89
+ expect(mockPromptOrganizationSelection).toHaveBeenCalled();
90
+ expect(mockPromptAppSelection).toHaveBeenCalledWith(orgId);
91
+ });
92
+ });
@@ -0,0 +1,118 @@
1
+ import appAutomationsService from '../../../services/app-automations.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
+ const clearableValue = (value) => (value === '' ? null : value);
9
+ export default defineCommand({
10
+ description: 'Update an existing app automation.',
11
+ options: defineOptions(z.object({
12
+ appId: z.string().optional().describe('ID of the app.'),
13
+ automationId: z.string().optional().describe('ID of the automation.'),
14
+ certificate: z
15
+ .string()
16
+ .optional()
17
+ .describe('The name of the certificate to use for the build. Pass an empty string to clear it.'),
18
+ channel: z
19
+ .string()
20
+ .optional()
21
+ .describe('The name of the channel to deploy to (Web only). Pass an empty string to clear it.'),
22
+ commitMessagePattern: z
23
+ .string()
24
+ .optional()
25
+ .describe('Only trigger for commits whose message matches this pattern (branch triggers only). Pass an empty string to clear it.'),
26
+ configuration: z
27
+ .string()
28
+ .optional()
29
+ .describe('The name of the native configuration (Android/iOS only). Pass an empty string to clear it.'),
30
+ destination: z
31
+ .string()
32
+ .optional()
33
+ .describe('The name of the destination to deploy to (Android/iOS only). Pass an empty string to clear it.'),
34
+ environment: z
35
+ .string()
36
+ .optional()
37
+ .describe('The name of the environment to use for the build. Pass an empty string to clear it.'),
38
+ json: z.boolean().optional().describe('Output in JSON format.'),
39
+ name: z.string().optional().describe('Name of the automation.'),
40
+ platform: z
41
+ .enum(['android', 'ios', 'web'], {
42
+ message: 'Platform must be either `android`, `ios`, or `web`.',
43
+ })
44
+ .optional()
45
+ .describe('The platform for the build. Supported values are `android`, `ios`, and `web`.'),
46
+ stack: z
47
+ .enum(['macos-sequoia', 'macos-tahoe'], {
48
+ message: 'Build stack must be either `macos-sequoia` or `macos-tahoe`.',
49
+ })
50
+ .optional()
51
+ .describe('The build stack to use for the build process.'),
52
+ triggerPattern: z
53
+ .string()
54
+ .optional()
55
+ .describe('Only trigger for branches or tags matching this pattern. Pass an empty string to clear it.'),
56
+ triggerType: z
57
+ .enum(['branch', 'tag'], {
58
+ message: 'Trigger type must be either `branch` or `tag`.',
59
+ })
60
+ .optional()
61
+ .describe('What triggers the automation. Supported values are `branch` and `tag`.'),
62
+ type: z
63
+ .enum(['app-store', 'ad-hoc', 'debug', 'development', 'release', 'simulator'], {
64
+ message: 'Build type must be one of `app-store`, `ad-hoc`, `debug`, `development`, `release`, or `simulator`.',
65
+ })
66
+ .optional()
67
+ .describe('The type of build to create.'),
68
+ })),
69
+ action: withAuth(async (options, args) => {
70
+ let { appId, automationId, json } = options;
71
+ if (!appId) {
72
+ if (!isInteractive()) {
73
+ consola.error('You must provide an app ID when running in non-interactive environment.');
74
+ process.exit(1);
75
+ }
76
+ const organizationId = await promptOrganizationSelection();
77
+ appId = await promptAppSelection(organizationId);
78
+ }
79
+ if (!automationId) {
80
+ if (!isInteractive()) {
81
+ consola.error('You must provide the automation ID when running in non-interactive environment.');
82
+ process.exit(1);
83
+ }
84
+ const automations = await appAutomationsService.findAll({ appId });
85
+ if (!automations.length) {
86
+ consola.error('No automations found for this app. Create one first.');
87
+ process.exit(1);
88
+ }
89
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
90
+ automationId = await prompt('Select the automation to update:', {
91
+ type: 'select',
92
+ options: automations.map((automation) => ({ label: automation.name, value: automation.id })),
93
+ });
94
+ }
95
+ await appAutomationsService.update({
96
+ appCertificateName: clearableValue(options.certificate),
97
+ appChannelName: clearableValue(options.channel),
98
+ appConfigurationName: clearableValue(options.configuration),
99
+ appDestinationName: clearableValue(options.destination),
100
+ appEnvironmentName: clearableValue(options.environment),
101
+ appId,
102
+ automationId,
103
+ buildStack: options.stack,
104
+ buildType: options.type,
105
+ commitMessagePattern: clearableValue(options.commitMessagePattern),
106
+ name: options.name,
107
+ platform: options.platform,
108
+ triggerPattern: clearableValue(options.triggerPattern),
109
+ triggerType: options.triggerType,
110
+ });
111
+ if (json) {
112
+ console.log(JSON.stringify({ id: automationId }, null, 2));
113
+ }
114
+ else {
115
+ consola.success('Automation updated successfully.');
116
+ }
117
+ }),
118
+ });
@@ -0,0 +1,124 @@
1
+ import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
2
+ import authorizationService from '../../../services/authorization-service.js';
3
+ import { prompt, promptAppSelection, promptOrganizationSelection } 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 updateAutomationCommand 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
+ vi.mock('@/utils/environment.js', () => ({
15
+ isInteractive: () => true,
16
+ }));
17
+ describe('apps-automations-update', () => {
18
+ const mockUserConfig = vi.mocked(userConfig);
19
+ const mockPrompt = vi.mocked(prompt);
20
+ const mockPromptOrganizationSelection = vi.mocked(promptOrganizationSelection);
21
+ const mockPromptAppSelection = vi.mocked(promptAppSelection);
22
+ const mockConsola = vi.mocked(consola);
23
+ const mockAuthorizationService = vi.mocked(authorizationService);
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ mockUserConfig.read.mockReturnValue({ token: 'test-token' });
27
+ mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
28
+ mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
29
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
30
+ throw new Error(`Process exited with code ${code}`);
31
+ });
32
+ });
33
+ afterEach(() => {
34
+ nock.cleanAll();
35
+ vi.restoreAllMocks();
36
+ });
37
+ it('should update automation with provided options', async () => {
38
+ const appId = 'app-123';
39
+ const automationId = 'automation-456';
40
+ const options = {
41
+ appId,
42
+ automationId,
43
+ configuration: 'Production',
44
+ name: 'renamed',
45
+ triggerPattern: 'release/*',
46
+ };
47
+ const scope = nock(DEFAULT_API_BASE_URL)
48
+ .patch(`/v1/apps/${appId}/automations/${automationId}`, {
49
+ appConfigurationName: 'Production',
50
+ name: 'renamed',
51
+ triggerPattern: 'release/*',
52
+ })
53
+ .matchHeader('Authorization', 'Bearer test-token')
54
+ .reply(204);
55
+ await updateAutomationCommand.action(options, undefined);
56
+ expect(scope.isDone()).toBe(true);
57
+ expect(mockConsola.success).toHaveBeenCalledWith('Automation updated successfully.');
58
+ });
59
+ it('should clear a reference when an empty string is passed', async () => {
60
+ const appId = 'app-123';
61
+ const automationId = 'automation-456';
62
+ const options = { appId, automationId, certificate: '', triggerPattern: '' };
63
+ const scope = nock(DEFAULT_API_BASE_URL)
64
+ .patch(`/v1/apps/${appId}/automations/${automationId}`, {
65
+ appCertificateName: null,
66
+ triggerPattern: null,
67
+ })
68
+ .matchHeader('Authorization', 'Bearer test-token')
69
+ .reply(204);
70
+ await updateAutomationCommand.action(options, undefined);
71
+ expect(scope.isDone()).toBe(true);
72
+ expect(mockConsola.success).toHaveBeenCalledWith('Automation updated successfully.');
73
+ });
74
+ it('should output JSON when json flag is set', async () => {
75
+ const appId = 'app-123';
76
+ const automationId = 'automation-456';
77
+ const options = { appId, automationId, json: true, name: 'renamed' };
78
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
79
+ const scope = nock(DEFAULT_API_BASE_URL)
80
+ .patch(`/v1/apps/${appId}/automations/${automationId}`, { name: 'renamed' })
81
+ .matchHeader('Authorization', 'Bearer test-token')
82
+ .reply(204);
83
+ await updateAutomationCommand.action(options, undefined);
84
+ expect(scope.isDone()).toBe(true);
85
+ expect(logSpy).toHaveBeenCalledWith(JSON.stringify({ id: automationId }, null, 2));
86
+ expect(mockConsola.success).not.toHaveBeenCalled();
87
+ });
88
+ it('should prompt for app and automation when not provided', async () => {
89
+ const orgId = 'org-1';
90
+ const appId = 'app-1';
91
+ const automationId = 'automation-456';
92
+ const options = { name: 'renamed' };
93
+ const listScope = nock(DEFAULT_API_BASE_URL)
94
+ .get(`/v1/apps/${appId}/automations`)
95
+ .matchHeader('Authorization', 'Bearer test-token')
96
+ .reply(200, [{ id: automationId, appId, name: 'nightly' }]);
97
+ const updateScope = nock(DEFAULT_API_BASE_URL)
98
+ .patch(`/v1/apps/${appId}/automations/${automationId}`, { name: 'renamed' })
99
+ .matchHeader('Authorization', 'Bearer test-token')
100
+ .reply(204);
101
+ mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
102
+ mockPromptAppSelection.mockResolvedValueOnce(appId);
103
+ mockPrompt.mockResolvedValueOnce(automationId);
104
+ await updateAutomationCommand.action(options, undefined);
105
+ expect(listScope.isDone()).toBe(true);
106
+ expect(updateScope.isDone()).toBe(true);
107
+ expect(mockPrompt).toHaveBeenCalledWith('Select the automation to update:', {
108
+ type: 'select',
109
+ options: [{ label: 'nightly', value: automationId }],
110
+ });
111
+ });
112
+ it('should handle API error', async () => {
113
+ const appId = 'app-123';
114
+ const automationId = 'automation-456';
115
+ const options = { appId, automationId, certificate: 'Missing' };
116
+ const scope = nock(DEFAULT_API_BASE_URL)
117
+ .patch(`/v1/apps/${appId}/automations/${automationId}`)
118
+ .matchHeader('Authorization', 'Bearer test-token')
119
+ .reply(400, { message: 'Certificate with name "Missing" not found for platform "android".' });
120
+ await expect(updateAutomationCommand.action(options, undefined)).rejects.toThrow();
121
+ expect(scope.isDone()).toBe(true);
122
+ expect(mockConsola.success).not.toHaveBeenCalled();
123
+ });
124
+ });
@@ -2,6 +2,7 @@ import { DEFAULT_CONSOLE_BASE_URL } from '../../../config/consts.js';
2
2
  import appBuildSourcesService from '../../../services/app-build-sources.js';
3
3
  import appBuildsService from '../../../services/app-builds.js';
4
4
  import appCertificatesService from '../../../services/app-certificates.js';
5
+ import appConfigurationsService from '../../../services/app-configurations.js';
5
6
  import appEnvironmentsService from '../../../services/app-environments.js';
6
7
  import appsService from '../../../services/apps.js';
7
8
  import { getAppBuildShareUrls } from '../../../utils/app-build-shares.js';
@@ -40,6 +41,7 @@ export default defineCommand({
40
41
  .describe('App ID to create the build for.'),
41
42
  certificate: z.string().optional().describe('The name of the certificate to use for the build.'),
42
43
  channel: z.string().optional().describe('The name of the channel to deploy to (Web only).'),
44
+ configuration: z.string().optional().describe('The name of the native configuration (Android/iOS only).'),
43
45
  destination: z.string().optional().describe('The name of the destination to deploy to (Android/iOS only).'),
44
46
  detached: z
45
47
  .boolean()
@@ -102,7 +104,7 @@ export default defineCommand({
102
104
  yes: z.boolean().optional().describe('Skip confirmation prompts.'),
103
105
  }), { y: 'yes' }),
104
106
  action: withAuth(async (options) => {
105
- let { appId, platform, type, gitRef, environment, certificate, json, stack, path: sourcePath, url } = options;
107
+ let { appId, platform, type, gitRef, environment, certificate, configuration, json, stack, path: sourcePath, url, } = options;
106
108
  // Validate that detached flag cannot be used with artifact flags
107
109
  if (options.detached && (options.apk || options.aab || options.ipa || options.zip)) {
108
110
  consola.error('The --detached flag cannot be used with --apk, --aab, --ipa, or --zip flags.');
@@ -251,6 +253,12 @@ export default defineCommand({
251
253
  consola.error('The --destination flag cannot be used with the web platform.');
252
254
  process.exit(1);
253
255
  }
256
+ // Validate that configuration is only used with the platforms that support it
257
+ const supportsConfiguration = platform === 'android' || platform === 'ios';
258
+ if (options.configuration && !supportsConfiguration) {
259
+ consola.error('The --configuration flag can only be used with the android and ios platforms.');
260
+ process.exit(1);
261
+ }
254
262
  // Prompt for environment if not provided
255
263
  if (!environment && !options.yes && isInteractive()) {
256
264
  // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
@@ -293,6 +301,27 @@ export default defineCommand({
293
301
  }
294
302
  }
295
303
  }
304
+ // Prompt for configuration if not provided
305
+ if (!configuration && supportsConfiguration && !options.yes && isInteractive()) {
306
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
307
+ const selectConfiguration = await prompt('Do you want to select a native configuration?', {
308
+ type: 'confirm',
309
+ initial: false,
310
+ });
311
+ if (selectConfiguration) {
312
+ const configurations = await appConfigurationsService.findAll({ appId });
313
+ if (configurations.length === 0) {
314
+ consola.warn('No native configurations found for this app.');
315
+ }
316
+ else {
317
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
318
+ configuration = await prompt('Select the native configuration for the build:', {
319
+ type: 'select',
320
+ options: configurations.map((config) => ({ label: config.name, value: config.name })),
321
+ });
322
+ }
323
+ }
324
+ }
296
325
  // Parse ad hoc environment variables from inline and file
297
326
  const variablesMap = new Map();
298
327
  if (options.variableFile) {
@@ -348,6 +377,7 @@ export default defineCommand({
348
377
  adHocEnvironmentVariables,
349
378
  appBuildSourceId,
350
379
  appCertificateName: certificate,
380
+ appConfigurationName: configuration,
351
381
  appEnvironmentName: environment,
352
382
  appId,
353
383
  stack,
@@ -92,6 +92,21 @@ describe('apps-builds-create', () => {
92
92
  await expect(createCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
93
93
  expect(mockConsola.error).toHaveBeenCalledWith('The --share-description and --share-expires-in-days flags require --share.');
94
94
  });
95
+ it('should reject --configuration combined with a platform that does not support it', async () => {
96
+ const options = { appId, platform: 'web', gitRef: 'main', configuration: 'production' };
97
+ await expect(createCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
98
+ expect(mockConsola.error).toHaveBeenCalledWith('The --configuration flag can only be used with the android and ios platforms.');
99
+ });
100
+ it.each(['android', 'ios'])('should accept --configuration with the %s platform', async (platform) => {
101
+ const options = { appId, platform, gitRef: 'main', configuration: 'production', detached: true };
102
+ const buildScope = nock(DEFAULT_API_BASE_URL)
103
+ .post(`/v1/apps/${appId}/builds`, (body) => body.appConfigurationName === 'production')
104
+ .matchHeader('Authorization', `Bearer ${testToken}`)
105
+ .reply(201, { id: buildId, jobId: 'job-1', numberAsString: '42' });
106
+ await createCommand.action(options, undefined);
107
+ expect(buildScope.isDone()).toBe(true);
108
+ expect(mockConsola.error).not.toHaveBeenCalled();
109
+ });
95
110
  it('should reject a non-positive shareExpiresInDays value', () => {
96
111
  const schema = createCommand.options?.schema;
97
112
  for (const shareExpiresInDays of [0, -1]) {