@capawesome/cli 4.23.1 → 4.24.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 +7 -0
- package/dist/commands/apps/automations/update.js +3 -3
- package/dist/commands/apps/automations/update.test.js +12 -0
- package/dist/commands/apps/update.js +79 -0
- package/dist/commands/apps/update.test.js +117 -0
- package/dist/index.js +1 -0
- package/dist/utils/cli-options.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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.24.0](https://github.com/capawesome-team/cli/compare/v4.23.1...v4.24.0) (2026-10-01)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **apps:** add the `apps:update` command ([#237](https://github.com/capawesome-team/cli/issues/237)) ([274cd09](https://github.com/capawesome-team/cli/commit/274cd09ce2898aa309d2a6613d059e8fa999e5d9))
|
|
11
|
+
|
|
5
12
|
## [4.23.1](https://github.com/capawesome-team/cli/compare/v4.23.0...v4.23.1) (2026-10-01)
|
|
6
13
|
|
|
7
14
|
|
|
@@ -45,11 +45,11 @@ export default defineCommand({
|
|
|
45
45
|
.optional()
|
|
46
46
|
.describe('The platform for the build. Supported values are `android`, `ios`, and `web`.'),
|
|
47
47
|
stack: z
|
|
48
|
-
.enum(['macos-sequoia', 'macos-tahoe', 'macos-golden-gate'], {
|
|
48
|
+
.union([z.enum(['macos-sequoia', 'macos-tahoe', 'macos-golden-gate']), z.literal('')], {
|
|
49
49
|
message: 'Build stack must be one of `macos-sequoia`, `macos-tahoe`, or `macos-golden-gate`.',
|
|
50
50
|
})
|
|
51
51
|
.optional()
|
|
52
|
-
.describe(
|
|
52
|
+
.describe("The build stack to use for the build process. Pass an empty string to use the app's default build stack."),
|
|
53
53
|
triggerPattern: z
|
|
54
54
|
.array(z.string())
|
|
55
55
|
.optional()
|
|
@@ -101,7 +101,7 @@ export default defineCommand({
|
|
|
101
101
|
appEnvironmentName: clearableValue(options.environment),
|
|
102
102
|
appId,
|
|
103
103
|
automationId,
|
|
104
|
-
buildStack: options.stack,
|
|
104
|
+
buildStack: clearableValue(options.stack),
|
|
105
105
|
buildType: options.type,
|
|
106
106
|
commitMessagePattern: clearableValue(options.commitMessagePattern),
|
|
107
107
|
name: options.name,
|
|
@@ -71,6 +71,18 @@ describe('apps-automations-update', () => {
|
|
|
71
71
|
expect(scope.isDone()).toBe(true);
|
|
72
72
|
expect(mockConsola.success).toHaveBeenCalledWith('Automation updated successfully.');
|
|
73
73
|
});
|
|
74
|
+
it('should clear the build stack when an empty string is passed', async () => {
|
|
75
|
+
const appId = 'app-123';
|
|
76
|
+
const automationId = 'automation-456';
|
|
77
|
+
const options = { appId, automationId, stack: '' };
|
|
78
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
79
|
+
.patch(`/v1/apps/${appId}/automations/${automationId}`, { buildStack: null })
|
|
80
|
+
.matchHeader('Authorization', 'Bearer test-token')
|
|
81
|
+
.reply(204);
|
|
82
|
+
await updateAutomationCommand.action(options, undefined);
|
|
83
|
+
expect(scope.isDone()).toBe(true);
|
|
84
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Automation updated successfully.');
|
|
85
|
+
});
|
|
74
86
|
it('should output JSON when json flag is set', async () => {
|
|
75
87
|
const appId = 'app-123';
|
|
76
88
|
const automationId = 'automation-456';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import appsService from '../../services/apps.js';
|
|
2
|
+
import { withAuth } from '../../utils/auth.js';
|
|
3
|
+
import { clearableValue } from '../../utils/cli-options.js';
|
|
4
|
+
import { isInteractive } from '../../utils/environment.js';
|
|
5
|
+
import { promptAppSelection, promptOrganizationSelection } from '../../utils/prompt.js';
|
|
6
|
+
import consola from 'consola';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { defineCommand, defineOptions } from 'zodline';
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
description: 'Update an existing app.',
|
|
11
|
+
options: defineOptions(z.object({
|
|
12
|
+
appId: z.string().optional().describe('ID of the app.'),
|
|
13
|
+
channel: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('The name of the default channel for live updates. Pass an empty string to clear it.'),
|
|
17
|
+
channelDiscovery: z
|
|
18
|
+
.union([z.boolean(), z.stringbool()], {
|
|
19
|
+
message: 'Channel discovery must be either `true` or `false`.',
|
|
20
|
+
})
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Enable or disable channel discovery. Supported values are `true` and `false`.'),
|
|
23
|
+
configuration: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('The name of the default native configuration. Pass an empty string to clear it.'),
|
|
27
|
+
environment: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('The name of the default environment. Pass an empty string to clear it.'),
|
|
31
|
+
json: z.boolean().optional().describe('Output in JSON format.'),
|
|
32
|
+
name: z.string().optional().describe('Name of the app.'),
|
|
33
|
+
nextBuildNumber: z.coerce
|
|
34
|
+
.number()
|
|
35
|
+
.int()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('The next build number. Must be greater than the latest build number.'),
|
|
38
|
+
stack: z
|
|
39
|
+
.union([z.enum(['macos-sequoia', 'macos-tahoe', 'macos-golden-gate']), z.literal('')], {
|
|
40
|
+
message: 'Build stack must be one of `macos-sequoia`, `macos-tahoe`, or `macos-golden-gate`.',
|
|
41
|
+
})
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('The default build stack for builds without an explicit build stack. Must be one of `macos-sequoia`, `macos-tahoe`, or `macos-golden-gate`. Pass an empty string to use the Capawesome Cloud default.'),
|
|
44
|
+
type: z
|
|
45
|
+
.enum(['android', 'capacitor', 'cordova', 'ios'], {
|
|
46
|
+
message: 'Type must be one of `android`, `capacitor`, `cordova`, or `ios`.',
|
|
47
|
+
})
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Type of the app. Supported values are `android`, `capacitor`, `cordova`, and `ios`.'),
|
|
50
|
+
})),
|
|
51
|
+
action: withAuth(async (options, args) => {
|
|
52
|
+
let { appId, json } = options;
|
|
53
|
+
if (!appId) {
|
|
54
|
+
if (!isInteractive()) {
|
|
55
|
+
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const organizationId = await promptOrganizationSelection();
|
|
59
|
+
appId = await promptAppSelection(organizationId);
|
|
60
|
+
}
|
|
61
|
+
const app = await appsService.update({
|
|
62
|
+
appChannelDiscoveryEnabled: options.channelDiscovery,
|
|
63
|
+
appChannelName: clearableValue(options.channel),
|
|
64
|
+
appConfigurationName: clearableValue(options.configuration),
|
|
65
|
+
appEnvironmentName: clearableValue(options.environment),
|
|
66
|
+
appId,
|
|
67
|
+
buildStack: clearableValue(options.stack),
|
|
68
|
+
name: options.name,
|
|
69
|
+
nextAppBuildNumber: options.nextBuildNumber,
|
|
70
|
+
type: options.type,
|
|
71
|
+
});
|
|
72
|
+
if (json) {
|
|
73
|
+
console.log(JSON.stringify(app, null, 2));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
consola.success('App updated successfully.');
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
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 updateAppCommand 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-update', () => {
|
|
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 update app with provided options', async () => {
|
|
37
|
+
const appId = 'app-123';
|
|
38
|
+
const options = updateAppCommand.options.schema.parse({
|
|
39
|
+
appId,
|
|
40
|
+
channelDiscovery: 'false',
|
|
41
|
+
configuration: 'Production',
|
|
42
|
+
name: 'renamed',
|
|
43
|
+
nextBuildNumber: '42',
|
|
44
|
+
stack: 'macos-tahoe',
|
|
45
|
+
type: 'ios',
|
|
46
|
+
});
|
|
47
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
48
|
+
.patch(`/v1/apps/${appId}`, {
|
|
49
|
+
appChannelDiscoveryEnabled: false,
|
|
50
|
+
appConfigurationName: 'Production',
|
|
51
|
+
buildStack: 'macos-tahoe',
|
|
52
|
+
name: 'renamed',
|
|
53
|
+
nextAppBuildNumber: 42,
|
|
54
|
+
type: 'ios',
|
|
55
|
+
})
|
|
56
|
+
.matchHeader('Authorization', 'Bearer test-token')
|
|
57
|
+
.reply(200, { id: appId, name: 'renamed' });
|
|
58
|
+
await updateAppCommand.action(options, undefined);
|
|
59
|
+
expect(scope.isDone()).toBe(true);
|
|
60
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App updated successfully.');
|
|
61
|
+
});
|
|
62
|
+
it('should clear defaults when an empty string is passed', async () => {
|
|
63
|
+
const appId = 'app-123';
|
|
64
|
+
const options = { appId, channel: '', environment: '', stack: '' };
|
|
65
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
66
|
+
.patch(`/v1/apps/${appId}`, {
|
|
67
|
+
appChannelName: null,
|
|
68
|
+
appEnvironmentName: null,
|
|
69
|
+
buildStack: null,
|
|
70
|
+
})
|
|
71
|
+
.matchHeader('Authorization', 'Bearer test-token')
|
|
72
|
+
.reply(200, { id: appId });
|
|
73
|
+
await updateAppCommand.action(options, undefined);
|
|
74
|
+
expect(scope.isDone()).toBe(true);
|
|
75
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App updated successfully.');
|
|
76
|
+
});
|
|
77
|
+
it('should output JSON when json flag is set', async () => {
|
|
78
|
+
const appId = 'app-123';
|
|
79
|
+
const app = { id: appId, name: 'renamed', organizationId: 'org-1', type: 'capacitor' };
|
|
80
|
+
const options = { appId, json: true, name: 'renamed' };
|
|
81
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
82
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
83
|
+
.patch(`/v1/apps/${appId}`, { name: 'renamed' })
|
|
84
|
+
.matchHeader('Authorization', 'Bearer test-token')
|
|
85
|
+
.reply(200, app);
|
|
86
|
+
await updateAppCommand.action(options, undefined);
|
|
87
|
+
expect(scope.isDone()).toBe(true);
|
|
88
|
+
expect(logSpy).toHaveBeenCalledWith(JSON.stringify(app, null, 2));
|
|
89
|
+
expect(mockConsola.success).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
it('should prompt for app when not provided', async () => {
|
|
92
|
+
const orgId = 'org-1';
|
|
93
|
+
const appId = 'app-1';
|
|
94
|
+
const options = { name: 'renamed' };
|
|
95
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
96
|
+
.patch(`/v1/apps/${appId}`, { name: 'renamed' })
|
|
97
|
+
.matchHeader('Authorization', 'Bearer test-token')
|
|
98
|
+
.reply(200, { id: appId, name: 'renamed' });
|
|
99
|
+
mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
|
|
100
|
+
mockPromptAppSelection.mockResolvedValueOnce(appId);
|
|
101
|
+
await updateAppCommand.action(options, undefined);
|
|
102
|
+
expect(scope.isDone()).toBe(true);
|
|
103
|
+
expect(mockPromptAppSelection).toHaveBeenCalledWith(orgId);
|
|
104
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App updated successfully.');
|
|
105
|
+
});
|
|
106
|
+
it('should handle API error', async () => {
|
|
107
|
+
const appId = 'app-123';
|
|
108
|
+
const options = { appId, nextBuildNumber: 1 };
|
|
109
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
110
|
+
.patch(`/v1/apps/${appId}`)
|
|
111
|
+
.matchHeader('Authorization', 'Bearer test-token')
|
|
112
|
+
.reply(400, { message: 'The next build number must be greater than the latest build number.' });
|
|
113
|
+
await expect(updateAppCommand.action(options, undefined)).rejects.toThrow();
|
|
114
|
+
expect(scope.isDone()).toBe(true);
|
|
115
|
+
expect(mockConsola.success).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ const config = defineConfig({
|
|
|
32
32
|
'apps:list': await import('./commands/apps/list.js').then((mod) => mod.default),
|
|
33
33
|
'apps:transfer': await import('./commands/apps/transfer.js').then((mod) => mod.default),
|
|
34
34
|
'apps:unlink': await import('./commands/apps/unlink.js').then((mod) => mod.default),
|
|
35
|
+
'apps:update': await import('./commands/apps/update.js').then((mod) => mod.default),
|
|
35
36
|
'apps:automations:create': await import('./commands/apps/automations/create.js').then((mod) => mod.default),
|
|
36
37
|
'apps:automations:delete': await import('./commands/apps/automations/delete.js').then((mod) => mod.default),
|
|
37
38
|
'apps:automations:get': await import('./commands/apps/automations/get.js').then((mod) => mod.default),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capawesome/cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.24.0",
|
|
4
4
|
"description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@napi-rs/keyring": "1.3.0",
|
|
60
60
|
"@sentry/node": "10.58.0",
|
|
61
61
|
"adm-zip": "0.6.1",
|
|
62
|
-
"axios": "1.
|
|
62
|
+
"axios": "1.20.0",
|
|
63
63
|
"axios-retry": "4.5.0",
|
|
64
64
|
"c12": "3.3.3",
|
|
65
65
|
"consola": "3.3.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"semver": "7.6.3",
|
|
74
74
|
"systeminformation": "5.31.17",
|
|
75
75
|
"zod": "4.0.17",
|
|
76
|
-
"zodline": "0.3.
|
|
76
|
+
"zodline": "0.3.2"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@ionic/prettier-config": "4.0.0",
|