@capawesome/cli 4.6.0-dev.0108a83.1774286472 → 4.6.0-dev.80c962a.1774597304
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/dist/commands/apps/builds/create.js +25 -45
- package/dist/commands/apps/devices/forcechannel.js +9 -7
- package/dist/commands/apps/devices/unforcechannel.js +9 -7
- package/dist/services/app-devices.js +8 -0
- package/dist/types/index.js +0 -1
- package/dist/utils/zip.js +2 -19
- package/package.json +1 -2
- package/dist/services/app-build-sources.js +0 -112
- package/dist/types/app-build-source.js +0 -1
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { DEFAULT_CONSOLE_BASE_URL } from '../../../config/consts.js';
|
|
2
|
-
import appBuildSourcesService from '../../../services/app-build-sources.js';
|
|
3
2
|
import appBuildsService from '../../../services/app-builds.js';
|
|
4
3
|
import appCertificatesService from '../../../services/app-certificates.js';
|
|
5
4
|
import appEnvironmentsService from '../../../services/app-environments.js';
|
|
6
5
|
import { unescapeAnsi } from '../../../utils/ansi.js';
|
|
6
|
+
import { parseKeyValuePairs } from '../../../utils/app-environments.js';
|
|
7
7
|
import { withAuth } from '../../../utils/auth.js';
|
|
8
8
|
import { isInteractive } from '../../../utils/environment.js';
|
|
9
9
|
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
10
10
|
import { wait } from '../../../utils/wait.js';
|
|
11
|
-
import zip from '../../../utils/zip.js';
|
|
12
11
|
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
13
12
|
import consola from 'consola';
|
|
14
13
|
import fs from 'fs/promises';
|
|
@@ -47,7 +46,6 @@ export default defineCommand({
|
|
|
47
46
|
.optional()
|
|
48
47
|
.describe('Download the generated IPA file (iOS only). Optionally provide a file path.'),
|
|
49
48
|
json: z.boolean().optional().describe('Output in JSON format.'),
|
|
50
|
-
path: z.string().optional().describe('Path to local source files to upload.'),
|
|
51
49
|
platform: z
|
|
52
50
|
.enum(['ios', 'android', 'web'], {
|
|
53
51
|
message: 'Platform must be either `ios`, `android`, or `web`.',
|
|
@@ -64,6 +62,14 @@ export default defineCommand({
|
|
|
64
62
|
.string()
|
|
65
63
|
.optional()
|
|
66
64
|
.describe('The type of build. For iOS, supported values are `simulator`, `development`, `ad-hoc`, `app-store`, and `enterprise`. For Android, supported values are `debug` and `release`. For Web, no type is required.'),
|
|
65
|
+
variable: z
|
|
66
|
+
.array(z.string())
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('Ad hoc environment variable in key=value format. Can be specified multiple times.'),
|
|
69
|
+
variableFile: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('Path to a file containing ad hoc environment variables in .env format.'),
|
|
67
73
|
zip: z
|
|
68
74
|
.union([z.boolean(), z.string()])
|
|
69
75
|
.optional()
|
|
@@ -71,7 +77,7 @@ export default defineCommand({
|
|
|
71
77
|
yes: z.boolean().optional().describe('Skip confirmation prompts.'),
|
|
72
78
|
}), { y: 'yes' }),
|
|
73
79
|
action: withAuth(async (options) => {
|
|
74
|
-
let { appId, platform, type, gitRef, environment, certificate, json, stack
|
|
80
|
+
let { appId, platform, type, gitRef, environment, certificate, json, stack } = options;
|
|
75
81
|
// Validate that detached flag cannot be used with artifact flags
|
|
76
82
|
if (options.detached && (options.apk || options.aab || options.ipa || options.zip)) {
|
|
77
83
|
consola.error('The --detached flag cannot be used with --apk, --aab, --ipa, or --zip flags.');
|
|
@@ -87,26 +93,6 @@ export default defineCommand({
|
|
|
87
93
|
consola.error('The --channel and --destination flags cannot be used together.');
|
|
88
94
|
process.exit(1);
|
|
89
95
|
}
|
|
90
|
-
// Validate that path and gitRef cannot be used together
|
|
91
|
-
if (sourcePath && gitRef) {
|
|
92
|
-
consola.error('The --path and --git-ref flags cannot be used together.');
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
// Validate path if provided
|
|
96
|
-
if (sourcePath) {
|
|
97
|
-
const resolvedPath = path.resolve(sourcePath);
|
|
98
|
-
const stat = await fs.stat(resolvedPath).catch(() => null);
|
|
99
|
-
if (!stat || !stat.isDirectory()) {
|
|
100
|
-
consola.error('The --path must point to an existing directory.');
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
const packageJsonPath = path.join(resolvedPath, 'package.json');
|
|
104
|
-
const packageJsonStat = await fs.stat(packageJsonPath).catch(() => null);
|
|
105
|
-
if (!packageJsonStat || !packageJsonStat.isFile()) {
|
|
106
|
-
consola.error('The directory specified by --path must contain a package.json file.');
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
96
|
// Prompt for app ID if not provided
|
|
111
97
|
if (!appId) {
|
|
112
98
|
if (!isInteractive()) {
|
|
@@ -136,10 +122,10 @@ export default defineCommand({
|
|
|
136
122
|
process.exit(1);
|
|
137
123
|
}
|
|
138
124
|
}
|
|
139
|
-
// Prompt for git ref if not provided
|
|
140
|
-
if (!
|
|
125
|
+
// Prompt for git ref if not provided
|
|
126
|
+
if (!gitRef) {
|
|
141
127
|
if (!isInteractive()) {
|
|
142
|
-
consola.error('You must provide a git ref
|
|
128
|
+
consola.error('You must provide a git ref when running in non-interactive environment.');
|
|
143
129
|
process.exit(1);
|
|
144
130
|
}
|
|
145
131
|
gitRef = await prompt('Enter the Git reference (branch, tag, or commit SHA):', {
|
|
@@ -220,28 +206,22 @@ export default defineCommand({
|
|
|
220
206
|
}
|
|
221
207
|
}
|
|
222
208
|
}
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
buffer,
|
|
234
|
-
name: 'source.zip',
|
|
235
|
-
}, (currentPart, totalParts) => {
|
|
236
|
-
consola.start(`Uploading source files (${currentPart}/${totalParts})...`);
|
|
237
|
-
});
|
|
238
|
-
appBuildSourceId = appBuildSource.id;
|
|
239
|
-
consola.success('Source files uploaded successfully.');
|
|
209
|
+
// Parse ad hoc environment variables from inline and file
|
|
210
|
+
const variablesMap = new Map();
|
|
211
|
+
if (options.variableFile) {
|
|
212
|
+
const fileContent = await fs.readFile(options.variableFile, 'utf-8');
|
|
213
|
+
const fileVariables = parseKeyValuePairs(fileContent);
|
|
214
|
+
fileVariables.forEach((v) => variablesMap.set(v.key, v.value));
|
|
215
|
+
}
|
|
216
|
+
if (options.variable) {
|
|
217
|
+
const inlineVariables = parseKeyValuePairs(options.variable.join('\n'));
|
|
218
|
+
inlineVariables.forEach((v) => variablesMap.set(v.key, v.value));
|
|
240
219
|
}
|
|
220
|
+
const adHocEnvironmentVariables = variablesMap.size > 0 ? Object.fromEntries(variablesMap) : undefined;
|
|
241
221
|
// Create the app build
|
|
242
222
|
consola.start('Creating build...');
|
|
243
223
|
const response = await appBuildsService.create({
|
|
244
|
-
|
|
224
|
+
adHocEnvironmentVariables,
|
|
245
225
|
appCertificateName: certificate,
|
|
246
226
|
appEnvironmentName: environment,
|
|
247
227
|
appId,
|
|
@@ -10,11 +10,11 @@ export default defineCommand({
|
|
|
10
10
|
description: 'Force a device to use a specific channel.',
|
|
11
11
|
options: defineOptions(z.object({
|
|
12
12
|
appId: z.string().uuid({ message: 'App ID must be a UUID.' }).optional().describe('ID of the app.'),
|
|
13
|
-
deviceId: z.string().optional().describe('ID of the device.'),
|
|
13
|
+
deviceId: z.array(z.string()).optional().describe('ID of the device. Can be specified multiple times.'),
|
|
14
14
|
channel: z.string().optional().describe('Name of the channel to force.'),
|
|
15
15
|
})),
|
|
16
16
|
action: withAuth(async (options, args) => {
|
|
17
|
-
let { appId, deviceId, channel } = options;
|
|
17
|
+
let { appId, deviceId: deviceIds, channel } = options;
|
|
18
18
|
if (!appId) {
|
|
19
19
|
if (!isInteractive()) {
|
|
20
20
|
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
@@ -23,14 +23,15 @@ export default defineCommand({
|
|
|
23
23
|
const organizationId = await promptOrganizationSelection();
|
|
24
24
|
appId = await promptAppSelection(organizationId);
|
|
25
25
|
}
|
|
26
|
-
if (!
|
|
26
|
+
if (!deviceIds || deviceIds.length === 0) {
|
|
27
27
|
if (!isInteractive()) {
|
|
28
28
|
consola.error('You must provide the device ID when running in non-interactive environment.');
|
|
29
29
|
process.exit(1);
|
|
30
30
|
}
|
|
31
|
-
deviceId = await prompt('Enter the device ID:', {
|
|
31
|
+
const deviceId = await prompt('Enter the device ID:', {
|
|
32
32
|
type: 'text',
|
|
33
33
|
});
|
|
34
|
+
deviceIds = [deviceId];
|
|
34
35
|
}
|
|
35
36
|
if (!channel) {
|
|
36
37
|
if (!isInteractive()) {
|
|
@@ -56,11 +57,12 @@ export default defineCommand({
|
|
|
56
57
|
consola.error('Channel ID not found.');
|
|
57
58
|
process.exit(1);
|
|
58
59
|
}
|
|
59
|
-
await appDevicesService.
|
|
60
|
+
await appDevicesService.updateMany({
|
|
60
61
|
appId,
|
|
61
|
-
|
|
62
|
+
deviceIds,
|
|
62
63
|
forcedAppChannelId: channelId,
|
|
63
64
|
});
|
|
64
|
-
|
|
65
|
+
const deviceCount = deviceIds.length;
|
|
66
|
+
consola.success(`${deviceCount === 1 ? 'Device' : `${deviceCount} devices`} forced to channel successfully.`);
|
|
65
67
|
}),
|
|
66
68
|
});
|
|
@@ -9,10 +9,10 @@ export default defineCommand({
|
|
|
9
9
|
description: 'Remove the forced channel from a device.',
|
|
10
10
|
options: defineOptions(z.object({
|
|
11
11
|
appId: z.string().uuid({ message: 'App ID must be a UUID.' }).optional().describe('ID of the app.'),
|
|
12
|
-
deviceId: z.string().optional().describe('ID of the device.'),
|
|
12
|
+
deviceId: z.array(z.string()).optional().describe('ID of the device. Can be specified multiple times.'),
|
|
13
13
|
})),
|
|
14
14
|
action: withAuth(async (options, args) => {
|
|
15
|
-
let { appId, deviceId } = options;
|
|
15
|
+
let { appId, deviceId: deviceIds } = options;
|
|
16
16
|
if (!appId) {
|
|
17
17
|
if (!isInteractive()) {
|
|
18
18
|
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
@@ -21,20 +21,22 @@ export default defineCommand({
|
|
|
21
21
|
const organizationId = await promptOrganizationSelection();
|
|
22
22
|
appId = await promptAppSelection(organizationId);
|
|
23
23
|
}
|
|
24
|
-
if (!
|
|
24
|
+
if (!deviceIds || deviceIds.length === 0) {
|
|
25
25
|
if (!isInteractive()) {
|
|
26
26
|
consola.error('You must provide the device ID when running in non-interactive environment.');
|
|
27
27
|
process.exit(1);
|
|
28
28
|
}
|
|
29
|
-
deviceId = await prompt('Enter the device ID:', {
|
|
29
|
+
const deviceId = await prompt('Enter the device ID:', {
|
|
30
30
|
type: 'text',
|
|
31
31
|
});
|
|
32
|
+
deviceIds = [deviceId];
|
|
32
33
|
}
|
|
33
|
-
await appDevicesService.
|
|
34
|
+
await appDevicesService.updateMany({
|
|
34
35
|
appId,
|
|
35
|
-
|
|
36
|
+
deviceIds,
|
|
36
37
|
forcedAppChannelId: null,
|
|
37
38
|
});
|
|
38
|
-
|
|
39
|
+
const deviceCount = deviceIds.length;
|
|
40
|
+
consola.success(`Forced channel removed from ${deviceCount === 1 ? 'device' : `${deviceCount} devices`} successfully.`);
|
|
39
41
|
}),
|
|
40
42
|
});
|
|
@@ -55,6 +55,14 @@ class AppDevicesServiceImpl {
|
|
|
55
55
|
},
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
+
async updateMany(data) {
|
|
59
|
+
const ids = data.deviceIds.join(',');
|
|
60
|
+
await this.httpClient.patch(`/v1/apps/${data.appId}/devices?ids=${ids}`, { forcedAppChannelId: data.forcedAppChannelId }, {
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
58
66
|
}
|
|
59
67
|
const appDevicesService = new AppDevicesServiceImpl(httpClient);
|
|
60
68
|
export default appDevicesService;
|
package/dist/types/index.js
CHANGED
package/dist/utils/zip.js
CHANGED
|
@@ -1,29 +1,12 @@
|
|
|
1
1
|
import AdmZip from 'adm-zip';
|
|
2
|
-
import { globby } from 'globby';
|
|
3
|
-
import path from 'path';
|
|
4
2
|
class ZipImpl {
|
|
5
3
|
async zipFolder(sourceFolder) {
|
|
6
4
|
const zip = new AdmZip();
|
|
7
5
|
zip.addLocalFolder(sourceFolder);
|
|
8
6
|
return zip.toBuffer();
|
|
9
7
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
cwd: sourceFolder,
|
|
13
|
-
gitignore: true,
|
|
14
|
-
ignore: ['.git/**'],
|
|
15
|
-
dot: true,
|
|
16
|
-
});
|
|
17
|
-
const zip = new AdmZip();
|
|
18
|
-
for (const file of files) {
|
|
19
|
-
const filePath = path.join(sourceFolder, file);
|
|
20
|
-
const dirName = path.dirname(file);
|
|
21
|
-
zip.addLocalFile(filePath, dirName === '.' ? '' : dirName);
|
|
22
|
-
}
|
|
23
|
-
return zip.toBuffer();
|
|
24
|
-
}
|
|
25
|
-
isZipped(filePath) {
|
|
26
|
-
return filePath.endsWith('.zip');
|
|
8
|
+
isZipped(path) {
|
|
9
|
+
return path.endsWith('.zip');
|
|
27
10
|
}
|
|
28
11
|
}
|
|
29
12
|
const zip = new ZipImpl();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capawesome/cli",
|
|
3
|
-
"version": "4.6.0-dev.
|
|
3
|
+
"version": "4.6.0-dev.80c962a.1774597304",
|
|
4
4
|
"description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
"c12": "3.3.3",
|
|
62
62
|
"consola": "3.3.0",
|
|
63
63
|
"form-data": "4.0.4",
|
|
64
|
-
"globby": "16.1.1",
|
|
65
64
|
"http-proxy-agent": "7.0.2",
|
|
66
65
|
"https-proxy-agent": "7.0.6",
|
|
67
66
|
"mime": "4.0.7",
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { MAX_CONCURRENT_PART_UPLOADS } from '../config/index.js';
|
|
2
|
-
import authorizationService from '../services/authorization-service.js';
|
|
3
|
-
import httpClient from '../utils/http-client.js';
|
|
4
|
-
import FormData from 'form-data';
|
|
5
|
-
class AppBuildSourcesServiceImpl {
|
|
6
|
-
httpClient;
|
|
7
|
-
constructor(httpClient) {
|
|
8
|
-
this.httpClient = httpClient;
|
|
9
|
-
}
|
|
10
|
-
async create(dto, onProgress) {
|
|
11
|
-
const response = await this.httpClient.post(`/v1/apps/${dto.appId}/build-sources`, { fileSizeInBytes: dto.fileSizeInBytes }, {
|
|
12
|
-
headers: {
|
|
13
|
-
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
await this.upload({
|
|
17
|
-
appBuildSourceId: response.data.id,
|
|
18
|
-
appId: dto.appId,
|
|
19
|
-
buffer: dto.buffer,
|
|
20
|
-
name: dto.name,
|
|
21
|
-
}, onProgress);
|
|
22
|
-
return response.data;
|
|
23
|
-
}
|
|
24
|
-
async completeUpload(dto) {
|
|
25
|
-
return this.httpClient
|
|
26
|
-
.post(`/v1/apps/${dto.appId}/build-sources/${dto.appBuildSourceId}/upload?action=mpu-complete&uploadId=${dto.uploadId}`, {
|
|
27
|
-
parts: dto.parts,
|
|
28
|
-
}, {
|
|
29
|
-
headers: {
|
|
30
|
-
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
31
|
-
},
|
|
32
|
-
})
|
|
33
|
-
.then((response) => response.data);
|
|
34
|
-
}
|
|
35
|
-
async createUpload(dto) {
|
|
36
|
-
const response = await this.httpClient.post(`/v1/apps/${dto.appId}/build-sources/${dto.appBuildSourceId}/upload?action=mpu-create`, {}, {
|
|
37
|
-
headers: {
|
|
38
|
-
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
return response.data;
|
|
42
|
-
}
|
|
43
|
-
async createUploadPart(dto) {
|
|
44
|
-
const formData = new FormData();
|
|
45
|
-
formData.append('blob', dto.buffer, { filename: dto.name });
|
|
46
|
-
formData.append('partNumber', dto.partNumber.toString());
|
|
47
|
-
return this.httpClient
|
|
48
|
-
.put(`/v1/apps/${dto.appId}/build-sources/${dto.appBuildSourceId}/upload?action=mpu-uploadpart&uploadId=${dto.uploadId}`, formData, {
|
|
49
|
-
headers: {
|
|
50
|
-
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
51
|
-
...formData.getHeaders(),
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
.then((response) => response.data);
|
|
55
|
-
}
|
|
56
|
-
async createUploadParts(dto, onProgress) {
|
|
57
|
-
const uploadedParts = [];
|
|
58
|
-
const partSize = 10 * 1024 * 1024; // 10 MB
|
|
59
|
-
const totalParts = Math.ceil(dto.buffer.byteLength / partSize);
|
|
60
|
-
let partNumber = 0;
|
|
61
|
-
const uploadNextPart = async () => {
|
|
62
|
-
if (partNumber >= totalParts) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
partNumber++;
|
|
66
|
-
onProgress?.(partNumber, totalParts);
|
|
67
|
-
const start = (partNumber - 1) * partSize;
|
|
68
|
-
const end = Math.min(start + partSize, dto.buffer.byteLength);
|
|
69
|
-
const partBuffer = dto.buffer.subarray(start, end);
|
|
70
|
-
const uploadedPart = await this.createUploadPart({
|
|
71
|
-
appBuildSourceId: dto.appBuildSourceId,
|
|
72
|
-
appId: dto.appId,
|
|
73
|
-
buffer: partBuffer,
|
|
74
|
-
name: dto.name,
|
|
75
|
-
partNumber,
|
|
76
|
-
uploadId: dto.uploadId,
|
|
77
|
-
});
|
|
78
|
-
uploadedParts.push(uploadedPart);
|
|
79
|
-
await uploadNextPart();
|
|
80
|
-
};
|
|
81
|
-
const uploadPartPromises = Array.from({ length: MAX_CONCURRENT_PART_UPLOADS });
|
|
82
|
-
for (let i = 0; i < MAX_CONCURRENT_PART_UPLOADS; i++) {
|
|
83
|
-
uploadPartPromises[i] = uploadNextPart();
|
|
84
|
-
}
|
|
85
|
-
await Promise.all(uploadPartPromises);
|
|
86
|
-
return uploadedParts.sort((a, b) => a.partNumber - b.partNumber);
|
|
87
|
-
}
|
|
88
|
-
async upload(dto, onProgress) {
|
|
89
|
-
// 1. Create a multipart upload
|
|
90
|
-
const { uploadId } = await this.createUpload({
|
|
91
|
-
appBuildSourceId: dto.appBuildSourceId,
|
|
92
|
-
appId: dto.appId,
|
|
93
|
-
});
|
|
94
|
-
// 2. Upload the file in parts
|
|
95
|
-
const parts = await this.createUploadParts({
|
|
96
|
-
appBuildSourceId: dto.appBuildSourceId,
|
|
97
|
-
appId: dto.appId,
|
|
98
|
-
buffer: dto.buffer,
|
|
99
|
-
name: dto.name,
|
|
100
|
-
uploadId,
|
|
101
|
-
}, onProgress);
|
|
102
|
-
// 3. Complete the upload
|
|
103
|
-
await this.completeUpload({
|
|
104
|
-
appBuildSourceId: dto.appBuildSourceId,
|
|
105
|
-
appId: dto.appId,
|
|
106
|
-
parts,
|
|
107
|
-
uploadId,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const appBuildSourcesService = new AppBuildSourcesServiceImpl(httpClient);
|
|
112
|
-
export default appBuildSourcesService;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|