@capawesome/cli 2.0.2 → 2.1.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,21 @@
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.1.0](https://github.com/capawesome-team/cli/compare/v2.0.3...v2.1.0) (2025-08-26)
6
+
7
+
8
+ ### Features
9
+
10
+ * retry failed http requests if status code is `5xx` ([4a2399f](https://github.com/capawesome-team/cli/commit/4a2399f8219edd35d4a5a6f79e3f4c5e15fa8659)), closes [#67](https://github.com/capawesome-team/cli/issues/67)
11
+
12
+ ## [2.0.3](https://github.com/capawesome-team/cli/compare/v2.0.2...v2.0.3) (2025-08-26)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * `--version` was no longer supported ([526819b](https://github.com/capawesome-team/cli/commit/526819bebb2e5296380183fa4ec331b364376718))
18
+ * do not capture HTTP errors ([83f93e2](https://github.com/capawesome-team/cli/commit/83f93e21fc706e3245f349891e76501385dea03b))
19
+
5
20
  ## [2.0.2](https://github.com/capawesome-team/cli/compare/v2.0.1...v2.0.2) (2025-08-24)
6
21
 
7
22
 
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import updateService from './services/update.js';
4
4
  import { getMessageFromUnknownError } from './utils/error.js';
5
5
  import { defineConfig, processConfig, ZliError } from '@robingenz/zli';
6
6
  import * as Sentry from '@sentry/node';
7
+ import { AxiosError } from 'axios';
7
8
  import consola from 'consola';
8
9
  import pkg from '../package.json' with { type: 'json' };
9
10
  const config = defineConfig({
@@ -33,9 +34,12 @@ const config = defineConfig({
33
34
  },
34
35
  });
35
36
  const captureException = async (error) => {
36
- // Check if the error is from the CLI itself
37
+ // Ignore errors from the CLI itself (e.g. "No command found.")
37
38
  if (error instanceof ZliError) {
38
- // Ignore CLI errors like "No command found."
39
+ return;
40
+ }
41
+ // Ignore failed HTTP requests
42
+ if (error instanceof AxiosError) {
39
43
  return;
40
44
  }
41
45
  const environment = await configService.getValueForKey('ENVIRONMENT');
@@ -0,0 +1,33 @@
1
+ import { defineConfig, processConfig } from '@robingenz/zli';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import pkg from '../package.json' with { type: 'json' };
4
+ const config = defineConfig({
5
+ meta: {
6
+ name: pkg.name,
7
+ version: pkg.version,
8
+ description: pkg.description,
9
+ },
10
+ commands: {},
11
+ });
12
+ describe('CLI', () => {
13
+ let consoleSpy;
14
+ let processExitSpy;
15
+ beforeEach(() => {
16
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
17
+ processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
18
+ });
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+ it('should display version when --version flag is used', () => {
23
+ try {
24
+ processConfig(config, ['--version']);
25
+ }
26
+ catch (error) {
27
+ // The processConfig function calls process.exit which we mock
28
+ // so we continue execution and check the mocks
29
+ }
30
+ expect(consoleSpy).toHaveBeenCalledWith(pkg.version);
31
+ expect(processExitSpy).toHaveBeenCalledWith(0);
32
+ });
33
+ });
@@ -1,6 +1,17 @@
1
+ import configService from '../services/config.js';
1
2
  import axios from 'axios';
3
+ import axiosRetry from 'axios-retry';
2
4
  import pkg from '../../package.json' with { type: 'json' };
3
- import configService from '../services/config.js';
5
+ // Register middleware to retry failed requests
6
+ axiosRetry(axios, {
7
+ retries: 3,
8
+ retryDelay: axiosRetry.exponentialDelay,
9
+ retryCondition: (error) => {
10
+ // Network errors and 5xx responses are retried
11
+ return (axiosRetry.isNetworkOrIdempotentRequestError(error) ||
12
+ (error.response?.status !== undefined && error.response.status >= 500));
13
+ },
14
+ });
4
15
  class HttpClientImpl {
5
16
  baseHeaders = {
6
17
  'User-Agent': `Capawesome CLI v${pkg.version}`,
@@ -0,0 +1,72 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import nock from 'nock';
3
+ import configService from '../services/config.js';
4
+ // Mock the config service
5
+ vi.mock('@/services/config.js', () => ({
6
+ default: {
7
+ getValueForKey: vi.fn().mockResolvedValue('https://api.example.com'),
8
+ },
9
+ }));
10
+ describe('http-client', () => {
11
+ beforeEach(() => {
12
+ nock.cleanAll();
13
+ vi.clearAllMocks();
14
+ });
15
+ it('should retry requests on 5xx status codes', async () => {
16
+ // Mock the API_BASE_URL
17
+ vi.mocked(configService.getValueForKey).mockResolvedValue('https://api.example.com');
18
+ // Mock the first two requests to return 500, then succeed on the third
19
+ nock('https://api.example.com')
20
+ .get('/test')
21
+ .reply(500, { error: 'Internal Server Error' })
22
+ .get('/test')
23
+ .reply(502, { error: 'Bad Gateway' })
24
+ .get('/test')
25
+ .reply(200, { success: true });
26
+ // Import http-client after mocking to ensure axios-retry is configured
27
+ const { default: httpClient } = await import('./http-client.js');
28
+ const response = await httpClient.get('/test');
29
+ expect(response.status).toBe(200);
30
+ expect(response.data).toEqual({ success: true });
31
+ expect(nock.isDone()).toBe(true);
32
+ });
33
+ it('should not retry requests on 4xx status codes', async () => {
34
+ vi.mocked(configService.getValueForKey).mockResolvedValue('https://api.example.com');
35
+ // Mock a 404 response - should not be retried
36
+ nock('https://api.example.com').get('/not-found').reply(404, { error: 'Not Found' });
37
+ const { default: httpClient } = await import('./http-client.js');
38
+ await expect(httpClient.get('/not-found')).rejects.toThrow();
39
+ expect(nock.isDone()).toBe(true);
40
+ });
41
+ it('should eventually fail after maximum retries on persistent 5xx errors', async () => {
42
+ vi.mocked(configService.getValueForKey).mockResolvedValue('https://api.example.com');
43
+ // Mock 4 consecutive 500 responses (initial + 3 retries)
44
+ nock('https://api.example.com').get('/persistent-error').times(4).reply(500, { error: 'Internal Server Error' });
45
+ const { default: httpClient } = await import('./http-client.js');
46
+ await expect(httpClient.get('/persistent-error')).rejects.toThrow();
47
+ expect(nock.isDone()).toBe(true);
48
+ });
49
+ it('should succeed on first try when no errors occur', async () => {
50
+ vi.mocked(configService.getValueForKey).mockResolvedValue('https://api.example.com');
51
+ nock('https://api.example.com').get('/success').reply(200, { data: 'success' });
52
+ const { default: httpClient } = await import('./http-client.js');
53
+ const response = await httpClient.get('/success');
54
+ expect(response.status).toBe(200);
55
+ expect(response.data).toEqual({ data: 'success' });
56
+ expect(nock.isDone()).toBe(true);
57
+ });
58
+ it('should retry on network errors', async () => {
59
+ vi.mocked(configService.getValueForKey).mockResolvedValue('https://api.example.com');
60
+ // Mock a network error followed by success
61
+ nock('https://api.example.com')
62
+ .get('/network-error')
63
+ .replyWithError('Network Error')
64
+ .get('/network-error')
65
+ .reply(200, { recovered: true });
66
+ const { default: httpClient } = await import('./http-client.js');
67
+ const response = await httpClient.get('/network-error');
68
+ expect(response.status).toBe(200);
69
+ expect(response.data).toEqual({ recovered: true });
70
+ expect(nock.isDone()).toBe(true);
71
+ });
72
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -49,10 +49,11 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@clack/prompts": "0.7.0",
52
- "@robingenz/zli": "0.1.4",
52
+ "@robingenz/zli": "0.1.5",
53
53
  "@sentry/node": "8.55.0",
54
54
  "archiver": "7.0.1",
55
55
  "axios": "1.8.4",
56
+ "axios-retry": "4.5.0",
56
57
  "c12": "2.0.1",
57
58
  "consola": "3.3.0",
58
59
  "form-data": "4.0.4",