@capawesome/cli 1.14.0-dev.8c382fa.1755584275 → 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 +4 -0
- package/dist/commands/apps/bundles/create.js +23 -13
- package/dist/commands/apps/bundles/create.test.js +276 -0
- package/dist/commands/apps/bundles/delete.js +13 -16
- package/dist/commands/apps/bundles/delete.test.js +139 -0
- package/dist/commands/apps/bundles/update.js +13 -21
- package/dist/commands/apps/bundles/update.test.js +141 -0
- package/dist/commands/apps/channels/create.js +16 -6
- package/dist/commands/apps/channels/create.test.js +119 -0
- package/dist/commands/apps/channels/delete.js +15 -18
- package/dist/commands/apps/channels/delete.test.js +141 -0
- package/dist/commands/apps/channels/get.js +34 -42
- package/dist/commands/apps/channels/get.test.js +135 -0
- package/dist/commands/apps/channels/list.js +20 -28
- package/dist/commands/apps/channels/list.test.js +121 -0
- package/dist/commands/apps/channels/update.js +10 -18
- package/dist/commands/apps/channels/update.test.js +138 -0
- package/dist/commands/apps/create.js +11 -14
- package/dist/commands/apps/create.test.js +117 -0
- package/dist/commands/apps/delete.js +10 -13
- package/dist/commands/apps/delete.test.js +120 -0
- package/dist/commands/apps/devices/delete.js +13 -16
- package/dist/commands/apps/devices/delete.test.js +139 -0
- package/dist/commands/doctor.test.js +52 -0
- package/dist/commands/login.js +11 -11
- package/dist/commands/login.test.js +116 -0
- package/dist/commands/logout.js +1 -1
- package/dist/commands/logout.test.js +47 -0
- 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 +10 -4
- package/dist/commands/whoami.test.js +30 -0
- package/dist/config/consts.js +2 -0
- package/dist/index.js +4 -0
- package/dist/services/apps.js +3 -2
- package/dist/services/authorization-service.js +1 -1
- package/dist/services/config.js +3 -2
- package/dist/services/organizations.js +8 -0
- package/dist/utils/buffer.js +6 -0
- package/dist/utils/private-key.js +23 -0
- package/package.json +10 -4
- /package/dist/utils/{userConfig.js → user-config.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
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
|
+
## [2.0.0](https://github.com/capawesome-team/cli/compare/v1.14.0...v2.0.0) (2025-08-23)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* You should now call the CLI using `@capawesome/cli` instead of just `capawesome`.
|
|
11
|
+
* set npm minimum version to `8.0.0`
|
|
12
|
+
* set Node.js minimum version to `16.0.0`
|
|
13
|
+
|
|
14
|
+
* deprecate `capawesome` command ([d59ea30](https://github.com/capawesome-team/cli/commit/d59ea305b7bb873071162b2ef896fc2f87b7ea21))
|
|
15
|
+
* set Node.js minimum version to `16.0.0` ([396688c](https://github.com/capawesome-team/cli/commit/396688c96b69131dc0433e79af2d79eca19ee7fe))
|
|
16
|
+
* set npm minimum version to `8.0.0` ([8c382fa](https://github.com/capawesome-team/cli/commit/8c382fab37c53b0df8d7efc18e6a0efbb45e1c39))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* add `organizations:create` command ([a3b5855](https://github.com/capawesome-team/cli/commit/a3b5855a84f0c58cc0463b40c9965811837d92bb))
|
|
22
|
+
* add link to report bugs ([#64](https://github.com/capawesome-team/cli/issues/64)) ([9eb95b2](https://github.com/capawesome-team/cli/commit/9eb95b2d1e8b47cb0f13f26aae6cccb50ec401e0)), closes [#56](https://github.com/capawesome-team/cli/issues/56)
|
|
23
|
+
* **apps:bundles:create:** support private key as plain text or file path ([#62](https://github.com/capawesome-team/cli/issues/62)) ([1db77a2](https://github.com/capawesome-team/cli/commit/1db77a2aa665f8f28e459598112a37e22c391944)), closes [#25](https://github.com/capawesome-team/cli/issues/25)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* add missing authorization token checks to commands ([6699b1e](https://github.com/capawesome-team/cli/commit/6699b1e6993cc8f4ff5cdcd5ebef64de53a93b0e))
|
|
29
|
+
* handle private keys without line breaks ([#66](https://github.com/capawesome-team/cli/issues/66)) ([d1b8d2f](https://github.com/capawesome-team/cli/commit/d1b8d2f69e64b9c97d0117a305e9bca0ec158d4f)), closes [#30](https://github.com/capawesome-team/cli/issues/30)
|
|
30
|
+
* **utils:** add error message for 403 status ([6922448](https://github.com/capawesome-team/cli/commit/69224483114190ff0b81e0dcfeb656e7ea208f54))
|
|
31
|
+
|
|
5
32
|
## [1.14.0](https://github.com/capawesome-team/cli/compare/v1.13.2...v1.14.0) (2025-08-11)
|
|
6
33
|
|
|
7
34
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# cli
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@capawesome/cli)
|
|
4
|
+
[](https://www.npmjs.com/package/@capawesome/cli)
|
|
5
|
+
[](https://github.com/capawesome-team/cli/blob/main/LICENSE)
|
|
6
|
+
|
|
3
7
|
💻 The Capawesome Cloud Command Line Interface (CLI) can be used to manage [Live Updates](https://capawesome.io/cloud/) from the command line.
|
|
4
8
|
|
|
5
9
|
## Installation
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
2
|
-
import consola from 'consola';
|
|
3
|
-
import { createReadStream } from 'fs';
|
|
4
|
-
import { z } from 'zod';
|
|
5
1
|
import { MAX_CONCURRENT_UPLOADS } from '../../../config/index.js';
|
|
6
2
|
import appBundleFilesService from '../../../services/app-bundle-files.js';
|
|
7
3
|
import appBundlesService from '../../../services/app-bundles.js';
|
|
8
4
|
import appsService from '../../../services/apps.js';
|
|
9
5
|
import authorizationService from '../../../services/authorization-service.js';
|
|
10
6
|
import organizationsService from '../../../services/organizations.js';
|
|
11
|
-
import { createBufferFromPath, createBufferFromReadStream } from '../../../utils/buffer.js';
|
|
12
|
-
import {
|
|
7
|
+
import { createBufferFromPath, createBufferFromReadStream, createBufferFromString, isPrivateKeyContent, } from '../../../utils/buffer.js';
|
|
8
|
+
import { formatPrivateKey } from '../../../utils/private-key.js';
|
|
13
9
|
import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
|
|
14
10
|
import { createHash } from '../../../utils/hash.js';
|
|
15
11
|
import { generateManifestJson } from '../../../utils/manifest.js';
|
|
16
12
|
import { prompt } from '../../../utils/prompt.js';
|
|
17
13
|
import { createSignature } from '../../../utils/signature.js';
|
|
18
14
|
import zip from '../../../utils/zip.js';
|
|
15
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
16
|
+
import consola from 'consola';
|
|
17
|
+
import { createReadStream } from 'fs';
|
|
18
|
+
import { z } from 'zod';
|
|
19
19
|
export default defineCommand({
|
|
20
20
|
description: 'Create a new app bundle.',
|
|
21
21
|
options: defineOptions(z.object({
|
|
@@ -72,7 +72,10 @@ export default defineCommand({
|
|
|
72
72
|
.string()
|
|
73
73
|
.optional()
|
|
74
74
|
.describe('Path to the bundle to upload. Must be a folder (e.g. `www` or `dist`) or a zip file.'),
|
|
75
|
-
privateKey: z
|
|
75
|
+
privateKey: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('The private key to sign the bundle with. Can be a file path to a .pem file or the private key content as plain text.'),
|
|
76
79
|
rollout: z.coerce
|
|
77
80
|
.number()
|
|
78
81
|
.min(0)
|
|
@@ -189,10 +192,19 @@ export default defineCommand({
|
|
|
189
192
|
// Create the private key buffer
|
|
190
193
|
let privateKeyBuffer;
|
|
191
194
|
if (privateKey) {
|
|
192
|
-
if (privateKey
|
|
195
|
+
if (isPrivateKeyContent(privateKey)) {
|
|
196
|
+
// Handle plain text private key content
|
|
197
|
+
const formattedPrivateKey = formatPrivateKey(privateKey);
|
|
198
|
+
privateKeyBuffer = createBufferFromString(formattedPrivateKey);
|
|
199
|
+
}
|
|
200
|
+
else if (privateKey.endsWith('.pem')) {
|
|
201
|
+
// Handle file path
|
|
193
202
|
const fileExists = await fileExistsAtPath(privateKey);
|
|
194
203
|
if (fileExists) {
|
|
195
|
-
|
|
204
|
+
const keyBuffer = await createBufferFromPath(privateKey);
|
|
205
|
+
const keyContent = keyBuffer.toString('utf8');
|
|
206
|
+
const formattedPrivateKey = formatPrivateKey(keyContent);
|
|
207
|
+
privateKeyBuffer = createBufferFromString(formattedPrivateKey);
|
|
196
208
|
}
|
|
197
209
|
else {
|
|
198
210
|
consola.error('Private key file not found.');
|
|
@@ -200,7 +212,7 @@ export default defineCommand({
|
|
|
200
212
|
}
|
|
201
213
|
}
|
|
202
214
|
else {
|
|
203
|
-
consola.error('Private key must be a path to a .pem file.');
|
|
215
|
+
consola.error('Private key must be either a path to a .pem file or the private key content as plain text.');
|
|
204
216
|
process.exit(1);
|
|
205
217
|
}
|
|
206
218
|
}
|
|
@@ -277,9 +289,7 @@ export default defineCommand({
|
|
|
277
289
|
// No-op
|
|
278
290
|
});
|
|
279
291
|
}
|
|
280
|
-
|
|
281
|
-
consola.error(message);
|
|
282
|
-
process.exit(1);
|
|
292
|
+
throw error;
|
|
283
293
|
}
|
|
284
294
|
},
|
|
285
295
|
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
3
|
+
import { fileExistsAtPath, isDirectory } from '../../../utils/file.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 createBundleCommand from './create.js';
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@/utils/user-config.js');
|
|
11
|
+
vi.mock('@/services/authorization-service.js');
|
|
12
|
+
vi.mock('@/utils/file.js');
|
|
13
|
+
vi.mock('@/utils/zip.js');
|
|
14
|
+
vi.mock('@/utils/buffer.js');
|
|
15
|
+
vi.mock('@/utils/private-key.js');
|
|
16
|
+
vi.mock('@/utils/hash.js');
|
|
17
|
+
vi.mock('@/utils/signature.js');
|
|
18
|
+
vi.mock('consola');
|
|
19
|
+
describe('apps-bundles-create', () => {
|
|
20
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
21
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
22
|
+
const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
|
|
23
|
+
const mockIsDirectory = vi.mocked(isDirectory);
|
|
24
|
+
const mockConsola = vi.mocked(consola);
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
28
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
29
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
30
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
31
|
+
throw new Error(`Process exited with code ${code}`);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
nock.cleanAll();
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
it('should require authentication', async () => {
|
|
39
|
+
const appId = 'app-123';
|
|
40
|
+
const options = { appId, path: './dist', artifactType: 'zip', rollout: 1 };
|
|
41
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
|
|
42
|
+
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
43
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command.');
|
|
44
|
+
});
|
|
45
|
+
it('should create bundle with self-hosted URL', async () => {
|
|
46
|
+
const appId = 'app-123';
|
|
47
|
+
const bundleUrl = 'https://example.com/bundle.zip';
|
|
48
|
+
const bundlePath = './bundle.zip';
|
|
49
|
+
const testHash = 'test-hash';
|
|
50
|
+
const bundleId = 'bundle-456';
|
|
51
|
+
const testToken = 'test-token';
|
|
52
|
+
const testBuffer = Buffer.from('test');
|
|
53
|
+
const options = {
|
|
54
|
+
appId,
|
|
55
|
+
url: bundleUrl,
|
|
56
|
+
path: bundlePath,
|
|
57
|
+
artifactType: 'zip',
|
|
58
|
+
rollout: 1,
|
|
59
|
+
};
|
|
60
|
+
mockFileExistsAtPath.mockResolvedValue(true);
|
|
61
|
+
mockIsDirectory.mockResolvedValue(false);
|
|
62
|
+
// Mock utility functions
|
|
63
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
64
|
+
const mockBuffer = await import('../../../utils/buffer.js');
|
|
65
|
+
const mockHash = await import('../../../utils/hash.js');
|
|
66
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
|
|
67
|
+
vi.mocked(mockBuffer.createBufferFromPath).mockResolvedValue(testBuffer);
|
|
68
|
+
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
|
|
69
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
70
|
+
.post(`/v1/apps/${appId}/bundles`, {
|
|
71
|
+
appId,
|
|
72
|
+
url: bundleUrl,
|
|
73
|
+
checksum: testHash,
|
|
74
|
+
artifactType: 'zip',
|
|
75
|
+
rolloutPercentage: 1,
|
|
76
|
+
})
|
|
77
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
78
|
+
.reply(201, { id: bundleId });
|
|
79
|
+
await createBundleCommand.action(options, undefined);
|
|
80
|
+
expect(scope.isDone()).toBe(true);
|
|
81
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Bundle successfully created.');
|
|
82
|
+
expect(mockConsola.info).toHaveBeenCalledWith(`Bundle ID: ${bundleId}`);
|
|
83
|
+
});
|
|
84
|
+
it('should handle path validation errors', async () => {
|
|
85
|
+
const appId = 'app-123';
|
|
86
|
+
const nonexistentPath = './nonexistent';
|
|
87
|
+
const options = { appId, path: nonexistentPath, artifactType: 'zip', rollout: 1 };
|
|
88
|
+
mockFileExistsAtPath.mockResolvedValue(false);
|
|
89
|
+
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
90
|
+
expect(mockConsola.error).toHaveBeenCalledWith('The path does not exist.');
|
|
91
|
+
});
|
|
92
|
+
it('should validate manifest artifact type requires directory', async () => {
|
|
93
|
+
const appId = 'app-123';
|
|
94
|
+
const bundlePath = './bundle.zip';
|
|
95
|
+
const options = {
|
|
96
|
+
appId,
|
|
97
|
+
path: bundlePath,
|
|
98
|
+
artifactType: 'manifest',
|
|
99
|
+
rollout: 1,
|
|
100
|
+
};
|
|
101
|
+
mockFileExistsAtPath.mockResolvedValue(true);
|
|
102
|
+
mockIsDirectory.mockResolvedValue(false);
|
|
103
|
+
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
104
|
+
expect(mockConsola.error).toHaveBeenCalledWith('The path must be a folder when creating a bundle with an artifact type of `manifest`.');
|
|
105
|
+
});
|
|
106
|
+
it('should validate manifest artifact type cannot use URL', async () => {
|
|
107
|
+
const appId = 'app-123';
|
|
108
|
+
const bundleUrl = 'https://example.com/bundle.zip';
|
|
109
|
+
const options = {
|
|
110
|
+
appId,
|
|
111
|
+
url: bundleUrl,
|
|
112
|
+
artifactType: 'manifest',
|
|
113
|
+
rollout: 1,
|
|
114
|
+
};
|
|
115
|
+
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
116
|
+
expect(mockConsola.error).toHaveBeenCalledWith('It is not yet possible to provide a URL when creating a bundle with an artifact type of `manifest`.');
|
|
117
|
+
});
|
|
118
|
+
it('should handle API error during creation', async () => {
|
|
119
|
+
const appId = 'app-123';
|
|
120
|
+
const bundleUrl = 'https://example.com/bundle.zip';
|
|
121
|
+
const testToken = 'test-token';
|
|
122
|
+
const options = {
|
|
123
|
+
appId,
|
|
124
|
+
url: bundleUrl,
|
|
125
|
+
artifactType: 'zip',
|
|
126
|
+
rollout: 1,
|
|
127
|
+
};
|
|
128
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
129
|
+
.post(`/v1/apps/${appId}/bundles`)
|
|
130
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
131
|
+
.reply(400, { message: 'Invalid bundle data' });
|
|
132
|
+
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow();
|
|
133
|
+
expect(scope.isDone()).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
it('should handle private key file path', async () => {
|
|
136
|
+
const appId = 'app-123';
|
|
137
|
+
const bundleUrl = 'https://example.com/bundle.zip';
|
|
138
|
+
const bundlePath = './bundle.zip';
|
|
139
|
+
const privateKeyPath = 'private-key.pem';
|
|
140
|
+
const testHash = 'test-hash';
|
|
141
|
+
const testSignature = 'test-signature';
|
|
142
|
+
const bundleId = 'bundle-456';
|
|
143
|
+
const testToken = 'test-token';
|
|
144
|
+
const testBuffer = Buffer.from('test');
|
|
145
|
+
const options = {
|
|
146
|
+
appId,
|
|
147
|
+
url: bundleUrl,
|
|
148
|
+
path: bundlePath,
|
|
149
|
+
privateKey: privateKeyPath,
|
|
150
|
+
artifactType: 'zip',
|
|
151
|
+
rollout: 1,
|
|
152
|
+
};
|
|
153
|
+
mockFileExistsAtPath.mockImplementation((path) => {
|
|
154
|
+
if (path === privateKeyPath)
|
|
155
|
+
return Promise.resolve(true);
|
|
156
|
+
if (path === bundlePath)
|
|
157
|
+
return Promise.resolve(true);
|
|
158
|
+
return Promise.resolve(false);
|
|
159
|
+
});
|
|
160
|
+
mockIsDirectory.mockResolvedValue(false);
|
|
161
|
+
// Mock utility functions
|
|
162
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
163
|
+
const mockBuffer = await import('../../../utils/buffer.js');
|
|
164
|
+
const mockPrivateKey = await import('../../../utils/private-key.js');
|
|
165
|
+
const mockHash = await import('../../../utils/hash.js');
|
|
166
|
+
const mockSignature = await import('../../../utils/signature.js');
|
|
167
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
|
|
168
|
+
vi.mocked(mockBuffer.createBufferFromPath).mockResolvedValue(testBuffer);
|
|
169
|
+
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
|
|
170
|
+
vi.mocked(mockPrivateKey.formatPrivateKey).mockReturnValue('formatted-private-key');
|
|
171
|
+
vi.mocked(mockBuffer.createBufferFromString).mockReturnValue(testBuffer);
|
|
172
|
+
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
|
|
173
|
+
vi.mocked(mockSignature.createSignature).mockResolvedValue(testSignature);
|
|
174
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
175
|
+
.post(`/v1/apps/${appId}/bundles`, {
|
|
176
|
+
appId,
|
|
177
|
+
url: bundleUrl,
|
|
178
|
+
checksum: testHash,
|
|
179
|
+
signature: testSignature,
|
|
180
|
+
artifactType: 'zip',
|
|
181
|
+
rolloutPercentage: 1,
|
|
182
|
+
})
|
|
183
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
184
|
+
.reply(201, { id: bundleId });
|
|
185
|
+
await createBundleCommand.action(options, undefined);
|
|
186
|
+
expect(scope.isDone()).toBe(true);
|
|
187
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Bundle successfully created.');
|
|
188
|
+
});
|
|
189
|
+
it('should handle private key plain text content', async () => {
|
|
190
|
+
const appId = 'app-123';
|
|
191
|
+
const bundleUrl = 'https://example.com/bundle.zip';
|
|
192
|
+
const bundlePath = './bundle.zip';
|
|
193
|
+
const privateKeyContent = '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCgxvzJrMCbmtjb\n-----END PRIVATE KEY-----';
|
|
194
|
+
const testHash = 'test-hash';
|
|
195
|
+
const testSignature = 'test-signature';
|
|
196
|
+
const bundleId = 'bundle-456';
|
|
197
|
+
const testToken = 'test-token';
|
|
198
|
+
const testBuffer = Buffer.from('test');
|
|
199
|
+
const options = {
|
|
200
|
+
appId,
|
|
201
|
+
url: bundleUrl,
|
|
202
|
+
path: bundlePath,
|
|
203
|
+
privateKey: privateKeyContent,
|
|
204
|
+
artifactType: 'zip',
|
|
205
|
+
rollout: 1,
|
|
206
|
+
};
|
|
207
|
+
mockFileExistsAtPath.mockResolvedValue(true);
|
|
208
|
+
mockIsDirectory.mockResolvedValue(false);
|
|
209
|
+
// Mock utility functions
|
|
210
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
211
|
+
const mockBuffer = await import('../../../utils/buffer.js');
|
|
212
|
+
const mockPrivateKey = await import('../../../utils/private-key.js');
|
|
213
|
+
const mockHash = await import('../../../utils/hash.js');
|
|
214
|
+
const mockSignature = await import('../../../utils/signature.js');
|
|
215
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
|
|
216
|
+
vi.mocked(mockBuffer.createBufferFromPath).mockResolvedValue(testBuffer);
|
|
217
|
+
vi.mocked(mockBuffer.createBufferFromString).mockReturnValue(testBuffer);
|
|
218
|
+
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(true);
|
|
219
|
+
vi.mocked(mockPrivateKey.formatPrivateKey).mockReturnValue('formatted-private-key');
|
|
220
|
+
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
|
|
221
|
+
vi.mocked(mockSignature.createSignature).mockResolvedValue(testSignature);
|
|
222
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
223
|
+
.post(`/v1/apps/${appId}/bundles`, {
|
|
224
|
+
appId,
|
|
225
|
+
url: bundleUrl,
|
|
226
|
+
checksum: testHash,
|
|
227
|
+
signature: testSignature,
|
|
228
|
+
artifactType: 'zip',
|
|
229
|
+
rolloutPercentage: 1,
|
|
230
|
+
})
|
|
231
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
232
|
+
.reply(201, { id: bundleId });
|
|
233
|
+
await createBundleCommand.action(options, undefined);
|
|
234
|
+
expect(scope.isDone()).toBe(true);
|
|
235
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Bundle successfully created.');
|
|
236
|
+
});
|
|
237
|
+
it('should handle private key file not found', async () => {
|
|
238
|
+
const appId = 'app-123';
|
|
239
|
+
const privateKeyPath = 'nonexistent-key.pem';
|
|
240
|
+
const options = {
|
|
241
|
+
appId,
|
|
242
|
+
path: './dist',
|
|
243
|
+
privateKey: privateKeyPath,
|
|
244
|
+
artifactType: 'zip',
|
|
245
|
+
rollout: 1,
|
|
246
|
+
};
|
|
247
|
+
mockFileExistsAtPath.mockImplementation((path) => {
|
|
248
|
+
if (path === privateKeyPath)
|
|
249
|
+
return Promise.resolve(false);
|
|
250
|
+
return Promise.resolve(true);
|
|
251
|
+
});
|
|
252
|
+
// Mock utility functions
|
|
253
|
+
const mockBuffer = await import('../../../utils/buffer.js');
|
|
254
|
+
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
|
|
255
|
+
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
256
|
+
expect(mockConsola.error).toHaveBeenCalledWith('Private key file not found.');
|
|
257
|
+
});
|
|
258
|
+
it('should handle invalid private key format', async () => {
|
|
259
|
+
const appId = 'app-123';
|
|
260
|
+
const invalidPrivateKey = 'not-a-valid-key';
|
|
261
|
+
const options = {
|
|
262
|
+
appId,
|
|
263
|
+
path: './dist',
|
|
264
|
+
privateKey: invalidPrivateKey,
|
|
265
|
+
artifactType: 'zip',
|
|
266
|
+
rollout: 1,
|
|
267
|
+
};
|
|
268
|
+
mockFileExistsAtPath.mockResolvedValue(true);
|
|
269
|
+
mockIsDirectory.mockResolvedValue(false);
|
|
270
|
+
// Mock utility functions
|
|
271
|
+
const mockBuffer = await import('../../../utils/buffer.js');
|
|
272
|
+
vi.mocked(mockBuffer.isPrivateKeyContent).mockReturnValue(false);
|
|
273
|
+
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
274
|
+
expect(mockConsola.error).toHaveBeenCalledWith('Private key must be either a path to a .pem file or the private key content as plain text.');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
2
|
-
import consola from 'consola';
|
|
3
|
-
import { z } from 'zod';
|
|
4
1
|
import appBundlesService from '../../../services/app-bundles.js';
|
|
5
2
|
import appsService from '../../../services/apps.js';
|
|
3
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
6
4
|
import organizationsService from '../../../services/organizations.js';
|
|
7
|
-
import { getMessageFromUnknownError } from '../../../utils/error.js';
|
|
8
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
9
|
export default defineCommand({
|
|
10
10
|
description: 'Delete an app bundle.',
|
|
11
11
|
options: defineOptions(z.object({
|
|
@@ -14,6 +14,10 @@ export default defineCommand({
|
|
|
14
14
|
})),
|
|
15
15
|
action: async (options, args) => {
|
|
16
16
|
let { appId, bundleId } = options;
|
|
17
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
18
|
+
consola.error('You must be logged in to run this command.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
17
21
|
// Prompt for missing arguments
|
|
18
22
|
if (!appId) {
|
|
19
23
|
const organizations = await organizationsService.findAll();
|
|
@@ -56,17 +60,10 @@ export default defineCommand({
|
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
62
|
// Delete bundle
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
consola.success('Bundle deleted successfully.');
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
const message = getMessageFromUnknownError(error);
|
|
68
|
-
consola.error(message);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
63
|
+
await appBundlesService.delete({
|
|
64
|
+
appId,
|
|
65
|
+
appBundleId: bundleId,
|
|
66
|
+
});
|
|
67
|
+
consola.success('Bundle deleted successfully.');
|
|
71
68
|
},
|
|
72
69
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
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 deleteBundleCommand 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
|
+
describe('apps-bundles-delete', () => {
|
|
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(() => undefined);
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
nock.cleanAll();
|
|
28
|
+
vi.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
it('should delete bundle with provided appId and bundleId after confirmation', async () => {
|
|
31
|
+
const appId = 'app-123';
|
|
32
|
+
const bundleId = 'bundle-456';
|
|
33
|
+
const testToken = 'test-token';
|
|
34
|
+
const options = { appId, bundleId };
|
|
35
|
+
mockPrompt.mockResolvedValueOnce(true); // confirmation
|
|
36
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
37
|
+
.delete(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
38
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
39
|
+
.reply(200);
|
|
40
|
+
await deleteBundleCommand.action(options, undefined);
|
|
41
|
+
expect(scope.isDone()).toBe(true);
|
|
42
|
+
expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to delete this bundle?', {
|
|
43
|
+
type: 'confirm',
|
|
44
|
+
});
|
|
45
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Bundle deleted successfully.');
|
|
46
|
+
});
|
|
47
|
+
it('should not delete bundle when confirmation is declined', async () => {
|
|
48
|
+
const appId = 'app-123';
|
|
49
|
+
const bundleId = 'bundle-456';
|
|
50
|
+
const options = { appId, bundleId };
|
|
51
|
+
mockPrompt.mockResolvedValueOnce(false); // declined confirmation
|
|
52
|
+
await deleteBundleCommand.action(options, undefined);
|
|
53
|
+
expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to delete this bundle?', {
|
|
54
|
+
type: 'confirm',
|
|
55
|
+
});
|
|
56
|
+
expect(mockConsola.success).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
it('should prompt for app selection when appId not provided', async () => {
|
|
59
|
+
const orgId = 'org-1';
|
|
60
|
+
const appId = 'app-1';
|
|
61
|
+
const bundleId = 'bundle-456';
|
|
62
|
+
const testToken = 'test-token';
|
|
63
|
+
const organization = { id: orgId, name: 'Org 1' };
|
|
64
|
+
const app = { id: appId, name: 'App 1' };
|
|
65
|
+
const options = { bundleId };
|
|
66
|
+
const orgsScope = nock(DEFAULT_API_BASE_URL)
|
|
67
|
+
.get('/v1/organizations')
|
|
68
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
69
|
+
.reply(200, [organization]);
|
|
70
|
+
const appsScope = nock(DEFAULT_API_BASE_URL)
|
|
71
|
+
.get('/v1/apps')
|
|
72
|
+
.query({ organizationId: orgId })
|
|
73
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
74
|
+
.reply(200, [app]);
|
|
75
|
+
const deleteScope = nock(DEFAULT_API_BASE_URL)
|
|
76
|
+
.delete(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
77
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
78
|
+
.reply(200);
|
|
79
|
+
mockPrompt
|
|
80
|
+
.mockResolvedValueOnce(orgId) // organization selection
|
|
81
|
+
.mockResolvedValueOnce(appId) // app selection
|
|
82
|
+
.mockResolvedValueOnce(true); // confirmation
|
|
83
|
+
await deleteBundleCommand.action(options, undefined);
|
|
84
|
+
expect(orgsScope.isDone()).toBe(true);
|
|
85
|
+
expect(appsScope.isDone()).toBe(true);
|
|
86
|
+
expect(deleteScope.isDone()).toBe(true);
|
|
87
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Bundle deleted successfully.');
|
|
88
|
+
});
|
|
89
|
+
it('should prompt for bundleId when not provided', async () => {
|
|
90
|
+
const appId = 'app-123';
|
|
91
|
+
const bundleId = 'bundle-456';
|
|
92
|
+
const testToken = 'test-token';
|
|
93
|
+
const options = { appId };
|
|
94
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
95
|
+
.delete(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
96
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
97
|
+
.reply(200);
|
|
98
|
+
mockPrompt
|
|
99
|
+
.mockResolvedValueOnce(bundleId) // bundle ID input
|
|
100
|
+
.mockResolvedValueOnce(true); // confirmation
|
|
101
|
+
await deleteBundleCommand.action(options, undefined);
|
|
102
|
+
expect(scope.isDone()).toBe(true);
|
|
103
|
+
expect(mockPrompt).toHaveBeenCalledWith('Enter the bundle ID:', { type: 'text' });
|
|
104
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Bundle deleted successfully.');
|
|
105
|
+
});
|
|
106
|
+
it('should handle error when no organizations exist', async () => {
|
|
107
|
+
const testToken = 'test-token';
|
|
108
|
+
const options = {};
|
|
109
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
110
|
+
.get('/v1/organizations')
|
|
111
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
112
|
+
.reply(200, []);
|
|
113
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
114
|
+
throw new Error(`process.exit called with code ${code}`);
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
await deleteBundleCommand.action(options, undefined);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
expect(error.message).toBe('process.exit called with code 1');
|
|
121
|
+
}
|
|
122
|
+
expect(scope.isDone()).toBe(true);
|
|
123
|
+
expect(mockConsola.error).toHaveBeenCalledWith('You must create an organization before deleting a bundle.');
|
|
124
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
125
|
+
});
|
|
126
|
+
it('should handle API error during deletion', async () => {
|
|
127
|
+
const appId = 'app-123';
|
|
128
|
+
const bundleId = 'bundle-456';
|
|
129
|
+
const testToken = 'test-token';
|
|
130
|
+
const options = { appId, bundleId };
|
|
131
|
+
mockPrompt.mockResolvedValueOnce(true);
|
|
132
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
133
|
+
.delete(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
134
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
135
|
+
.reply(404, { message: 'Bundle not found' });
|
|
136
|
+
await expect(deleteBundleCommand.action(options, undefined)).rejects.toThrow();
|
|
137
|
+
expect(scope.isDone()).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
2
|
-
import consola from 'consola';
|
|
3
|
-
import { z } from 'zod';
|
|
4
1
|
import appBundlesService from '../../../services/app-bundles.js';
|
|
5
2
|
import appsService from '../../../services/apps.js';
|
|
6
3
|
import authorizationService from '../../../services/authorization-service.js';
|
|
7
4
|
import organizationsService from '../../../services/organizations.js';
|
|
8
|
-
import { getMessageFromUnknownError } from '../../../utils/error.js';
|
|
9
5
|
import { prompt } from '../../../utils/prompt.js';
|
|
6
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
7
|
+
import consola from 'consola';
|
|
8
|
+
import { z } from 'zod';
|
|
10
9
|
export default defineCommand({
|
|
11
10
|
description: 'Update an app bundle.',
|
|
12
11
|
options: defineOptions(z.object({
|
|
@@ -78,22 +77,15 @@ export default defineCommand({
|
|
|
78
77
|
});
|
|
79
78
|
}
|
|
80
79
|
// Update bundle
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
consola.success('Bundle updated successfully.');
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
const message = getMessageFromUnknownError(error);
|
|
95
|
-
consola.error(message);
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
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.');
|
|
98
90
|
},
|
|
99
91
|
});
|