@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
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.18.0](https://github.com/capawesome-team/cli/compare/v4.17.3...v4.18.0) (2026-08-02)
6
+
7
+
8
+ ### Features
9
+
10
+ * **apps:** add `apps:builds:run` command ([#198](https://github.com/capawesome-team/cli/issues/198)) ([a3926fd](https://github.com/capawesome-team/cli/commit/a3926fd1514f2534f9132fccb1e9d7f7eb75565c))
11
+ * **apps:** add apps:automations commands ([#199](https://github.com/capawesome-team/cli/issues/199)) ([6e95719](https://github.com/capawesome-team/cli/commit/6e9571914754df2ba64c61e2d2172f2b2031785c))
12
+ * **apps:** add commands to manage native configurations ([#196](https://github.com/capawesome-team/cli/issues/196)) ([5a3480e](https://github.com/capawesome-team/cli/commit/5a3480e094e16a72ada5baac1ff1cbc8ffbfd512))
13
+
14
+ ## [4.17.3](https://github.com/capawesome-team/cli/compare/v4.17.2...v4.17.3) (2026-07-21)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **deps:** update `brace-expansion` to `5.0.7` ([1757368](https://github.com/capawesome-team/cli/commit/1757368e7eb796a0cc9a6975e460e340f51fd48a))
20
+
5
21
  ## [4.17.2](https://github.com/capawesome-team/cli/compare/v4.17.1...v4.17.2) (2026-07-18)
6
22
 
7
23
 
@@ -0,0 +1,141 @@
1
+ import appAutomationsService from '../../../services/app-automations.js';
2
+ import appsService from '../../../services/apps.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 { z } from 'zod';
9
+ export default defineCommand({
10
+ description: 'Create a new app automation.',
11
+ options: defineOptions(z.object({
12
+ appId: z.string().optional().describe('ID of the app.'),
13
+ certificate: z.string().optional().describe('The name of the certificate to use for the build.'),
14
+ channel: z.string().optional().describe('The name of the channel to deploy to (Web only).'),
15
+ commitMessagePattern: z
16
+ .string()
17
+ .optional()
18
+ .describe('Only trigger for commits whose message matches this pattern (branch triggers only).'),
19
+ configuration: z.string().optional().describe('The name of the native configuration (Android/iOS only).'),
20
+ destination: z.string().optional().describe('The name of the destination to deploy to (Android/iOS only).'),
21
+ environment: z.string().optional().describe('The name of the environment to use for the build.'),
22
+ json: z.boolean().optional().describe('Output in JSON format.'),
23
+ name: z.string().optional().describe('Name of the automation.'),
24
+ platform: z
25
+ .enum(['android', 'ios', 'web'], {
26
+ message: 'Platform must be either `android`, `ios`, or `web`.',
27
+ })
28
+ .optional()
29
+ .describe('The platform for the build. Supported values are `android`, `ios`, and `web`.'),
30
+ stack: z
31
+ .enum(['macos-sequoia', 'macos-tahoe'], {
32
+ message: 'Build stack must be either `macos-sequoia` or `macos-tahoe`.',
33
+ })
34
+ .optional()
35
+ .describe('The build stack to use for the build process.'),
36
+ triggerPattern: z
37
+ .string()
38
+ .optional()
39
+ .describe('Only trigger for branches or tags matching this pattern. Defaults to all.'),
40
+ triggerType: z
41
+ .enum(['branch', 'tag'], {
42
+ message: 'Trigger type must be either `branch` or `tag`.',
43
+ })
44
+ .optional()
45
+ .describe('What triggers the automation. Supported values are `branch` and `tag`.'),
46
+ type: z
47
+ .enum(['app-store', 'ad-hoc', 'debug', 'development', 'release', 'simulator'], {
48
+ message: 'Build type must be one of `app-store`, `ad-hoc`, `debug`, `development`, `release`, or `simulator`.',
49
+ })
50
+ .optional()
51
+ .describe('The type of build to create.'),
52
+ })),
53
+ action: withAuth(async (options, args) => {
54
+ let { appId, json, name, platform, triggerType } = options;
55
+ if (!appId) {
56
+ if (!isInteractive()) {
57
+ consola.error('You must provide an app ID when running in non-interactive environment.');
58
+ process.exit(1);
59
+ }
60
+ const organizationId = await promptOrganizationSelection();
61
+ appId = await promptAppSelection(organizationId);
62
+ }
63
+ if (!name) {
64
+ if (!isInteractive()) {
65
+ consola.error('You must provide the automation name when running in non-interactive environment.');
66
+ process.exit(1);
67
+ }
68
+ name = await prompt('Enter the name of the automation:', { type: 'text' });
69
+ if (!name) {
70
+ consola.error('You must provide an automation name.');
71
+ process.exit(1);
72
+ }
73
+ }
74
+ if (!triggerType) {
75
+ if (!isInteractive()) {
76
+ consola.error('You must provide the trigger type when running in non-interactive environment.');
77
+ process.exit(1);
78
+ }
79
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
80
+ triggerType = await prompt('Select what triggers the automation:', {
81
+ type: 'select',
82
+ options: [
83
+ { label: 'Branch', value: 'branch' },
84
+ { label: 'Tag', value: 'tag' },
85
+ ],
86
+ });
87
+ if (!triggerType) {
88
+ consola.error('You must select a trigger type.');
89
+ process.exit(1);
90
+ }
91
+ }
92
+ // Derive platform from app type for single-platform apps
93
+ if (!platform) {
94
+ const app = await appsService.findOne({ appId });
95
+ if (app.type === 'android' || app.type === 'ios') {
96
+ platform = app.type;
97
+ }
98
+ }
99
+ if (!platform) {
100
+ if (!isInteractive()) {
101
+ consola.error('You must provide a platform when running in non-interactive environment.');
102
+ process.exit(1);
103
+ }
104
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
105
+ platform = await prompt('Select the platform for the build:', {
106
+ type: 'select',
107
+ options: [
108
+ { label: 'Android', value: 'android' },
109
+ { label: 'iOS', value: 'ios' },
110
+ { label: 'Web', value: 'web' },
111
+ ],
112
+ });
113
+ if (!platform) {
114
+ consola.error('You must select a platform.');
115
+ process.exit(1);
116
+ }
117
+ }
118
+ const automation = await appAutomationsService.create({
119
+ appCertificateName: options.certificate,
120
+ appChannelName: options.channel,
121
+ appConfigurationName: options.configuration,
122
+ appDestinationName: options.destination,
123
+ appEnvironmentName: options.environment,
124
+ appId,
125
+ buildStack: options.stack,
126
+ buildType: options.type,
127
+ commitMessagePattern: options.commitMessagePattern,
128
+ name,
129
+ platform,
130
+ triggerPattern: options.triggerPattern,
131
+ triggerType,
132
+ });
133
+ if (json) {
134
+ console.log(JSON.stringify({ id: automation.id }, null, 2));
135
+ }
136
+ else {
137
+ consola.info(`Automation ID: ${automation.id}`);
138
+ consola.success('Automation created successfully.');
139
+ }
140
+ }),
141
+ });
@@ -0,0 +1,162 @@
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 createAutomationCommand 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
+ vi.mock('@/utils/environment.js', () => ({
15
+ isInteractive: () => true,
16
+ }));
17
+ describe('apps-automations-create', () => {
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 create automation with provided options', async () => {
38
+ const appId = 'app-123';
39
+ const automationId = 'automation-456';
40
+ const options = {
41
+ appId,
42
+ certificate: 'Release Keystore',
43
+ configuration: 'Production',
44
+ environment: 'Staging',
45
+ name: 'nightly',
46
+ platform: 'android',
47
+ stack: 'macos-tahoe',
48
+ triggerPattern: 'main',
49
+ triggerType: 'branch',
50
+ type: 'release',
51
+ };
52
+ const scope = nock(DEFAULT_API_BASE_URL)
53
+ .post(`/v1/apps/${appId}/automations`, {
54
+ appCertificateName: 'Release Keystore',
55
+ appConfigurationName: 'Production',
56
+ appEnvironmentName: 'Staging',
57
+ buildStack: 'macos-tahoe',
58
+ buildType: 'release',
59
+ name: 'nightly',
60
+ platform: 'android',
61
+ triggerPattern: 'main',
62
+ triggerType: 'branch',
63
+ })
64
+ .matchHeader('Authorization', 'Bearer test-token')
65
+ .reply(201, { id: automationId, name: 'nightly' });
66
+ await createAutomationCommand.action(options, undefined);
67
+ expect(scope.isDone()).toBe(true);
68
+ expect(mockConsola.info).toHaveBeenCalledWith(`Automation ID: ${automationId}`);
69
+ expect(mockConsola.success).toHaveBeenCalledWith('Automation created successfully.');
70
+ });
71
+ it('should output JSON when json flag is set', async () => {
72
+ const appId = 'app-123';
73
+ const automationId = 'automation-456';
74
+ const options = {
75
+ appId,
76
+ json: true,
77
+ name: 'nightly',
78
+ platform: 'web',
79
+ triggerType: 'tag',
80
+ };
81
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
82
+ const scope = nock(DEFAULT_API_BASE_URL)
83
+ .post(`/v1/apps/${appId}/automations`, {
84
+ name: 'nightly',
85
+ platform: 'web',
86
+ triggerType: 'tag',
87
+ })
88
+ .matchHeader('Authorization', 'Bearer test-token')
89
+ .reply(201, { id: automationId, name: 'nightly' });
90
+ await createAutomationCommand.action(options, undefined);
91
+ expect(scope.isDone()).toBe(true);
92
+ expect(logSpy).toHaveBeenCalledWith(JSON.stringify({ id: automationId }, null, 2));
93
+ expect(mockConsola.info).not.toHaveBeenCalled();
94
+ });
95
+ it('should prompt for app, name and trigger type when not provided', async () => {
96
+ const orgId = 'org-1';
97
+ const appId = 'app-1';
98
+ const automationId = 'automation-456';
99
+ const options = { platform: 'ios' };
100
+ const scope = nock(DEFAULT_API_BASE_URL)
101
+ .post(`/v1/apps/${appId}/automations`, {
102
+ name: 'releases',
103
+ platform: 'ios',
104
+ triggerType: 'tag',
105
+ })
106
+ .matchHeader('Authorization', 'Bearer test-token')
107
+ .reply(201, { id: automationId, name: 'releases' });
108
+ mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
109
+ mockPromptAppSelection.mockResolvedValueOnce(appId);
110
+ mockPrompt
111
+ .mockResolvedValueOnce('releases') // name
112
+ .mockResolvedValueOnce('tag'); // trigger type
113
+ await createAutomationCommand.action(options, undefined);
114
+ expect(scope.isDone()).toBe(true);
115
+ expect(mockPrompt).toHaveBeenCalledWith('Enter the name of the automation:', { type: 'text' });
116
+ expect(mockPrompt).toHaveBeenCalledWith('Select what triggers the automation:', {
117
+ type: 'select',
118
+ options: [
119
+ { label: 'Branch', value: 'branch' },
120
+ { label: 'Tag', value: 'tag' },
121
+ ],
122
+ });
123
+ expect(mockConsola.success).toHaveBeenCalledWith('Automation created successfully.');
124
+ });
125
+ it('should derive the platform from the app type when not provided', async () => {
126
+ const appId = 'app-123';
127
+ const automationId = 'automation-456';
128
+ const options = { appId, name: 'nightly', triggerType: 'branch' };
129
+ const appScope = nock(DEFAULT_API_BASE_URL)
130
+ .get(`/v1/apps/${appId}`)
131
+ .matchHeader('Authorization', 'Bearer test-token')
132
+ .reply(200, { id: appId, type: 'android' });
133
+ const createScope = nock(DEFAULT_API_BASE_URL)
134
+ .post(`/v1/apps/${appId}/automations`, {
135
+ name: 'nightly',
136
+ platform: 'android',
137
+ triggerType: 'branch',
138
+ })
139
+ .matchHeader('Authorization', 'Bearer test-token')
140
+ .reply(201, { id: automationId, name: 'nightly' });
141
+ await createAutomationCommand.action(options, undefined);
142
+ expect(appScope.isDone()).toBe(true);
143
+ expect(createScope.isDone()).toBe(true);
144
+ expect(mockConsola.success).toHaveBeenCalledWith('Automation created successfully.');
145
+ });
146
+ it('should handle API error', async () => {
147
+ const appId = 'app-123';
148
+ const options = {
149
+ appId,
150
+ name: 'nightly',
151
+ platform: 'android',
152
+ triggerType: 'branch',
153
+ };
154
+ const scope = nock(DEFAULT_API_BASE_URL)
155
+ .post(`/v1/apps/${appId}/automations`)
156
+ .matchHeader('Authorization', 'Bearer test-token')
157
+ .reply(400, { message: 'Automation with this name already exists.' });
158
+ await expect(createAutomationCommand.action(options, undefined)).rejects.toThrow();
159
+ expect(scope.isDone()).toBe(true);
160
+ expect(mockConsola.success).not.toHaveBeenCalled();
161
+ });
162
+ });
@@ -0,0 +1,66 @@
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
+ export default defineCommand({
9
+ description: 'Delete an app automation.',
10
+ options: defineOptions(z.object({
11
+ appId: z.string().optional().describe('ID of the app.'),
12
+ automationId: z.string().optional().describe('ID of the automation. Either the ID or name must be provided.'),
13
+ name: z.string().optional().describe('Name of the automation. Either the ID or name must be provided.'),
14
+ yes: z.boolean().optional().describe('Skip confirmation prompt.'),
15
+ }), { y: 'yes' }),
16
+ action: withAuth(async (options, args) => {
17
+ let { appId, automationId, name } = options;
18
+ if (!appId) {
19
+ if (!isInteractive()) {
20
+ consola.error('You must provide an app ID when running in non-interactive environment.');
21
+ process.exit(1);
22
+ }
23
+ const organizationId = await promptOrganizationSelection();
24
+ appId = await promptAppSelection(organizationId);
25
+ }
26
+ if (!automationId && !name) {
27
+ if (!isInteractive()) {
28
+ consola.error('You must provide either the automation ID or name when running in non-interactive environment.');
29
+ process.exit(1);
30
+ }
31
+ const automations = await appAutomationsService.findAll({ appId });
32
+ if (!automations.length) {
33
+ consola.error('No automations found for this app. Create one first.');
34
+ process.exit(1);
35
+ }
36
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
37
+ automationId = await prompt('Select the automation to delete:', {
38
+ type: 'select',
39
+ options: automations.map((automation) => ({ label: automation.name, value: automation.id })),
40
+ });
41
+ }
42
+ if (!automationId && name) {
43
+ const automations = await appAutomationsService.findAll({ appId, name });
44
+ const automation = automations[0];
45
+ if (!automation) {
46
+ consola.error(`No automation found with name '${name}'.`);
47
+ process.exit(1);
48
+ }
49
+ automationId = automation.id;
50
+ }
51
+ if (!automationId) {
52
+ consola.error('Automation not found.');
53
+ process.exit(1);
54
+ }
55
+ if (!options.yes && isInteractive()) {
56
+ const confirmed = await prompt('Are you sure you want to delete this automation?', {
57
+ type: 'confirm',
58
+ });
59
+ if (!confirmed) {
60
+ return;
61
+ }
62
+ }
63
+ await appAutomationsService.delete({ appId, automationId });
64
+ consola.success('Automation deleted successfully.');
65
+ }),
66
+ });
@@ -0,0 +1,127 @@
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 deleteAutomationCommand from './delete.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-delete', () => {
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 delete automation by ID after confirmation', async () => {
38
+ const appId = 'app-123';
39
+ const automationId = 'automation-456';
40
+ const options = { appId, automationId };
41
+ mockPrompt.mockResolvedValueOnce(true); // confirmation
42
+ const scope = nock(DEFAULT_API_BASE_URL)
43
+ .delete(`/v1/apps/${appId}/automations/${automationId}`)
44
+ .matchHeader('Authorization', 'Bearer test-token')
45
+ .reply(204);
46
+ await deleteAutomationCommand.action(options, undefined);
47
+ expect(scope.isDone()).toBe(true);
48
+ expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to delete this automation?', {
49
+ type: 'confirm',
50
+ });
51
+ expect(mockConsola.success).toHaveBeenCalledWith('Automation deleted successfully.');
52
+ });
53
+ it('should delete automation by name when yes flag is set', async () => {
54
+ const appId = 'app-123';
55
+ const automationId = 'automation-456';
56
+ const options = { appId, name: 'nightly', yes: true };
57
+ const listScope = nock(DEFAULT_API_BASE_URL)
58
+ .get(`/v1/apps/${appId}/automations`)
59
+ .query({ name: 'nightly' })
60
+ .matchHeader('Authorization', 'Bearer test-token')
61
+ .reply(200, [{ id: automationId, appId, name: 'nightly' }]);
62
+ const deleteScope = nock(DEFAULT_API_BASE_URL)
63
+ .delete(`/v1/apps/${appId}/automations/${automationId}`)
64
+ .matchHeader('Authorization', 'Bearer test-token')
65
+ .reply(204);
66
+ await deleteAutomationCommand.action(options, undefined);
67
+ expect(listScope.isDone()).toBe(true);
68
+ expect(deleteScope.isDone()).toBe(true);
69
+ expect(mockPrompt).not.toHaveBeenCalled();
70
+ expect(mockConsola.success).toHaveBeenCalledWith('Automation deleted successfully.');
71
+ });
72
+ it('should exit when no automation matches the name', async () => {
73
+ const appId = 'app-123';
74
+ const options = { appId, name: 'missing', yes: true };
75
+ const scope = nock(DEFAULT_API_BASE_URL)
76
+ .get(`/v1/apps/${appId}/automations`)
77
+ .query({ name: 'missing' })
78
+ .matchHeader('Authorization', 'Bearer test-token')
79
+ .reply(200, []);
80
+ await expect(deleteAutomationCommand.action(options, undefined)).rejects.toThrow();
81
+ expect(scope.isDone()).toBe(true);
82
+ expect(mockConsola.error).toHaveBeenCalledWith("No automation found with name 'missing'.");
83
+ });
84
+ it('should not delete automation when confirmation is declined', async () => {
85
+ const options = { appId: 'app-123', automationId: 'automation-456' };
86
+ mockPrompt.mockResolvedValueOnce(false); // declined confirmation
87
+ await deleteAutomationCommand.action(options, undefined);
88
+ expect(mockConsola.success).not.toHaveBeenCalled();
89
+ });
90
+ it('should prompt for app and automation when not provided', async () => {
91
+ const orgId = 'org-1';
92
+ const appId = 'app-1';
93
+ const automationId = 'automation-456';
94
+ const options = {};
95
+ const listScope = nock(DEFAULT_API_BASE_URL)
96
+ .get(`/v1/apps/${appId}/automations`)
97
+ .matchHeader('Authorization', 'Bearer test-token')
98
+ .reply(200, [{ id: automationId, appId, name: 'nightly' }]);
99
+ const deleteScope = nock(DEFAULT_API_BASE_URL)
100
+ .delete(`/v1/apps/${appId}/automations/${automationId}`)
101
+ .matchHeader('Authorization', 'Bearer test-token')
102
+ .reply(204);
103
+ mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
104
+ mockPromptAppSelection.mockResolvedValueOnce(appId);
105
+ mockPrompt
106
+ .mockResolvedValueOnce(automationId) // automation selection
107
+ .mockResolvedValueOnce(true); // confirmation
108
+ await deleteAutomationCommand.action(options, undefined);
109
+ expect(listScope.isDone()).toBe(true);
110
+ expect(deleteScope.isDone()).toBe(true);
111
+ expect(mockPrompt).toHaveBeenCalledWith('Select the automation to delete:', {
112
+ type: 'select',
113
+ options: [{ label: 'nightly', value: automationId }],
114
+ });
115
+ });
116
+ it('should handle API error', async () => {
117
+ const appId = 'app-123';
118
+ const automationId = 'automation-456';
119
+ const options = { appId, automationId, yes: true };
120
+ const scope = nock(DEFAULT_API_BASE_URL)
121
+ .delete(`/v1/apps/${appId}/automations/${automationId}`)
122
+ .matchHeader('Authorization', 'Bearer test-token')
123
+ .reply(404, { message: 'Automation not found.' });
124
+ await expect(deleteAutomationCommand.action(options, undefined)).rejects.toThrow();
125
+ expect(scope.isDone()).toBe(true);
126
+ });
127
+ });
@@ -0,0 +1,62 @@
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
+ export default defineCommand({
9
+ description: 'Get an existing app automation.',
10
+ options: defineOptions(z.object({
11
+ appId: z.string().optional().describe('ID of the app.'),
12
+ automationId: z.string().optional().describe('ID of the automation. Either the ID or name must be provided.'),
13
+ json: z.boolean().optional().describe('Output in JSON format.'),
14
+ name: z.string().optional().describe('Name of the automation. Either the ID or name must be provided.'),
15
+ })),
16
+ action: withAuth(async (options, args) => {
17
+ let { appId, automationId, json, name } = options;
18
+ if (!appId) {
19
+ if (!isInteractive()) {
20
+ consola.error('You must provide an app ID when running in non-interactive environment.');
21
+ process.exit(1);
22
+ }
23
+ const organizationId = await promptOrganizationSelection();
24
+ appId = await promptAppSelection(organizationId);
25
+ }
26
+ if (!automationId && !name) {
27
+ if (!isInteractive()) {
28
+ consola.error('You must provide either the automation ID or name when running in non-interactive environment.');
29
+ process.exit(1);
30
+ }
31
+ const automations = await appAutomationsService.findAll({ appId });
32
+ if (!automations.length) {
33
+ consola.error('No automations found for this app. Create one first.');
34
+ process.exit(1);
35
+ }
36
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
37
+ automationId = await prompt('Select the automation:', {
38
+ type: 'select',
39
+ options: automations.map((automation) => ({ label: automation.name, value: automation.id })),
40
+ });
41
+ }
42
+ let automation;
43
+ if (automationId) {
44
+ automation = await appAutomationsService.findOneById({ appId, automationId });
45
+ }
46
+ else if (name) {
47
+ const automations = await appAutomationsService.findAll({ appId, name });
48
+ automation = automations[0];
49
+ }
50
+ if (!automation) {
51
+ consola.error('Automation not found.');
52
+ process.exit(1);
53
+ }
54
+ if (json) {
55
+ console.log(JSON.stringify(automation, null, 2));
56
+ }
57
+ else {
58
+ console.table(automation);
59
+ consola.success('Automation retrieved successfully.');
60
+ }
61
+ }),
62
+ });