@capawesome/cli 4.17.2 → 4.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/commands/apps/automations/create.js +141 -0
  3. package/dist/commands/apps/automations/create.test.js +162 -0
  4. package/dist/commands/apps/automations/delete.js +66 -0
  5. package/dist/commands/apps/automations/delete.test.js +127 -0
  6. package/dist/commands/apps/automations/get.js +62 -0
  7. package/dist/commands/apps/automations/get.test.js +118 -0
  8. package/dist/commands/apps/automations/list.js +46 -0
  9. package/dist/commands/apps/automations/list.test.js +92 -0
  10. package/dist/commands/apps/automations/update.js +118 -0
  11. package/dist/commands/apps/automations/update.test.js +124 -0
  12. package/dist/commands/apps/builds/create.js +31 -1
  13. package/dist/commands/apps/builds/create.test.js +15 -0
  14. package/dist/commands/apps/builds/run.js +265 -0
  15. package/dist/commands/apps/configurations/create.js +51 -0
  16. package/dist/commands/apps/configurations/create.test.js +120 -0
  17. package/dist/commands/apps/configurations/delete.js +61 -0
  18. package/dist/commands/apps/configurations/delete.test.js +112 -0
  19. package/dist/commands/apps/configurations/get.js +65 -0
  20. package/dist/commands/apps/configurations/get.test.js +119 -0
  21. package/dist/commands/apps/configurations/list.js +39 -0
  22. package/dist/commands/apps/configurations/list.test.js +94 -0
  23. package/dist/commands/apps/configurations/update.js +61 -0
  24. package/dist/commands/apps/configurations/update.test.js +122 -0
  25. package/dist/index.js +11 -0
  26. package/dist/services/app-automations.js +64 -0
  27. package/dist/services/app-configurations.js +77 -0
  28. package/dist/types/app-automation.js +1 -0
  29. package/dist/types/app-configuration.js +1 -0
  30. package/dist/types/index.js +1 -0
  31. package/dist/utils/android-emulator.js +170 -0
  32. package/dist/utils/ios-simulator.js +57 -0
  33. package/dist/utils/zip.js +4 -0
  34. package/package.json +2 -2
@@ -0,0 +1,77 @@
1
+ import authorizationService from '../services/authorization-service.js';
2
+ import httpClient from '../utils/http-client.js';
3
+ class AppConfigurationsServiceImpl {
4
+ httpClient;
5
+ constructor(httpClient) {
6
+ this.httpClient = httpClient;
7
+ }
8
+ async create(dto) {
9
+ const { appId, ...bodyData } = dto;
10
+ const response = await this.httpClient.post(`/v1/apps/${appId}/configurations`, bodyData, {
11
+ headers: {
12
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
13
+ },
14
+ });
15
+ return response.data;
16
+ }
17
+ async delete(dto) {
18
+ if (dto.id) {
19
+ await this.httpClient.delete(`/v1/apps/${dto.appId}/configurations/${dto.id}`, {
20
+ headers: {
21
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
22
+ },
23
+ });
24
+ }
25
+ else if (dto.name) {
26
+ await this.httpClient.delete(`/v1/apps/${dto.appId}/configurations`, {
27
+ headers: {
28
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
29
+ },
30
+ params: {
31
+ name: dto.name,
32
+ },
33
+ });
34
+ }
35
+ }
36
+ async findAll(dto) {
37
+ const params = {};
38
+ if (dto.limit !== undefined) {
39
+ params.limit = dto.limit.toString();
40
+ }
41
+ if (dto.name) {
42
+ params.name = dto.name;
43
+ }
44
+ if (dto.offset !== undefined) {
45
+ params.offset = dto.offset.toString();
46
+ }
47
+ if (dto.query) {
48
+ params.query = dto.query;
49
+ }
50
+ const response = await this.httpClient.get(`/v1/apps/${dto.appId}/configurations`, {
51
+ headers: {
52
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
53
+ },
54
+ params,
55
+ });
56
+ return response.data;
57
+ }
58
+ async findOneById(dto) {
59
+ const response = await this.httpClient.get(`/v1/apps/${dto.appId}/configurations/${dto.id}`, {
60
+ headers: {
61
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
62
+ },
63
+ });
64
+ return response.data;
65
+ }
66
+ async update(dto) {
67
+ const { appId, configurationId, ...bodyData } = dto;
68
+ const response = await this.httpClient.patch(`/v1/apps/${appId}/configurations/${configurationId}`, bodyData, {
69
+ headers: {
70
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
71
+ },
72
+ });
73
+ return response.data;
74
+ }
75
+ }
76
+ const appConfigurationsService = new AppConfigurationsServiceImpl(httpClient);
77
+ export default appConfigurationsService;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
1
  export * from './app-apple-api-key.js';
2
+ export * from './app-automation.js';
2
3
  export * from './app-build-source.js';
3
4
  export * from './app-bundle.js';
4
5
  export * from './app-certificate.js';
@@ -0,0 +1,170 @@
1
+ import { getCodeFromUnknownError, UserError } from '../utils/error.js';
2
+ import { wait } from '../utils/wait.js';
3
+ import { execFileSync, spawn } from 'child_process';
4
+ import fs from 'fs';
5
+ import os from 'os';
6
+ import path from 'path';
7
+ const BOOT_POLL_INTERVAL_IN_MS = 2000;
8
+ const BOOT_TIMEOUT_IN_MS = 300000;
9
+ /**
10
+ * Find all Android emulators (AVDs) installed on this machine.
11
+ */
12
+ export const findAllAndroidEmulators = () => {
13
+ const ids = runEmulator(['-list-avds'])
14
+ .split('\n')
15
+ .map((line) => line.trim())
16
+ .filter(Boolean);
17
+ const serialById = findAllRunningAndroidEmulators();
18
+ return ids.map((id) => {
19
+ const serial = serialById.get(id) ?? null;
20
+ const { displayName, sdkVersion } = getAndroidEmulatorConfig(id);
21
+ return {
22
+ id,
23
+ name: displayName ?? id,
24
+ running: !!serial,
25
+ sdkVersion,
26
+ serial,
27
+ };
28
+ });
29
+ };
30
+ /**
31
+ * Boot an Android emulator and wait until it has finished booting.
32
+ *
33
+ * Returns the serial of the running emulator.
34
+ */
35
+ export const bootAndroidEmulator = async (emulator) => {
36
+ if (emulator.serial) {
37
+ return emulator.serial;
38
+ }
39
+ const child = spawn(getAndroidToolPath('emulator', 'emulator'), ['-avd', emulator.id], {
40
+ detached: true,
41
+ stdio: 'ignore',
42
+ });
43
+ child.unref();
44
+ const deadline = Date.now() + BOOT_TIMEOUT_IN_MS;
45
+ while (Date.now() < deadline) {
46
+ await wait(BOOT_POLL_INTERVAL_IN_MS);
47
+ const serial = findAllRunningAndroidEmulators().get(emulator.id);
48
+ if (serial && isAndroidEmulatorBooted(serial)) {
49
+ return serial;
50
+ }
51
+ }
52
+ throw new UserError(`The emulator "${emulator.name}" did not finish booting in time.`);
53
+ };
54
+ /**
55
+ * Install an APK on a running Android emulator.
56
+ */
57
+ export const installAndroidApp = (serial, apkPath) => {
58
+ runAdb(['-s', serial, 'install', '-r', apkPath]);
59
+ };
60
+ /**
61
+ * Launch an app on a running Android emulator.
62
+ */
63
+ export const launchAndroidApp = (serial, packageName) => {
64
+ runAdb(['-s', serial, 'shell', 'monkey', '-p', packageName, '-c', 'android.intent.category.LAUNCHER', '1']);
65
+ };
66
+ /**
67
+ * Map the AVD ID of every running emulator to its serial.
68
+ */
69
+ const findAllRunningAndroidEmulators = () => {
70
+ const serials = runAdb(['devices'])
71
+ .split('\n')
72
+ .slice(1)
73
+ .map((line) => line.split('\t')[0]?.trim())
74
+ .filter((serial) => !!serial && serial.startsWith('emulator-'));
75
+ const serialById = new Map();
76
+ for (const serial of serials) {
77
+ if (!serial) {
78
+ continue;
79
+ }
80
+ try {
81
+ const id = runAdb(['-s', serial, 'emu', 'avd', 'name']).split('\n')[0]?.trim();
82
+ if (id) {
83
+ serialById.set(id, serial);
84
+ }
85
+ }
86
+ catch {
87
+ // Ignore emulators that do not respond to the console command.
88
+ }
89
+ }
90
+ return serialById;
91
+ };
92
+ /**
93
+ * Read the display name and API level of an emulator from its AVD configuration.
94
+ */
95
+ const getAndroidEmulatorConfig = (id) => {
96
+ try {
97
+ const config = fs.readFileSync(path.join(getAndroidAvdHome(), `${id}.avd`, 'config.ini'), 'utf-8');
98
+ return {
99
+ displayName: config.match(/^avd\.ini\.displayname=(.+)$/m)?.[1]?.trim() ?? null,
100
+ sdkVersion: config.match(/android-(\d+)/)?.[1] ?? null,
101
+ };
102
+ }
103
+ catch {
104
+ return { displayName: null, sdkVersion: null };
105
+ }
106
+ };
107
+ const getAndroidAvdHome = () => {
108
+ const avdHome = process.env.ANDROID_AVD_HOME;
109
+ if (avdHome) {
110
+ return avdHome;
111
+ }
112
+ const sdkHome = process.env.ANDROID_SDK_HOME;
113
+ return sdkHome ? path.join(sdkHome, '.android', 'avd') : path.join(os.homedir(), '.android', 'avd');
114
+ };
115
+ const isAndroidEmulatorBooted = (serial) => {
116
+ try {
117
+ return runAdb(['-s', serial, 'shell', 'getprop', 'sys.boot_completed']).trim() === '1';
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ };
123
+ const runAdb = (args) => run(getAndroidToolPath('platform-tools', 'adb'), args, 'adb');
124
+ const runEmulator = (args) => run(getAndroidToolPath('emulator', 'emulator'), args, 'emulator');
125
+ const run = (command, args, toolName) => {
126
+ try {
127
+ return execFileSync(command, args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
128
+ }
129
+ catch (error) {
130
+ if (getCodeFromUnknownError(error) === 'ENOENT') {
131
+ throw new UserError(`Could not find "${toolName}". Make sure the Android SDK is installed and either the ANDROID_HOME or ANDROID_SDK_ROOT environment variable points to it.`);
132
+ }
133
+ const stderr = error.stderr?.trim();
134
+ throw new UserError(stderr ? `The command "${toolName}" failed: ${stderr}` : `The command "${toolName}" failed.`);
135
+ }
136
+ };
137
+ /**
138
+ * Resolve an Android SDK tool, falling back to the `PATH` if the SDK cannot be located.
139
+ */
140
+ const getAndroidToolPath = (directory, binary) => {
141
+ const sdkRoot = getAndroidSdkRoot();
142
+ if (sdkRoot) {
143
+ const toolPath = path.join(sdkRoot, directory, binary);
144
+ if (fs.existsSync(toolPath)) {
145
+ return toolPath;
146
+ }
147
+ }
148
+ return binary;
149
+ };
150
+ const getAndroidSdkRoot = () => {
151
+ const sdkRoot = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
152
+ if (sdkRoot) {
153
+ return sdkRoot;
154
+ }
155
+ const defaultSdkRoot = getDefaultAndroidSdkRoot();
156
+ return fs.existsSync(defaultSdkRoot) ? defaultSdkRoot : undefined;
157
+ };
158
+ const getDefaultAndroidSdkRoot = () => {
159
+ switch (process.platform) {
160
+ case 'darwin': {
161
+ return path.join(os.homedir(), 'Library', 'Android', 'sdk');
162
+ }
163
+ case 'win32': {
164
+ return path.join(os.homedir(), 'AppData', 'Local', 'Android', 'Sdk');
165
+ }
166
+ default: {
167
+ return path.join(os.homedir(), 'Android', 'Sdk');
168
+ }
169
+ }
170
+ };
@@ -0,0 +1,57 @@
1
+ import { getCodeFromUnknownError, UserError } from '../utils/error.js';
2
+ import { execFileSync } from 'child_process';
3
+ /**
4
+ * Find all available iOS simulators installed on this machine.
5
+ */
6
+ export const findAllIosSimulators = () => {
7
+ const output = runSimctl(['list', 'devices', 'available', '--json']);
8
+ const devicesByRuntime = (JSON.parse(output).devices ?? {});
9
+ return Object.entries(devicesByRuntime)
10
+ .filter(([runtime]) => runtime.includes('iOS'))
11
+ .flatMap(([runtime, devices]) => devices.map((device) => ({
12
+ id: device.udid,
13
+ name: device.name,
14
+ running: device.state === 'Booted',
15
+ sdkVersion: getRuntimeVersion(runtime),
16
+ })));
17
+ };
18
+ /**
19
+ * Boot an iOS simulator, wait until it has finished booting and bring it to the front.
20
+ */
21
+ export const bootIosSimulator = (simulator) => {
22
+ runSimctl(['bootstatus', simulator.id, '-b']);
23
+ run('open', ['-a', 'Simulator'], 'open');
24
+ };
25
+ /**
26
+ * Install an app bundle on a booted iOS simulator.
27
+ */
28
+ export const installIosApp = (id, appPath) => {
29
+ runSimctl(['install', id, appPath]);
30
+ };
31
+ /**
32
+ * Launch an app on a booted iOS simulator.
33
+ */
34
+ export const launchIosApp = (id, packageName) => {
35
+ runSimctl(['launch', id, packageName]);
36
+ };
37
+ /**
38
+ * Extract the version from a simulator runtime identifier (e.g. `18.2`).
39
+ */
40
+ const getRuntimeVersion = (runtime) => {
41
+ const identifier = runtime.split('.').pop() ?? runtime;
42
+ const [, ...version] = identifier.split('-');
43
+ return version.join('.');
44
+ };
45
+ const runSimctl = (args) => run('xcrun', ['simctl', ...args], 'simctl');
46
+ const run = (command, args, toolName) => {
47
+ try {
48
+ return execFileSync(command, args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
49
+ }
50
+ catch (error) {
51
+ if (getCodeFromUnknownError(error) === 'ENOENT') {
52
+ throw new UserError(`Could not find "${toolName}". Make sure Xcode is installed.`);
53
+ }
54
+ const stderr = error.stderr?.trim();
55
+ throw new UserError(stderr ? `The command "${toolName}" failed: ${stderr}` : `The command "${toolName}" failed.`);
56
+ }
57
+ };
package/dist/utils/zip.js CHANGED
@@ -4,6 +4,10 @@ import { globby } from 'globby';
4
4
  import path from 'path';
5
5
  const MAX_ZIP_ENTRIES = 65535;
6
6
  class ZipImpl {
7
+ async unzipToFolder(buffer, targetFolder) {
8
+ const zip = new AdmZip(buffer);
9
+ zip.extractAllTo(targetFolder, true);
10
+ }
7
11
  async zipFolder(sourceFolder) {
8
12
  const zip = new AdmZip();
9
13
  zip.addLocalFolder(sourceFolder);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "4.17.2",
3
+ "version": "4.18.0",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -60,7 +60,7 @@
60
60
  "@robingenz/zli": "0.2.0",
61
61
  "@sentry/node": "10.58.0",
62
62
  "adm-zip": "0.6.0",
63
- "axios": "1.16.0",
63
+ "axios": "1.18.1",
64
64
  "axios-retry": "4.5.0",
65
65
  "c12": "3.3.3",
66
66
  "consola": "3.3.0",