@capawesome/cli 4.16.0 → 4.17.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
+ ## [4.17.0](https://github.com/capawesome-team/cli/compare/v4.16.1...v4.17.0) (2026-07-12)
6
+
7
+
8
+ ### Features
9
+
10
+ * add `apps:builds:unshare` command ([#184](https://github.com/capawesome-team/cli/issues/184)) ([55ecd8e](https://github.com/capawesome-team/cli/commit/55ecd8e7892da4a70a6438b90b6e9db3470c315f))
11
+
12
+ ## [4.16.1](https://github.com/capawesome-team/cli/compare/v4.16.0...v4.16.1) (2026-07-12)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * rename share JSON output to `appBuildShare` with `webUrl` ([#183](https://github.com/capawesome-team/cli/issues/183)) ([32d8bd8](https://github.com/capawesome-team/cli/commit/32d8bd8460c40cd78d3417849007e8394ead35fd))
18
+
5
19
  ## [4.16.0](https://github.com/capawesome-team/cli/compare/v4.15.0...v4.16.0) (2026-07-12)
6
20
 
7
21
 
@@ -418,7 +418,7 @@ export default defineCommand({
418
418
  });
419
419
  }
420
420
  // Create a public share link if requested
421
- let share;
421
+ let appBuildShare;
422
422
  if (options.share) {
423
423
  const expiresAt = options.shareExpiresInDays !== undefined
424
424
  ? new Date(Date.now() + options.shareExpiresInDays * 24 * 60 * 60 * 1000).toISOString()
@@ -430,15 +430,15 @@ export default defineCommand({
430
430
  expiresAt,
431
431
  });
432
432
  const shareUrls = await getAppBuildShareUrls(createdShare.id);
433
- share = {
433
+ appBuildShare = {
434
434
  id: createdShare.id,
435
- url: shareUrls.url,
436
435
  qrCodeUrl: shareUrls.qrCodeUrl,
436
+ webUrl: shareUrls.webUrl,
437
437
  expiresAt: createdShare.expiresAt,
438
438
  };
439
439
  if (!json) {
440
440
  consola.success('Build shared successfully.');
441
- consola.info(`Share URL: ${shareUrls.url}`);
441
+ consola.info(`Share URL: ${shareUrls.webUrl}`);
442
442
  }
443
443
  }
444
444
  // Create deployment if channel or destination is set
@@ -450,7 +450,7 @@ export default defineCommand({
450
450
  console.log(JSON.stringify({
451
451
  id: response.id,
452
452
  numberAsString: response.numberAsString,
453
- ...(share ? { share } : {}),
453
+ ...(appBuildShare ? { appBuildShare } : {}),
454
454
  }, null, 2));
455
455
  }
456
456
  }
@@ -69,12 +69,12 @@ describe('apps-builds-create', () => {
69
69
  expect(buildScope.isDone()).toBe(true);
70
70
  expect(findScope.isDone()).toBe(true);
71
71
  expect(shareScope.isDone()).toBe(true);
72
- const url = `${DEFAULT_CONSOLE_BASE_URL}/app-build-shares/${shareId}`;
73
- const qrCodeUrl = `${DEFAULT_API_BASE_URL}/v1/qrcodes?content=${encodeURIComponent(url)}&format=png`;
72
+ const webUrl = `${DEFAULT_CONSOLE_BASE_URL}/app-build-shares/${shareId}`;
73
+ const qrCodeUrl = `${DEFAULT_API_BASE_URL}/v1/qrcodes?content=${encodeURIComponent(webUrl)}&format=png`;
74
74
  expect(console.log).toHaveBeenCalledWith(JSON.stringify({
75
75
  id: buildId,
76
76
  numberAsString: '42',
77
- share: { id: shareId, url, qrCodeUrl, expiresAt: null },
77
+ appBuildShare: { id: shareId, qrCodeUrl, webUrl, expiresAt: null },
78
78
  }, null, 2));
79
79
  });
80
80
  it('should reject --share combined with --detached', async () => {
@@ -89,13 +89,13 @@ export default defineCommand({
89
89
  ? new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000).toISOString()
90
90
  : undefined;
91
91
  const share = await appBuildsService.createShare({ appId, appBuildId: buildId, description, expiresAt });
92
- const { url, qrCodeUrl } = await getAppBuildShareUrls(share.id);
92
+ const { qrCodeUrl, webUrl } = await getAppBuildShareUrls(share.id);
93
93
  if (json) {
94
- console.log(JSON.stringify({ id: share.id, url, qrCodeUrl, expiresAt: share.expiresAt }, null, 2));
94
+ console.log(JSON.stringify({ id: share.id, qrCodeUrl, webUrl, expiresAt: share.expiresAt }, null, 2));
95
95
  }
96
96
  else {
97
97
  consola.success('Build shared successfully.');
98
- consola.info(`Share URL: ${url}`);
98
+ consola.info(`Share URL: ${webUrl}`);
99
99
  consola.info(`QR Code URL: ${qrCodeUrl}`);
100
100
  if (share.expiresAt) {
101
101
  consola.info(`Expires At: ${share.expiresAt}`);
@@ -58,9 +58,9 @@ describe('apps-builds-share', () => {
58
58
  await shareCommand.action(options, undefined);
59
59
  expect(buildScope.isDone()).toBe(true);
60
60
  expect(shareScope.isDone()).toBe(true);
61
- const url = `${DEFAULT_CONSOLE_BASE_URL}/app-build-shares/${shareId}`;
62
- const qrCodeUrl = `${DEFAULT_API_BASE_URL}/v1/qrcodes?content=${encodeURIComponent(url)}&format=png`;
63
- expect(console.log).toHaveBeenCalledWith(JSON.stringify({ id: shareId, url, qrCodeUrl, expiresAt: null }, null, 2));
61
+ const webUrl = `${DEFAULT_CONSOLE_BASE_URL}/app-build-shares/${shareId}`;
62
+ const qrCodeUrl = `${DEFAULT_API_BASE_URL}/v1/qrcodes?content=${encodeURIComponent(webUrl)}&format=png`;
63
+ expect(console.log).toHaveBeenCalledWith(JSON.stringify({ id: shareId, qrCodeUrl, webUrl, expiresAt: null }, null, 2));
64
64
  expect(mockConsola.success).not.toHaveBeenCalled();
65
65
  });
66
66
  it('should compute expiresAt from expiresInDays', async () => {
@@ -0,0 +1,91 @@
1
+ import appBuildsService from '../../../services/app-builds.js';
2
+ import { withAuth } from '../../../utils/auth.js';
3
+ import { isInteractive } from '../../../utils/environment.js';
4
+ import { prompt, 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: 'Revoke the public share link of an app build.',
10
+ options: defineOptions(z.object({
11
+ appId: z
12
+ .uuid({
13
+ message: 'App ID must be a UUID.',
14
+ })
15
+ .optional()
16
+ .describe('App ID the build belongs to.'),
17
+ buildId: z
18
+ .uuid({
19
+ message: 'Build ID must be a UUID.',
20
+ })
21
+ .optional()
22
+ .describe('Build ID to unshare.'),
23
+ buildNumber: z.string().optional().describe('Build number to unshare (e.g., "1", "42").'),
24
+ yes: z.boolean().optional().describe('Skip confirmation prompt.'),
25
+ }), { y: 'yes' }),
26
+ action: withAuth(async (options) => {
27
+ let { appId, buildId } = options;
28
+ const { buildNumber } = options;
29
+ // Prompt for app ID if not provided
30
+ if (!appId) {
31
+ if (!isInteractive()) {
32
+ consola.error('You must provide an app ID when running in non-interactive environment.');
33
+ process.exit(1);
34
+ }
35
+ const organizationId = await promptOrganizationSelection();
36
+ appId = await promptAppSelection(organizationId);
37
+ }
38
+ // Convert build number to build ID if provided
39
+ if (!buildId && buildNumber) {
40
+ const builds = await appBuildsService.findAll({ appId, numberAsString: buildNumber });
41
+ if (builds.length === 0) {
42
+ consola.error(`Build #${buildNumber} not found.`);
43
+ process.exit(1);
44
+ }
45
+ buildId = builds[0]?.id;
46
+ }
47
+ // Prompt for build ID if not provided
48
+ if (!buildId) {
49
+ if (!isInteractive()) {
50
+ consola.error('You must provide a build ID when running in non-interactive environment.');
51
+ process.exit(1);
52
+ }
53
+ const builds = await appBuildsService.findAll({ appId });
54
+ if (builds.length === 0) {
55
+ consola.error('No builds found for this app.');
56
+ process.exit(1);
57
+ }
58
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
59
+ buildId = await prompt('Select the build you want to unshare:', {
60
+ type: 'select',
61
+ options: builds.map((build) => ({
62
+ label: `Build #${build.numberAsString || build.id} (${build.platform} - ${build.type})`,
63
+ value: build.id,
64
+ })),
65
+ });
66
+ if (!buildId) {
67
+ consola.error('You must select a build to unshare.');
68
+ process.exit(1);
69
+ }
70
+ }
71
+ // Find the active share
72
+ const shares = await appBuildsService.findAllShares({ appId, appBuildId: buildId });
73
+ const share = shares[0];
74
+ if (!share) {
75
+ consola.error('This build is not shared.');
76
+ process.exit(1);
77
+ }
78
+ // Confirm revocation
79
+ if (!options.yes && isInteractive()) {
80
+ const confirmed = await prompt('Are you sure you want to revoke the share link for this build?', {
81
+ type: 'confirm',
82
+ });
83
+ if (!confirmed) {
84
+ return;
85
+ }
86
+ }
87
+ // Revoke the share
88
+ await appBuildsService.deleteShare({ appId, appBuildId: buildId, id: share.id });
89
+ consola.success('Share revoked successfully.');
90
+ }),
91
+ });
@@ -0,0 +1,75 @@
1
+ import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
2
+ import authorizationService from '../../../services/authorization-service.js';
3
+ import userConfig from '../../../utils/user-config.js';
4
+ import consola from 'consola';
5
+ import nock from 'nock';
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import unshareCommand from './unshare.js';
8
+ // Mock dependencies
9
+ vi.mock('@/utils/user-config.js');
10
+ vi.mock('@/utils/prompt.js');
11
+ vi.mock('@/services/authorization-service.js');
12
+ vi.mock('consola');
13
+ vi.mock('@/utils/environment.js', () => ({
14
+ isInteractive: () => false,
15
+ }));
16
+ describe('apps-builds-unshare', () => {
17
+ const mockUserConfig = vi.mocked(userConfig);
18
+ const mockAuthorizationService = vi.mocked(authorizationService);
19
+ const mockConsola = vi.mocked(consola);
20
+ const testToken = 'test-token';
21
+ const appId = '00000000-0000-0000-0000-000000000001';
22
+ const buildId = '00000000-0000-0000-0000-000000000002';
23
+ const shareId = 'share-abc';
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ mockUserConfig.read.mockReturnValue({ token: testToken });
27
+ mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
28
+ mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue(testToken);
29
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
30
+ throw new Error(`Process exited with code ${code}`);
31
+ });
32
+ });
33
+ afterEach(() => {
34
+ nock.cleanAll();
35
+ vi.restoreAllMocks();
36
+ });
37
+ it('should revoke the active share', async () => {
38
+ const options = { appId, buildId, yes: true };
39
+ const sharesScope = nock(DEFAULT_API_BASE_URL)
40
+ .get(`/v1/apps/${appId}/builds/${buildId}/shares`)
41
+ .matchHeader('Authorization', `Bearer ${testToken}`)
42
+ .reply(200, [
43
+ {
44
+ id: shareId,
45
+ appBuildId: buildId,
46
+ description: null,
47
+ expiresAt: null,
48
+ createdAt: '2024-01-01T00:00:00Z',
49
+ updatedAt: '2024-01-01T00:00:00Z',
50
+ },
51
+ ]);
52
+ const deleteScope = nock(DEFAULT_API_BASE_URL)
53
+ .delete(`/v1/apps/${appId}/builds/${buildId}/shares/${shareId}`)
54
+ .matchHeader('Authorization', `Bearer ${testToken}`)
55
+ .reply(204);
56
+ await unshareCommand.action(options, undefined);
57
+ expect(sharesScope.isDone()).toBe(true);
58
+ expect(deleteScope.isDone()).toBe(true);
59
+ expect(mockConsola.success).toHaveBeenCalledWith('Share revoked successfully.');
60
+ });
61
+ it('should error when the build is not shared', async () => {
62
+ const options = { appId, buildId, yes: true };
63
+ nock(DEFAULT_API_BASE_URL)
64
+ .get(`/v1/apps/${appId}/builds/${buildId}/shares`)
65
+ .matchHeader('Authorization', `Bearer ${testToken}`)
66
+ .reply(200, []);
67
+ await expect(unshareCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
68
+ expect(mockConsola.error).toHaveBeenCalledWith('This build is not shared.');
69
+ });
70
+ it('should error when no app ID is provided in non-interactive environment', async () => {
71
+ const options = {};
72
+ await expect(unshareCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
73
+ expect(mockConsola.error).toHaveBeenCalledWith('You must provide an app ID when running in non-interactive environment.');
74
+ });
75
+ });
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ const config = defineConfig({
37
37
  'apps:builds:list': await import('./commands/apps/builds/list.js').then((mod) => mod.default),
38
38
  'apps:builds:logs': await import('./commands/apps/builds/logs.js').then((mod) => mod.default),
39
39
  'apps:builds:share': await import('./commands/apps/builds/share.js').then((mod) => mod.default),
40
+ 'apps:builds:unshare': await import('./commands/apps/builds/unshare.js').then((mod) => mod.default),
40
41
  'apps:builds:download': await import('./commands/apps/builds/download.js').then((mod) => mod.default),
41
42
  'apps:bundles:create': await import('./commands/apps/bundles/create.js').then((mod) => mod.default),
42
43
  'apps:bundles:delete': await import('./commands/apps/bundles/delete.js').then((mod) => mod.default),
@@ -23,6 +23,13 @@ class AppBuildsServiceImpl {
23
23
  });
24
24
  return response.data;
25
25
  }
26
+ async deleteShare(dto) {
27
+ await this.httpClient.delete(`/v1/apps/${dto.appId}/builds/${dto.appBuildId}/shares/${dto.id}`, {
28
+ headers: {
29
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
30
+ },
31
+ });
32
+ }
26
33
  async findAll(dto) {
27
34
  const params = {};
28
35
  if (dto.limit !== undefined) {
@@ -48,6 +55,14 @@ class AppBuildsServiceImpl {
48
55
  });
49
56
  return response.data;
50
57
  }
58
+ async findAllShares(dto) {
59
+ const response = await this.httpClient.get(`/v1/apps/${dto.appId}/builds/${dto.appBuildId}/shares`, {
60
+ headers: {
61
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
62
+ },
63
+ });
64
+ return response.data;
65
+ }
51
66
  async findOne(dto) {
52
67
  const { appId, appBuildId, relations } = dto;
53
68
  const params = {};
@@ -5,7 +5,7 @@ import configService from '../services/config.js';
5
5
  export const getAppBuildShareUrls = async (shareId) => {
6
6
  const consoleBaseUrl = await configService.getValueForKey('CONSOLE_BASE_URL');
7
7
  const apiBaseUrl = await configService.getValueForKey('API_BASE_URL');
8
- const url = `${consoleBaseUrl}/app-build-shares/${shareId}`;
9
- const qrCodeUrl = `${apiBaseUrl}/v1/qrcodes?content=${encodeURIComponent(url)}&format=png`;
10
- return { url, qrCodeUrl };
8
+ const webUrl = `${consoleBaseUrl}/app-build-shares/${shareId}`;
9
+ const qrCodeUrl = `${apiBaseUrl}/v1/qrcodes?content=${encodeURIComponent(webUrl)}&format=png`;
10
+ return { qrCodeUrl, webUrl };
11
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "4.16.0",
3
+ "version": "4.17.0",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {