@capawesome/cli 3.5.0 → 3.7.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 CHANGED
@@ -2,6 +2,20 @@
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
+ ## [3.7.0](https://github.com/capawesome-team/cli/compare/v3.6.0...v3.7.0) (2025-12-02)
6
+
7
+
8
+ ### Features
9
+
10
+ * **apps:builds:create:** add JSON output option ([#101](https://github.com/capawesome-team/cli/issues/101)) ([32797f9](https://github.com/capawesome-team/cli/commit/32797f93229848fc23565e89f245772e3e509500))
11
+
12
+ ## [3.6.0](https://github.com/capawesome-team/cli/compare/v3.5.0...v3.6.0) (2025-11-29)
13
+
14
+
15
+ ### Features
16
+
17
+ * add proxy support for HTTP and HTTPS requests ([#99](https://github.com/capawesome-team/cli/issues/99)) ([5a6e627](https://github.com/capawesome-team/cli/commit/5a6e627a85634628ea9c365d4e7b25cc69fbe11d))
18
+
5
19
  ## [3.5.0](https://github.com/capawesome-team/cli/compare/v3.4.2...v3.5.0) (2025-11-24)
6
20
 
7
21
 
package/README.md CHANGED
@@ -34,6 +34,8 @@ npx @capawesome/cli --help
34
34
 
35
35
  ## Development
36
36
 
37
+ ### Getting Started
38
+
37
39
  Run the following commands to get started with development:
38
40
 
39
41
  1. Clone the repository:
@@ -57,6 +59,26 @@ Run the following commands to get started with development:
57
59
 
58
60
  **Note:** The `--` is required to pass arguments to the script.
59
61
 
62
+ ### Testing Proxy Support
63
+
64
+ To test HTTP/HTTPS proxy functionality locally:
65
+
66
+ 1. Start Squid proxy in a separate terminal:
67
+ ```bash
68
+ docker run --rm --name squid-proxy -p 3128:3128 -v $(pwd)/squid.conf:/etc/squid/squid.conf:ro sameersbn/squid:latest
69
+ ```
70
+
71
+ 2. Set proxy environment variables and run the CLI:
72
+ ```bash
73
+ export https_proxy=http://localhost:3128
74
+ npm run build && node ./dist/index.js login
75
+ ```
76
+
77
+ 3. To see debug output:
78
+ ```bash
79
+ DEBUG=https-proxy-agent node ./dist/index.js login
80
+ ```
81
+
60
82
  ## Changelog
61
83
 
62
84
  See [CHANGELOG](./CHANGELOG.md).
@@ -44,6 +44,7 @@ export default defineCommand({
44
44
  .union([z.boolean(), z.string()])
45
45
  .optional()
46
46
  .describe('Download the generated IPA file (iOS only). Optionally provide a file path.'),
47
+ json: z.boolean().optional().describe('Output in JSON format.'),
47
48
  platform: z
48
49
  .enum(['ios', 'android'], {
49
50
  message: 'Platform must be either `ios` or `android`.',
@@ -56,7 +57,7 @@ export default defineCommand({
56
57
  .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`.'),
57
58
  })),
58
59
  action: async (options) => {
59
- let { appId, platform, type, gitRef, environment, certificate } = options;
60
+ let { appId, platform, type, gitRef, environment, certificate, json } = options;
60
61
  // Check if the user is logged in
61
62
  if (!authorizationService.hasAuthorizationToken()) {
62
63
  consola.error('You must be logged in to run this command.');
@@ -193,7 +194,9 @@ export default defineCommand({
193
194
  }
194
195
  }
195
196
  // Create the app build
196
- consola.start('Creating build...');
197
+ if (!json) {
198
+ consola.start('Creating build...');
199
+ }
197
200
  const response = await appBuildsService.create({
198
201
  appCertificateName: certificate,
199
202
  appEnvironmentName: environment,
@@ -202,10 +205,12 @@ export default defineCommand({
202
205
  platform,
203
206
  type,
204
207
  });
205
- consola.success(`Build created successfully.`);
206
- consola.info(`Build Number: ${response.numberAsString}`);
207
- consola.info(`Build ID: ${response.id}`);
208
- consola.info(`Build URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/builds/${response.id}`);
208
+ if (!json) {
209
+ consola.success(`Build created successfully.`);
210
+ consola.info(`Build Number: ${response.numberAsString}`);
211
+ consola.info(`Build ID: ${response.id}`);
212
+ consola.info(`Build URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/builds/${response.id}`);
213
+ }
209
214
  // Wait for build job to complete by default, unless --detached flag is set
210
215
  const shouldWait = !options.detached;
211
216
  if (shouldWait) {
@@ -226,7 +231,7 @@ export default defineCommand({
226
231
  const jobStatus = build.job.status;
227
232
  // Show spinner while queued or pending
228
233
  if (jobStatus === 'queued' || jobStatus === 'pending') {
229
- if (isWaitingForStart) {
234
+ if (isWaitingForStart && !json) {
230
235
  consola.start(`Waiting for build to start (status: ${jobStatus})...`);
231
236
  }
232
237
  await wait(3000);
@@ -235,10 +240,12 @@ export default defineCommand({
235
240
  // Stop spinner when job moves to in_progress
236
241
  if (isWaitingForStart && jobStatus === 'in_progress') {
237
242
  isWaitingForStart = false;
238
- consola.success('Build started...');
243
+ if (!json) {
244
+ consola.success('Build started...');
245
+ }
239
246
  }
240
247
  // Print new logs
241
- if (build.job.jobLogs && build.job.jobLogs.length > 0) {
248
+ if (!json && build.job.jobLogs && build.job.jobLogs.length > 0) {
242
249
  const newLogs = build.job.jobLogs
243
250
  .filter((log) => log.number > lastPrintedLogNumber)
244
251
  .sort((a, b) => a.number - b.number);
@@ -253,10 +260,14 @@ export default defineCommand({
253
260
  jobStatus === 'canceled' ||
254
261
  jobStatus === 'rejected' ||
255
262
  jobStatus === 'timed_out') {
256
- console.log(); // New line for better readability
257
- if (jobStatus === 'succeeded') {
258
- consola.success('Build completed successfully.');
263
+ if (!json) {
259
264
  console.log(); // New line for better readability
265
+ }
266
+ if (jobStatus === 'succeeded') {
267
+ if (!json) {
268
+ consola.success('Build completed successfully.');
269
+ console.log(); // New line for better readability
270
+ }
260
271
  // Download artifacts if flags are set
261
272
  if (options.apk && platform === 'android') {
262
273
  await handleArtifactDownload({
@@ -265,6 +276,7 @@ export default defineCommand({
265
276
  buildArtifacts: build.appBuildArtifacts,
266
277
  artifactType: 'apk',
267
278
  filePath: typeof options.apk === 'string' ? options.apk : undefined,
279
+ json,
268
280
  });
269
281
  }
270
282
  if (options.aab && platform === 'android') {
@@ -274,6 +286,7 @@ export default defineCommand({
274
286
  buildArtifacts: build.appBuildArtifacts,
275
287
  artifactType: 'aab',
276
288
  filePath: typeof options.aab === 'string' ? options.aab : undefined,
289
+ json,
277
290
  });
278
291
  }
279
292
  if (options.ipa && platform === 'ios') {
@@ -283,8 +296,17 @@ export default defineCommand({
283
296
  buildArtifacts: build.appBuildArtifacts,
284
297
  artifactType: 'ipa',
285
298
  filePath: typeof options.ipa === 'string' ? options.ipa : undefined,
299
+ json,
286
300
  });
287
301
  }
302
+ // Output JSON if json flag is set
303
+ if (json) {
304
+ console.log(JSON.stringify({
305
+ id: response.id,
306
+ numberAsString: response.numberAsString,
307
+ }, null, 2));
308
+ }
309
+ // Exit successfully
288
310
  process.exit(0);
289
311
  }
290
312
  else if (jobStatus === 'failed') {
@@ -313,24 +335,38 @@ export default defineCommand({
313
335
  }
314
336
  }
315
337
  }
338
+ else {
339
+ if (json) {
340
+ console.log(JSON.stringify({
341
+ id: response.id,
342
+ numberAsString: response.numberAsString,
343
+ }, null, 2));
344
+ }
345
+ }
316
346
  },
317
347
  });
318
348
  /**
319
349
  * Download a build artifact (APK, AAB, or IPA).
320
350
  */
321
351
  const handleArtifactDownload = async (options) => {
322
- const { appId, buildId, buildArtifacts, artifactType, filePath } = options;
352
+ const { appId, buildId, buildArtifacts, artifactType, filePath, json } = options;
323
353
  try {
324
354
  const artifactTypeUpper = artifactType.toUpperCase();
325
- consola.start(`Downloading ${artifactTypeUpper}...`);
355
+ if (!json) {
356
+ consola.start(`Downloading ${artifactTypeUpper}...`);
357
+ }
326
358
  // Find the artifact
327
359
  const artifact = buildArtifacts?.find((artifact) => artifact.type === artifactType);
328
360
  if (!artifact) {
329
- consola.warn(`No ${artifactTypeUpper} artifact found for this build.`);
361
+ if (!json) {
362
+ consola.warn(`No ${artifactTypeUpper} artifact found for this build.`);
363
+ }
330
364
  return;
331
365
  }
332
366
  if (artifact.status !== 'ready') {
333
- consola.warn(`${artifactTypeUpper} artifact is not ready (status: ${artifact.status}).`);
367
+ if (!json) {
368
+ consola.warn(`${artifactTypeUpper} artifact is not ready (status: ${artifact.status}).`);
369
+ }
334
370
  return;
335
371
  }
336
372
  // Download the artifact
@@ -351,7 +387,9 @@ const handleArtifactDownload = async (options) => {
351
387
  }
352
388
  // Save the file
353
389
  await fs.writeFile(outputPath, Buffer.from(artifactData));
354
- consola.success(`${artifactTypeUpper} downloaded successfully: ${outputPath}`);
390
+ if (!json) {
391
+ consola.success(`${artifactTypeUpper} downloaded successfully: ${outputPath}`);
392
+ }
355
393
  }
356
394
  catch (error) {
357
395
  consola.error(`Failed to download ${artifactType.toUpperCase()}:`, error);
@@ -2,6 +2,8 @@ import { createRequire } from 'module';
2
2
  import configService from '../services/config.js';
3
3
  import axios from 'axios';
4
4
  import axiosRetry from 'axios-retry';
5
+ import { HttpProxyAgent } from 'http-proxy-agent';
6
+ import { HttpsProxyAgent } from 'https-proxy-agent';
5
7
  const require = createRequire(import.meta.url);
6
8
  const pkg = require('../../package.json');
7
9
  // Register middleware to retry failed requests
@@ -14,6 +16,22 @@ axiosRetry(axios, {
14
16
  (error.response?.status !== undefined && error.response.status >= 500));
15
17
  },
16
18
  });
19
+ /**
20
+ * Gets the appropriate proxy agent based on the target URL protocol and environment variables.
21
+ * This ensures that HTTPS requests use HTTPS even when the proxy itself is accessed via HTTP.
22
+ */
23
+ function getProxyAgent(targetUrl) {
24
+ const isHttps = targetUrl.startsWith('https://');
25
+ const proxyUrl = isHttps
26
+ ? process.env.HTTPS_PROXY || process.env.https_proxy
27
+ : process.env.HTTP_PROXY || process.env.http_proxy;
28
+ if (!proxyUrl) {
29
+ return undefined;
30
+ }
31
+ // Use the appropriate agent based on the TARGET protocol, not the proxy protocol
32
+ // This allows using an HTTP proxy for HTTPS requests
33
+ return isHttps ? new HttpsProxyAgent(proxyUrl) : new HttpProxyAgent(proxyUrl);
34
+ }
17
35
  class HttpClientImpl {
18
36
  baseHeaders = {
19
37
  'User-Agent': `Capawesome CLI v${pkg.version}`,
@@ -21,27 +39,62 @@ class HttpClientImpl {
21
39
  async delete(url, config) {
22
40
  const baseUrl = await configService.getValueForKey('API_BASE_URL');
23
41
  const urlWithHost = url.startsWith('http') ? url : baseUrl + url;
24
- return axios.delete(urlWithHost, { ...config, headers: { ...this.baseHeaders, ...config?.headers } });
42
+ const proxyAgent = getProxyAgent(urlWithHost);
43
+ const axiosConfig = {
44
+ ...config,
45
+ headers: { ...this.baseHeaders, ...config?.headers },
46
+ ...(proxyAgent && urlWithHost.startsWith('https://') ? { httpsAgent: proxyAgent, proxy: false } : {}),
47
+ ...(proxyAgent && urlWithHost.startsWith('http://') ? { httpAgent: proxyAgent, proxy: false } : {}),
48
+ };
49
+ return axios.delete(urlWithHost, axiosConfig);
25
50
  }
26
51
  async get(url, config) {
27
52
  const baseUrl = await configService.getValueForKey('API_BASE_URL');
28
53
  const urlWithHost = url.startsWith('http') ? url : baseUrl + url;
29
- return axios.get(urlWithHost, { ...config, headers: { ...this.baseHeaders, ...config?.headers } });
54
+ const proxyAgent = getProxyAgent(urlWithHost);
55
+ const axiosConfig = {
56
+ ...config,
57
+ headers: { ...this.baseHeaders, ...config?.headers },
58
+ ...(proxyAgent && urlWithHost.startsWith('https://') ? { httpsAgent: proxyAgent, proxy: false } : {}),
59
+ ...(proxyAgent && urlWithHost.startsWith('http://') ? { httpAgent: proxyAgent, proxy: false } : {}),
60
+ };
61
+ return axios.get(urlWithHost, axiosConfig);
30
62
  }
31
63
  async patch(url, data, config) {
32
64
  const baseUrl = await configService.getValueForKey('API_BASE_URL');
33
65
  const urlWithHost = url.startsWith('http') ? url : baseUrl + url;
34
- return axios.patch(urlWithHost, data, { ...config, headers: { ...this.baseHeaders, ...config?.headers } });
66
+ const proxyAgent = getProxyAgent(urlWithHost);
67
+ const axiosConfig = {
68
+ ...config,
69
+ headers: { ...this.baseHeaders, ...config?.headers },
70
+ ...(proxyAgent && urlWithHost.startsWith('https://') ? { httpsAgent: proxyAgent, proxy: false } : {}),
71
+ ...(proxyAgent && urlWithHost.startsWith('http://') ? { httpAgent: proxyAgent, proxy: false } : {}),
72
+ };
73
+ return axios.patch(urlWithHost, data, axiosConfig);
35
74
  }
36
75
  async post(url, data, config) {
37
76
  const baseUrl = await configService.getValueForKey('API_BASE_URL');
38
77
  const urlWithHost = url.startsWith('http') ? url : baseUrl + url;
39
- return axios.post(urlWithHost, data, { ...config, headers: { ...this.baseHeaders, ...config?.headers } });
78
+ const proxyAgent = getProxyAgent(urlWithHost);
79
+ const axiosConfig = {
80
+ ...config,
81
+ headers: { ...this.baseHeaders, ...config?.headers },
82
+ ...(proxyAgent && urlWithHost.startsWith('https://') ? { httpsAgent: proxyAgent, proxy: false } : {}),
83
+ ...(proxyAgent && urlWithHost.startsWith('http://') ? { httpAgent: proxyAgent, proxy: false } : {}),
84
+ };
85
+ return axios.post(urlWithHost, data, axiosConfig);
40
86
  }
41
87
  async put(url, data, config) {
42
88
  const baseUrl = await configService.getValueForKey('API_BASE_URL');
43
89
  const urlWithHost = url.startsWith('http') ? url : baseUrl + url;
44
- return axios.put(urlWithHost, data, { ...config, headers: { ...this.baseHeaders, ...config?.headers } });
90
+ const proxyAgent = getProxyAgent(urlWithHost);
91
+ const axiosConfig = {
92
+ ...config,
93
+ headers: { ...this.baseHeaders, ...config?.headers },
94
+ ...(proxyAgent && urlWithHost.startsWith('https://') ? { httpsAgent: proxyAgent, proxy: false } : {}),
95
+ ...(proxyAgent && urlWithHost.startsWith('http://') ? { httpAgent: proxyAgent, proxy: false } : {}),
96
+ };
97
+ return axios.put(urlWithHost, data, axiosConfig);
45
98
  }
46
99
  }
47
100
  let httpClient = new HttpClientImpl();
@@ -1,6 +1,6 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import nock from 'nock';
3
1
  import configService from '../services/config.js';
2
+ import nock from 'nock';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  // Mock the config service
5
5
  vi.mock('@/services/config.js', () => ({
6
6
  default: {
@@ -8,9 +8,21 @@ vi.mock('@/services/config.js', () => ({
8
8
  },
9
9
  }));
10
10
  describe('http-client', () => {
11
+ let originalEnv;
11
12
  beforeEach(() => {
13
+ // Save original environment variables
14
+ originalEnv = { ...process.env };
12
15
  nock.cleanAll();
13
16
  vi.clearAllMocks();
17
+ // Clear proxy environment variables
18
+ delete process.env.HTTP_PROXY;
19
+ delete process.env.http_proxy;
20
+ delete process.env.HTTPS_PROXY;
21
+ delete process.env.https_proxy;
22
+ });
23
+ afterEach(() => {
24
+ // Restore original environment variables
25
+ process.env = originalEnv;
14
26
  });
15
27
  it('should retry requests on 5xx status codes', async () => {
16
28
  // Mock the API_BASE_URL
@@ -69,4 +81,19 @@ describe('http-client', () => {
69
81
  expect(response.data).toEqual({ recovered: true });
70
82
  expect(nock.isDone()).toBe(true);
71
83
  });
84
+ it('should work without proxy when no environment variables are set', async () => {
85
+ vi.mocked(configService.getValueForKey).mockResolvedValue('https://api.example.com');
86
+ nock('https://api.example.com').get('/no-proxy').reply(200, { success: true });
87
+ const { default: httpClient } = await import('./http-client.js');
88
+ const response = await httpClient.get('/no-proxy');
89
+ expect(response.status).toBe(200);
90
+ expect(response.data).toEqual({ success: true });
91
+ });
92
+ // Note: Testing actual proxy behavior with nock is not reliable as nock intercepts
93
+ // requests at a different level than proxy agents. The proxy functionality is handled
94
+ // by the http-proxy-agent and https-proxy-agent libraries which are well-tested.
95
+ // The implementation ensures that:
96
+ // - HTTPS requests use HttpsProxyAgent when https_proxy/HTTPS_PROXY is set
97
+ // - HTTP requests use HttpProxyAgent when http_proxy/HTTP_PROXY is set
98
+ // - HTTP proxies (http://) work correctly for HTTPS targets (https://)
72
99
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -61,6 +61,8 @@
61
61
  "c12": "2.0.1",
62
62
  "consola": "3.3.0",
63
63
  "form-data": "4.0.4",
64
+ "http-proxy-agent": "7.0.2",
65
+ "https-proxy-agent": "7.0.6",
64
66
  "mime": "4.0.7",
65
67
  "open": "10.2.0",
66
68
  "rc9": "2.1.2",