@capawesome/cli 4.8.3 → 4.9.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,19 @@
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
+ ## [4.9.0](https://github.com/capawesome-team/cli/compare/v4.8.3...v4.9.0) (2026-05-01)
6
+
7
+
8
+ ### Features
9
+
10
+ * add `get`/`list` commands for apps, builds, deployments, and organizations ([#156](https://github.com/capawesome-team/cli/issues/156)) ([32363d4](https://github.com/capawesome-team/cli/commit/32363d4f7d44bc0d7036f9062a1d74c01e15dfed))
11
+ * **apps:** add `--type` option to `apps:create` ([#154](https://github.com/capawesome-team/cli/issues/154)) ([c23c877](https://github.com/capawesome-team/cli/commit/c23c8775359de0841ce57904dc4c75b9420fad04))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * surface actionable error when config file is not writable ([#153](https://github.com/capawesome-team/cli/issues/153)) ([ab8ed4c](https://github.com/capawesome-team/cli/commit/ab8ed4cb33ab02c5e5db62140ca3501b02e263b4))
17
+
5
18
  ## [4.8.3](https://github.com/capawesome-team/cli/compare/v4.8.2...v4.8.3) (2026-04-20)
6
19
 
7
20
 
@@ -0,0 +1,36 @@
1
+ import appBuildsService from '../../../services/app-builds.js';
2
+ import { withAuth } from '../../../utils/auth.js';
3
+ import { defineCommand, defineOptions } from '@robingenz/zli';
4
+ import consola from 'consola';
5
+ import { z } from 'zod';
6
+ export default defineCommand({
7
+ description: 'Get an existing app build.',
8
+ options: defineOptions(z.object({
9
+ appId: z.string().optional().describe('ID of the app.'),
10
+ buildId: z.string().optional().describe('ID of the build.'),
11
+ json: z.boolean().optional().describe('Output in JSON format.'),
12
+ })),
13
+ action: withAuth(async (options, args) => {
14
+ const { appId, buildId, json } = options;
15
+ if (!appId) {
16
+ consola.error('You must provide an app ID.');
17
+ process.exit(1);
18
+ }
19
+ if (!buildId) {
20
+ consola.error('You must provide a build ID.');
21
+ process.exit(1);
22
+ }
23
+ const build = await appBuildsService.findOne({
24
+ appId,
25
+ appBuildId: buildId,
26
+ relations: json ? 'job' : undefined,
27
+ });
28
+ if (json) {
29
+ console.log(JSON.stringify(build, null, 2));
30
+ }
31
+ else {
32
+ console.table(build);
33
+ consola.success('Build retrieved successfully.');
34
+ }
35
+ }),
36
+ });
@@ -0,0 +1,44 @@
1
+ import appBuildsService from '../../../services/app-builds.js';
2
+ import { withAuth } from '../../../utils/auth.js';
3
+ import { isInteractive } from '../../../utils/environment.js';
4
+ import { promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
5
+ import { defineCommand, defineOptions } from '@robingenz/zli';
6
+ import consola from 'consola';
7
+ import { z } from 'zod';
8
+ export default defineCommand({
9
+ description: 'Retrieve a list of existing app builds.',
10
+ options: defineOptions(z.object({
11
+ appId: z.string().optional().describe('ID of the app.'),
12
+ json: z.boolean().optional().describe('Output in JSON format.'),
13
+ limit: z.coerce.number().optional().describe('Limit for pagination.'),
14
+ numberAsString: z.string().optional().describe('Build number to filter by.'),
15
+ offset: z.coerce.number().optional().describe('Offset for pagination.'),
16
+ platform: z.enum(['android', 'ios', 'web']).optional().describe('Platform to filter by.'),
17
+ })),
18
+ action: withAuth(async (options, args) => {
19
+ let { appId, json, limit, numberAsString, offset, platform } = options;
20
+ if (!appId) {
21
+ if (!isInteractive()) {
22
+ consola.error('You must provide an app ID when running in non-interactive environment.');
23
+ process.exit(1);
24
+ }
25
+ const organizationId = await promptOrganizationSelection();
26
+ appId = await promptAppSelection(organizationId);
27
+ }
28
+ const foundBuilds = await appBuildsService.findAll({
29
+ appId,
30
+ limit,
31
+ numberAsString,
32
+ offset,
33
+ platform,
34
+ relations: json ? 'job' : undefined,
35
+ });
36
+ if (json) {
37
+ console.log(JSON.stringify(foundBuilds, null, 2));
38
+ }
39
+ else {
40
+ console.table(foundBuilds);
41
+ consola.success('Builds retrieved successfully.');
42
+ }
43
+ }),
44
+ });
@@ -13,10 +13,15 @@ export default defineCommand({
13
13
  link: z.boolean().optional().describe('Connect the created app to the local git repository.'),
14
14
  name: z.string().optional().describe('Name of the app.'),
15
15
  organizationId: z.string().optional().describe('ID of the organization to create the app in.'),
16
+ type: z
17
+ .enum(['android', 'capacitor', 'cordova', 'ios'])
18
+ .optional()
19
+ .describe('Type of the app. Defaults to `capacitor`.'),
16
20
  yes: z.boolean().optional().describe('Skip all confirmation prompts.'),
17
21
  }), { y: 'yes' }),
18
22
  action: withAuth(async (options, args) => {
19
23
  let { json, name, organizationId } = options;
24
+ const type = options.type ?? 'capacitor';
20
25
  if (!organizationId) {
21
26
  if (!isInteractive()) {
22
27
  consola.error('You must provide the organization ID when running in non-interactive environment.');
@@ -31,7 +36,7 @@ export default defineCommand({
31
36
  }
32
37
  name = await prompt('Enter the name of the app:', { type: 'text' });
33
38
  }
34
- const response = await appsService.create({ name, organizationId });
39
+ const response = await appsService.create({ name, organizationId, type });
35
40
  if (!json) {
36
41
  consola.info(`App ID: ${response.id}`);
37
42
  consola.info(`App URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${response.id}`);
@@ -38,9 +38,9 @@ describe('apps-create', () => {
38
38
  const organizationId = 'org-123';
39
39
  const appId = 'app-456';
40
40
  const testToken = 'test-token';
41
- const options = { name: appName, organizationId };
41
+ const options = { name: appName, organizationId, type: 'capacitor' };
42
42
  const scope = nock(DEFAULT_API_BASE_URL)
43
- .post(`/v1/apps?organizationId=${organizationId}`, { name: appName })
43
+ .post(`/v1/apps?organizationId=${organizationId}`, { name: appName, type: 'capacitor' })
44
44
  .matchHeader('Authorization', `Bearer ${testToken}`)
45
45
  .reply(201, { id: appId, name: appName });
46
46
  await createAppCommand.action(options, undefined);
@@ -53,9 +53,9 @@ describe('apps-create', () => {
53
53
  const orgId = 'org-1';
54
54
  const appId = 'app-456';
55
55
  const testToken = 'test-token';
56
- const options = { name: appName };
56
+ const options = { name: appName, type: 'capacitor' };
57
57
  const createScope = nock(DEFAULT_API_BASE_URL)
58
- .post(`/v1/apps?organizationId=${orgId}`, { name: appName })
58
+ .post(`/v1/apps?organizationId=${orgId}`, { name: appName, type: 'capacitor' })
59
59
  .matchHeader('Authorization', `Bearer ${testToken}`)
60
60
  .reply(201, { id: appId, name: appName });
61
61
  mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
@@ -69,9 +69,9 @@ describe('apps-create', () => {
69
69
  const promptedAppName = 'Prompted App';
70
70
  const appId = 'app-456';
71
71
  const testToken = 'test-token';
72
- const options = { organizationId };
72
+ const options = { organizationId, type: 'capacitor' };
73
73
  const scope = nock(DEFAULT_API_BASE_URL)
74
- .post(`/v1/apps?organizationId=${organizationId}`, { name: promptedAppName })
74
+ .post(`/v1/apps?organizationId=${organizationId}`, { name: promptedAppName, type: 'capacitor' })
75
75
  .matchHeader('Authorization', `Bearer ${testToken}`)
76
76
  .reply(201, { id: appId, name: promptedAppName });
77
77
  mockPrompt.mockResolvedValueOnce(promptedAppName);
@@ -80,7 +80,7 @@ describe('apps-create', () => {
80
80
  expect(mockPrompt).toHaveBeenCalledWith('Enter the name of the app:', { type: 'text' });
81
81
  });
82
82
  it('should exit when promptOrganizationSelection exits', async () => {
83
- const options = { name: 'Test App' };
83
+ const options = { name: 'Test App', type: 'capacitor' };
84
84
  mockPromptOrganizationSelection.mockImplementation(() => {
85
85
  process.exit(1);
86
86
  return Promise.resolve('');
@@ -91,9 +91,9 @@ describe('apps-create', () => {
91
91
  const appName = 'Test App';
92
92
  const organizationId = 'org-123';
93
93
  const testToken = 'test-token';
94
- const options = { name: appName, organizationId };
94
+ const options = { name: appName, organizationId, type: 'capacitor' };
95
95
  const scope = nock(DEFAULT_API_BASE_URL)
96
- .post(`/v1/apps?organizationId=${organizationId}`, { name: appName })
96
+ .post(`/v1/apps?organizationId=${organizationId}`, { name: appName, type: 'capacitor' })
97
97
  .matchHeader('Authorization', `Bearer ${testToken}`)
98
98
  .reply(400, { message: 'App name already exists' });
99
99
  await expect(createAppCommand.action(options, undefined)).rejects.toThrow();
@@ -0,0 +1,36 @@
1
+ import appDeploymentsService from '../../../services/app-deployments.js';
2
+ import { withAuth } from '../../../utils/auth.js';
3
+ import { defineCommand, defineOptions } from '@robingenz/zli';
4
+ import consola from 'consola';
5
+ import { z } from 'zod';
6
+ export default defineCommand({
7
+ description: 'Get an existing app deployment.',
8
+ options: defineOptions(z.object({
9
+ appId: z.string().optional().describe('ID of the app.'),
10
+ deploymentId: z.string().optional().describe('ID of the deployment.'),
11
+ json: z.boolean().optional().describe('Output in JSON format.'),
12
+ })),
13
+ action: withAuth(async (options, args) => {
14
+ const { appId, deploymentId, json } = options;
15
+ if (!appId) {
16
+ consola.error('You must provide an app ID.');
17
+ process.exit(1);
18
+ }
19
+ if (!deploymentId) {
20
+ consola.error('You must provide a deployment ID.');
21
+ process.exit(1);
22
+ }
23
+ const deployment = await appDeploymentsService.findOne({
24
+ appId,
25
+ appDeploymentId: deploymentId,
26
+ relations: json ? 'job' : undefined,
27
+ });
28
+ if (json) {
29
+ console.log(JSON.stringify(deployment, null, 2));
30
+ }
31
+ else {
32
+ console.table(deployment);
33
+ consola.success('Deployment retrieved successfully.');
34
+ }
35
+ }),
36
+ });
@@ -0,0 +1,46 @@
1
+ import appDeploymentsService from '../../../services/app-deployments.js';
2
+ import { withAuth } from '../../../utils/auth.js';
3
+ import { isInteractive } from '../../../utils/environment.js';
4
+ import { promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
5
+ import { defineCommand, defineOptions } from '@robingenz/zli';
6
+ import consola from 'consola';
7
+ import { z } from 'zod';
8
+ export default defineCommand({
9
+ description: 'Retrieve a list of existing app deployments.',
10
+ options: defineOptions(z.object({
11
+ appId: z.string().optional().describe('ID of the app.'),
12
+ buildId: z.string().optional().describe('ID of the build to filter by.'),
13
+ channelId: z.string().optional().describe('ID of the channel to filter by.'),
14
+ destinationId: z.string().optional().describe('ID of the destination to filter by.'),
15
+ json: z.boolean().optional().describe('Output in JSON format.'),
16
+ limit: z.coerce.number().optional().describe('Limit for pagination.'),
17
+ offset: z.coerce.number().optional().describe('Offset for pagination.'),
18
+ })),
19
+ action: withAuth(async (options, args) => {
20
+ let { appId, buildId, channelId, destinationId, json, limit, offset } = options;
21
+ if (!appId) {
22
+ if (!isInteractive()) {
23
+ consola.error('You must provide an app ID when running in non-interactive environment.');
24
+ process.exit(1);
25
+ }
26
+ const organizationId = await promptOrganizationSelection();
27
+ appId = await promptAppSelection(organizationId);
28
+ }
29
+ const foundDeployments = await appDeploymentsService.findAll({
30
+ appId,
31
+ appBuildId: buildId,
32
+ appChannelId: channelId,
33
+ appDestinationId: destinationId,
34
+ limit,
35
+ offset,
36
+ relations: json ? 'job' : undefined,
37
+ });
38
+ if (json) {
39
+ console.log(JSON.stringify(foundDeployments, null, 2));
40
+ }
41
+ else {
42
+ console.table(foundDeployments);
43
+ consola.success('Deployments retrieved successfully.');
44
+ }
45
+ }),
46
+ });
@@ -0,0 +1,27 @@
1
+ import appsService from '../../services/apps.js';
2
+ import { withAuth } from '../../utils/auth.js';
3
+ import { defineCommand, defineOptions } from '@robingenz/zli';
4
+ import consola from 'consola';
5
+ import { z } from 'zod';
6
+ export default defineCommand({
7
+ description: 'Get an existing app.',
8
+ options: defineOptions(z.object({
9
+ appId: z.string().optional().describe('ID of the app.'),
10
+ json: z.boolean().optional().describe('Output in JSON format.'),
11
+ })),
12
+ action: withAuth(async (options, args) => {
13
+ const { appId, json } = options;
14
+ if (!appId) {
15
+ consola.error('You must provide an app ID.');
16
+ process.exit(1);
17
+ }
18
+ const app = await appsService.findOne({ appId });
19
+ if (json) {
20
+ console.log(JSON.stringify(app, null, 2));
21
+ }
22
+ else {
23
+ console.table(app);
24
+ consola.success('App retrieved successfully.');
25
+ }
26
+ }),
27
+ });
@@ -0,0 +1,38 @@
1
+ import appsService from '../../services/apps.js';
2
+ import { withAuth } from '../../utils/auth.js';
3
+ import { isInteractive } from '../../utils/environment.js';
4
+ import { promptOrganizationSelection } from '../../utils/prompt.js';
5
+ import { defineCommand, defineOptions } from '@robingenz/zli';
6
+ import consola from 'consola';
7
+ import { z } from 'zod';
8
+ export default defineCommand({
9
+ description: 'Retrieve a list of existing apps.',
10
+ options: defineOptions(z.object({
11
+ json: z.boolean().optional().describe('Output in JSON format.'),
12
+ limit: z.coerce.number().optional().describe('Limit for pagination.'),
13
+ offset: z.coerce.number().optional().describe('Offset for pagination.'),
14
+ organizationId: z.string().optional().describe('ID of the organization.'),
15
+ })),
16
+ action: withAuth(async (options, args) => {
17
+ let { json, limit, offset, organizationId } = options;
18
+ if (!organizationId) {
19
+ if (!isInteractive()) {
20
+ consola.error('You must provide an organization ID when running in non-interactive environment.');
21
+ process.exit(1);
22
+ }
23
+ organizationId = await promptOrganizationSelection();
24
+ }
25
+ const foundApps = await appsService.findAll({
26
+ organizationId,
27
+ limit,
28
+ offset,
29
+ });
30
+ if (json) {
31
+ console.log(JSON.stringify(foundApps, null, 2));
32
+ }
33
+ else {
34
+ console.table(foundApps);
35
+ consola.success('Apps retrieved successfully.');
36
+ }
37
+ }),
38
+ });
@@ -0,0 +1,27 @@
1
+ import organizationsService from '../../services/organizations.js';
2
+ import { withAuth } from '../../utils/auth.js';
3
+ import { defineCommand, defineOptions } from '@robingenz/zli';
4
+ import consola from 'consola';
5
+ import { z } from 'zod';
6
+ export default defineCommand({
7
+ description: 'Get an existing organization.',
8
+ options: defineOptions(z.object({
9
+ json: z.boolean().optional().describe('Output in JSON format.'),
10
+ organizationId: z.string().optional().describe('ID of the organization.'),
11
+ })),
12
+ action: withAuth(async (options, args) => {
13
+ const { json, organizationId } = options;
14
+ if (!organizationId) {
15
+ consola.error('You must provide an organization ID.');
16
+ process.exit(1);
17
+ }
18
+ const organization = await organizationsService.findOne({ organizationId });
19
+ if (json) {
20
+ console.log(JSON.stringify(organization, null, 2));
21
+ }
22
+ else {
23
+ console.table(organization);
24
+ consola.success('Organization retrieved successfully.');
25
+ }
26
+ }),
27
+ });
@@ -0,0 +1,27 @@
1
+ import organizationsService from '../../services/organizations.js';
2
+ import { withAuth } from '../../utils/auth.js';
3
+ import { defineCommand, defineOptions } from '@robingenz/zli';
4
+ import consola from 'consola';
5
+ import { z } from 'zod';
6
+ export default defineCommand({
7
+ description: 'Retrieve a list of existing organizations.',
8
+ options: defineOptions(z.object({
9
+ json: z.boolean().optional().describe('Output in JSON format.'),
10
+ limit: z.coerce.number().optional().describe('Limit for pagination.'),
11
+ offset: z.coerce.number().optional().describe('Offset for pagination.'),
12
+ })),
13
+ action: withAuth(async (options, args) => {
14
+ const { json, limit, offset } = options;
15
+ const foundOrganizations = await organizationsService.findAll({
16
+ limit,
17
+ offset,
18
+ });
19
+ if (json) {
20
+ console.log(JSON.stringify(foundOrganizations, null, 2));
21
+ }
22
+ else {
23
+ console.table(foundOrganizations);
24
+ consola.success('Organizations retrieved successfully.');
25
+ }
26
+ }),
27
+ });
package/dist/index.js CHANGED
@@ -23,11 +23,15 @@ const config = defineConfig({
23
23
  doctor: await import('./commands/doctor.js').then((mod) => mod.default),
24
24
  'apps:create': await import('./commands/apps/create.js').then((mod) => mod.default),
25
25
  'apps:delete': await import('./commands/apps/delete.js').then((mod) => mod.default),
26
+ 'apps:get': await import('./commands/apps/get.js').then((mod) => mod.default),
26
27
  'apps:link': await import('./commands/apps/link.js').then((mod) => mod.default),
28
+ 'apps:list': await import('./commands/apps/list.js').then((mod) => mod.default),
27
29
  'apps:transfer': await import('./commands/apps/transfer.js').then((mod) => mod.default),
28
30
  'apps:unlink': await import('./commands/apps/unlink.js').then((mod) => mod.default),
29
31
  'apps:builds:cancel': await import('./commands/apps/builds/cancel.js').then((mod) => mod.default),
30
32
  'apps:builds:create': await import('./commands/apps/builds/create.js').then((mod) => mod.default),
33
+ 'apps:builds:get': await import('./commands/apps/builds/get.js').then((mod) => mod.default),
34
+ 'apps:builds:list': await import('./commands/apps/builds/list.js').then((mod) => mod.default),
31
35
  'apps:builds:logs': await import('./commands/apps/builds/logs.js').then((mod) => mod.default),
32
36
  'apps:builds:download': await import('./commands/apps/builds/download.js').then((mod) => mod.default),
33
37
  'apps:bundles:create': await import('./commands/apps/bundles/create.js').then((mod) => mod.default),
@@ -47,6 +51,8 @@ const config = defineConfig({
47
51
  'apps:channels:update': await import('./commands/apps/channels/update.js').then((mod) => mod.default),
48
52
  'apps:deployments:create': await import('./commands/apps/deployments/create.js').then((mod) => mod.default),
49
53
  'apps:deployments:cancel': await import('./commands/apps/deployments/cancel.js').then((mod) => mod.default),
54
+ 'apps:deployments:get': await import('./commands/apps/deployments/get.js').then((mod) => mod.default),
55
+ 'apps:deployments:list': await import('./commands/apps/deployments/list.js').then((mod) => mod.default),
50
56
  'apps:deployments:logs': await import('./commands/apps/deployments/logs.js').then((mod) => mod.default),
51
57
  'apps:destinations:create': await import('./commands/apps/destinations/create.js').then((mod) => mod.default),
52
58
  'apps:destinations:delete': await import('./commands/apps/destinations/delete.js').then((mod) => mod.default),
@@ -73,6 +79,8 @@ const config = defineConfig({
73
79
  'apps:liveupdates:generatemanifest': await import('./commands/apps/liveupdates/generate-manifest.js').then((mod) => mod.default),
74
80
  'manifests:generate': await import('./commands/manifests/generate.js').then((mod) => mod.default),
75
81
  'organizations:create': await import('./commands/organizations/create.js').then((mod) => mod.default),
82
+ 'organizations:get': await import('./commands/organizations/get.js').then((mod) => mod.default),
83
+ 'organizations:list': await import('./commands/organizations/list.js').then((mod) => mod.default),
76
84
  },
77
85
  });
78
86
  const captureException = async (error) => {
@@ -16,12 +16,21 @@ class AppBuildsServiceImpl {
16
16
  }
17
17
  async findAll(dto) {
18
18
  const params = {};
19
- if (dto.platform) {
20
- params.platform = dto.platform;
19
+ if (dto.limit !== undefined) {
20
+ params.limit = dto.limit.toString();
21
21
  }
22
22
  if (dto.numberAsString) {
23
23
  params.numberAsString = dto.numberAsString;
24
24
  }
25
+ if (dto.offset !== undefined) {
26
+ params.offset = dto.offset.toString();
27
+ }
28
+ if (dto.platform) {
29
+ params.platform = dto.platform;
30
+ }
31
+ if (dto.relations) {
32
+ params.relations = dto.relations;
33
+ }
25
34
  const response = await this.httpClient.get(`/v1/apps/${dto.appId}/builds`, {
26
35
  headers: {
27
36
  Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
@@ -15,12 +15,21 @@ class AppDeploymentsServiceImpl {
15
15
  }
16
16
  async findAll(dto) {
17
17
  const params = {};
18
+ if (dto.appBuildId) {
19
+ params.appBuildId = dto.appBuildId;
20
+ }
18
21
  if (dto.appChannelId) {
19
22
  params.appChannelId = dto.appChannelId;
20
23
  }
21
- if (dto.limit) {
24
+ if (dto.appDestinationId) {
25
+ params.appDestinationId = dto.appDestinationId;
26
+ }
27
+ if (dto.limit !== undefined) {
22
28
  params.limit = dto.limit.toString();
23
29
  }
30
+ if (dto.offset !== undefined) {
31
+ params.offset = dto.offset.toString();
32
+ }
24
33
  if (dto.relations) {
25
34
  params.relations = dto.relations;
26
35
  }
@@ -27,6 +27,9 @@ class AppsServiceImpl {
27
27
  if (dto.limit !== undefined) {
28
28
  params.append('limit', dto.limit.toString());
29
29
  }
30
+ if (dto.offset !== undefined) {
31
+ params.append('offset', dto.offset.toString());
32
+ }
30
33
  const response = await this.httpClient.get(`/v1/apps?${params.toString()}`, {
31
34
  headers: {
32
35
  Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
@@ -13,11 +13,27 @@ class OrganizationsServiceImpl {
13
13
  });
14
14
  return response.data;
15
15
  }
16
- async findAll() {
16
+ async findAll(dto) {
17
+ const params = {};
18
+ if (dto?.limit !== undefined) {
19
+ params.limit = dto.limit.toString();
20
+ }
21
+ if (dto?.offset !== undefined) {
22
+ params.offset = dto.offset.toString();
23
+ }
17
24
  const response = await this.httpClient.get(`/v1/organizations`, {
18
25
  headers: {
19
26
  Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
20
27
  },
28
+ params,
29
+ });
30
+ return response.data;
31
+ }
32
+ async findOne(dto) {
33
+ const response = await this.httpClient.get(`/v1/organizations/${dto.organizationId}`, {
34
+ headers: {
35
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
36
+ },
21
37
  });
22
38
  return response.data;
23
39
  }
@@ -6,6 +6,12 @@ export class UserError extends Error {
6
6
  this.name = 'UserError';
7
7
  }
8
8
  }
9
+ export const getCodeFromUnknownError = (error) => {
10
+ if (error instanceof Error && 'code' in error && typeof error.code === 'string') {
11
+ return error.code;
12
+ }
13
+ return undefined;
14
+ };
9
15
  export const getMessageFromUnknownError = (error) => {
10
16
  let message = 'An unknown error has occurred.';
11
17
  if (error instanceof AxiosError) {
@@ -1,11 +1,38 @@
1
+ import { getCodeFromUnknownError, UserError } from '../utils/error.js';
2
+ import { homedir } from 'node:os';
3
+ import { resolve } from 'node:path';
1
4
  import { readUser, writeUser } from 'rc9';
2
5
  class UserConfigImpl {
3
6
  file = '.capawesome';
4
7
  read() {
5
- return readUser({ name: this.file });
8
+ try {
9
+ return readUser({ name: this.file });
10
+ }
11
+ catch (error) {
12
+ this.rethrow(error);
13
+ }
6
14
  }
7
15
  write(config) {
8
- writeUser(config, { name: this.file });
16
+ try {
17
+ writeUser(config, { name: this.file });
18
+ }
19
+ catch (error) {
20
+ this.rethrow(error);
21
+ }
22
+ }
23
+ rethrow(error) {
24
+ const code = getCodeFromUnknownError(error);
25
+ if (code === 'EACCES' || code === 'EPERM') {
26
+ const path = error instanceof Error && 'path' in error && typeof error.path === 'string'
27
+ ? error.path
28
+ : this.getResolvedPath();
29
+ throw new UserError(`Permission denied accessing ${path}. The file may be owned by another user (e.g. from a previous run with sudo). ` +
30
+ `Try running \`sudo chown $USER ${path}\` or \`rm ${path}\`, then retry.`);
31
+ }
32
+ throw error;
33
+ }
34
+ getResolvedPath() {
35
+ return resolve(process.env.XDG_CONFIG_HOME || homedir(), this.file);
9
36
  }
10
37
  }
11
38
  const userConfig = new UserConfigImpl();
@@ -0,0 +1,93 @@
1
+ import { homedir } from 'node:os';
2
+ import { resolve } from 'node:path';
3
+ import { readUser, writeUser } from 'rc9';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { UserError } from './error.js';
6
+ import userConfig from './user-config.js';
7
+ vi.mock('rc9');
8
+ describe('userConfig', () => {
9
+ const mockReadUser = vi.mocked(readUser);
10
+ const mockWriteUser = vi.mocked(writeUser);
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+ const createFsError = (code, path) => Object.assign(new Error(code), { code, path });
15
+ describe('read', () => {
16
+ it('should return the config on success', () => {
17
+ mockReadUser.mockReturnValue({ token: 'abc' });
18
+ expect(userConfig.read()).toEqual({ token: 'abc' });
19
+ });
20
+ it('should throw UserError on EACCES with the failing path in the message', () => {
21
+ const error = createFsError('EACCES', '/home/user/.capawesome');
22
+ mockReadUser.mockImplementation(() => {
23
+ throw error;
24
+ });
25
+ expect(() => userConfig.read()).toThrow(UserError);
26
+ expect(() => userConfig.read()).toThrow(/\/home\/user\/\.capawesome/);
27
+ });
28
+ it('should throw UserError on EPERM', () => {
29
+ const error = createFsError('EPERM', '/home/user/.capawesome');
30
+ mockReadUser.mockImplementation(() => {
31
+ throw error;
32
+ });
33
+ expect(() => userConfig.read()).toThrow(UserError);
34
+ });
35
+ it('should fall back to the resolved home path when the error has no path', () => {
36
+ const error = Object.assign(new Error('EACCES'), { code: 'EACCES' });
37
+ mockReadUser.mockImplementation(() => {
38
+ throw error;
39
+ });
40
+ const expectedPath = resolve(process.env.XDG_CONFIG_HOME || homedir(), '.capawesome');
41
+ expect(() => userConfig.read()).toThrow(expectedPath);
42
+ });
43
+ it('should rethrow non-access errors as-is', () => {
44
+ const error = new Error('something else');
45
+ mockReadUser.mockImplementation(() => {
46
+ throw error;
47
+ });
48
+ let caught;
49
+ try {
50
+ userConfig.read();
51
+ }
52
+ catch (e) {
53
+ caught = e;
54
+ }
55
+ expect(caught).toBe(error);
56
+ });
57
+ });
58
+ describe('write', () => {
59
+ it('should call writeUser on success', () => {
60
+ userConfig.write({ token: 'abc' });
61
+ expect(mockWriteUser).toHaveBeenCalledWith({ token: 'abc' }, { name: '.capawesome' });
62
+ });
63
+ it('should throw UserError on EACCES with the failing path in the message', () => {
64
+ const error = createFsError('EACCES', '/home/user/.capawesome');
65
+ mockWriteUser.mockImplementation(() => {
66
+ throw error;
67
+ });
68
+ expect(() => userConfig.write({})).toThrow(UserError);
69
+ expect(() => userConfig.write({})).toThrow(/\/home\/user\/\.capawesome/);
70
+ });
71
+ it('should throw UserError on EPERM', () => {
72
+ const error = createFsError('EPERM', '/home/user/.capawesome');
73
+ mockWriteUser.mockImplementation(() => {
74
+ throw error;
75
+ });
76
+ expect(() => userConfig.write({})).toThrow(UserError);
77
+ });
78
+ it('should rethrow non-access errors as-is', () => {
79
+ const error = new Error('something else');
80
+ mockWriteUser.mockImplementation(() => {
81
+ throw error;
82
+ });
83
+ let caught;
84
+ try {
85
+ userConfig.write({});
86
+ }
87
+ catch (e) {
88
+ caught = e;
89
+ }
90
+ expect(caught).toBe(error);
91
+ });
92
+ });
93
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "4.8.3",
3
+ "version": "4.9.0",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {