@capawesome/cli 4.15.0 → 4.16.1

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,26 @@
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.16.1](https://github.com/capawesome-team/cli/compare/v4.16.0...v4.16.1) (2026-07-12)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * 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))
11
+
12
+ ## [4.16.0](https://github.com/capawesome-team/cli/compare/v4.15.0...v4.16.0) (2026-07-12)
13
+
14
+
15
+ ### Features
16
+
17
+ * add `apps:builds:share` command and `--share` flag on `apps:builds:create` ([#178](https://github.com/capawesome-team/cli/issues/178)) ([05588c3](https://github.com/capawesome-team/cli/commit/05588c3db4fab2843f54fa1180de9e37c0d9b518))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * correct success message when using `--detached` ([#179](https://github.com/capawesome-team/cli/issues/179)) ([33cd33b](https://github.com/capawesome-team/cli/commit/33cd33b5266353fe62b8ba9858f58c7f13dd9e78))
23
+ * run deployment before `--json` output in `apps:builds:create` ([#182](https://github.com/capawesome-team/cli/issues/182)) ([1d693fb](https://github.com/capawesome-team/cli/commit/1d693fb1cafccd02237dd61c94b81e9e62b79f36))
24
+
5
25
  ## [4.15.0](https://github.com/capawesome-team/cli/compare/v4.14.0...v4.15.0) (2026-06-23)
6
26
 
7
27
 
@@ -4,6 +4,7 @@ import appBuildsService from '../../../services/app-builds.js';
4
4
  import appCertificatesService from '../../../services/app-certificates.js';
5
5
  import appEnvironmentsService from '../../../services/app-environments.js';
6
6
  import appsService from '../../../services/apps.js';
7
+ import { getAppBuildShareUrls } from '../../../utils/app-build-shares.js';
7
8
  import { parseKeyValuePairs } from '../../../utils/app-environments.js';
8
9
  import { withAuth } from '../../../utils/auth.js';
9
10
  import { createBufferFromPath } from '../../../utils/buffer.js';
@@ -62,6 +63,19 @@ export default defineCommand({
62
63
  })
63
64
  .optional()
64
65
  .describe('The platform for the build. Supported values are `ios`, `android`, and `web`.'),
66
+ share: z.boolean().optional().describe('Create a public share link for the build after it succeeds.'),
67
+ shareDescription: z
68
+ .string()
69
+ .optional()
70
+ .describe('Additional information for testers, e.g. what to test. Requires --share.'),
71
+ shareExpiresInDays: z.coerce
72
+ .number()
73
+ .int()
74
+ .min(1, {
75
+ message: 'Expires in days must be a positive integer.',
76
+ })
77
+ .optional()
78
+ .describe('Number of days until the share link expires. Requires --share.'),
65
79
  stack: z
66
80
  .enum(['macos-sequoia', 'macos-tahoe'], {
67
81
  message: 'Build stack must be either `macos-sequoia` or `macos-tahoe`.',
@@ -104,6 +118,16 @@ export default defineCommand({
104
118
  consola.error('The --detached flag cannot be used with --failure-summary.');
105
119
  process.exit(1);
106
120
  }
121
+ // Validate that detached flag cannot be used with share
122
+ if (options.detached && options.share) {
123
+ consola.error('The --detached flag cannot be used with --share. Sharing requires waiting for completion.');
124
+ process.exit(1);
125
+ }
126
+ // Validate that share sub-flags cannot be used without share
127
+ if (!options.share && (options.shareDescription !== undefined || options.shareExpiresInDays !== undefined)) {
128
+ consola.error('The --share-description and --share-expires-in-days flags require --share.');
129
+ process.exit(1);
130
+ }
107
131
  // Validate that channel and destination cannot be used together
108
132
  if (options.channel && options.destination) {
109
133
  consola.error('The --channel and --destination flags cannot be used together.');
@@ -393,11 +417,40 @@ export default defineCommand({
393
417
  filePath: typeof options.zip === 'string' ? options.zip : undefined,
394
418
  });
395
419
  }
420
+ // Create a public share link if requested
421
+ let appBuildShare;
422
+ if (options.share) {
423
+ const expiresAt = options.shareExpiresInDays !== undefined
424
+ ? new Date(Date.now() + options.shareExpiresInDays * 24 * 60 * 60 * 1000).toISOString()
425
+ : undefined;
426
+ const createdShare = await appBuildsService.createShare({
427
+ appId,
428
+ appBuildId: response.id,
429
+ description: options.shareDescription,
430
+ expiresAt,
431
+ });
432
+ const shareUrls = await getAppBuildShareUrls(createdShare.id);
433
+ appBuildShare = {
434
+ id: createdShare.id,
435
+ qrCodeUrl: shareUrls.qrCodeUrl,
436
+ webUrl: shareUrls.webUrl,
437
+ expiresAt: createdShare.expiresAt,
438
+ };
439
+ if (!json) {
440
+ consola.success('Build shared successfully.');
441
+ consola.info(`Share URL: ${shareUrls.webUrl}`);
442
+ }
443
+ }
444
+ // Create deployment if channel or destination is set
445
+ if (options.channel || options.destination) {
446
+ await (await import('../../../commands/apps/deployments/create.js').then((mod) => mod.default)).action({ appId, buildId: response.id, channel: options.channel, destination: options.destination }, undefined);
447
+ }
396
448
  // Output JSON if json flag is set
397
449
  if (json) {
398
450
  console.log(JSON.stringify({
399
451
  id: response.id,
400
452
  numberAsString: response.numberAsString,
453
+ ...(appBuildShare ? { appBuildShare } : {}),
401
454
  }, null, 2));
402
455
  }
403
456
  }
@@ -412,13 +465,9 @@ export default defineCommand({
412
465
  consola.info(`Build ID: ${response.id}`);
413
466
  consola.info(`Build Number: ${response.numberAsString}`);
414
467
  consola.info(`Build URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/builds/${response.id}`);
415
- consola.success(`Build completed successfully.`);
468
+ consola.success('Build started successfully.');
416
469
  }
417
470
  }
418
- // Create deployment if channel or destination is set
419
- if (options.channel || options.destination) {
420
- await (await import('../../../commands/apps/deployments/create.js').then((mod) => mod.default)).action({ appId, buildId: response.id, channel: options.channel, destination: options.destination }, undefined);
421
- }
422
471
  }),
423
472
  });
424
473
  /**
@@ -0,0 +1,121 @@
1
+ import { DEFAULT_API_BASE_URL, DEFAULT_CONSOLE_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 createCommand from './create.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('@/utils/job.js');
13
+ vi.mock('consola');
14
+ vi.mock('@/utils/environment.js', () => ({
15
+ isInteractive: () => false,
16
+ }));
17
+ describe('apps-builds-create', () => {
18
+ const mockUserConfig = vi.mocked(userConfig);
19
+ const mockAuthorizationService = vi.mocked(authorizationService);
20
+ const mockConsola = vi.mocked(consola);
21
+ const testToken = 'test-token';
22
+ const appId = '00000000-0000-0000-0000-000000000001';
23
+ const buildId = '00000000-0000-0000-0000-000000000002';
24
+ const validAppId = '11111111-1111-4111-8111-111111111111';
25
+ const shareId = 'share-abc';
26
+ beforeEach(async () => {
27
+ vi.clearAllMocks();
28
+ mockUserConfig.read.mockReturnValue({ token: testToken });
29
+ mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
30
+ mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue(testToken);
31
+ // Mock waitForJobCompletion to resolve immediately as succeeded
32
+ const jobUtils = await import('../../../utils/job.js');
33
+ vi.mocked(jobUtils.waitForJobCompletion).mockResolvedValue({
34
+ id: 'job-1',
35
+ status: 'succeeded',
36
+ createdAt: '2024-01-01T00:00:00Z',
37
+ });
38
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
39
+ throw new Error(`Process exited with code ${code}`);
40
+ });
41
+ vi.spyOn(console, 'log').mockImplementation(() => { });
42
+ });
43
+ afterEach(() => {
44
+ nock.cleanAll();
45
+ vi.restoreAllMocks();
46
+ });
47
+ it('should create a share and merge it into the JSON output', async () => {
48
+ const options = { appId, platform: 'web', gitRef: 'main', share: true, json: true };
49
+ const buildScope = nock(DEFAULT_API_BASE_URL)
50
+ .post(`/v1/apps/${appId}/builds`)
51
+ .matchHeader('Authorization', `Bearer ${testToken}`)
52
+ .reply(201, { id: buildId, jobId: 'job-1', numberAsString: '42' });
53
+ const findScope = nock(DEFAULT_API_BASE_URL)
54
+ .get(`/v1/apps/${appId}/builds/${buildId}`)
55
+ .query({ relations: 'appBuildArtifacts' })
56
+ .reply(200, { id: buildId, appBuildArtifacts: [] });
57
+ const shareScope = nock(DEFAULT_API_BASE_URL)
58
+ .post(`/v1/apps/${appId}/builds/${buildId}/shares`, {})
59
+ .matchHeader('Authorization', `Bearer ${testToken}`)
60
+ .reply(201, {
61
+ id: shareId,
62
+ appBuildId: buildId,
63
+ description: null,
64
+ expiresAt: null,
65
+ createdAt: '2024-01-01T00:00:00Z',
66
+ updatedAt: '2024-01-01T00:00:00Z',
67
+ });
68
+ await createCommand.action(options, undefined);
69
+ expect(buildScope.isDone()).toBe(true);
70
+ expect(findScope.isDone()).toBe(true);
71
+ expect(shareScope.isDone()).toBe(true);
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
+ expect(console.log).toHaveBeenCalledWith(JSON.stringify({
75
+ id: buildId,
76
+ numberAsString: '42',
77
+ appBuildShare: { id: shareId, qrCodeUrl, webUrl, expiresAt: null },
78
+ }, null, 2));
79
+ });
80
+ it('should reject --share combined with --detached', async () => {
81
+ const options = { appId, platform: 'web', gitRef: 'main', share: true, detached: true };
82
+ await expect(createCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
83
+ expect(mockConsola.error).toHaveBeenCalledWith('The --detached flag cannot be used with --share. Sharing requires waiting for completion.');
84
+ });
85
+ it('should reject --share-description without --share', async () => {
86
+ const options = { appId, platform: 'web', gitRef: 'main', shareDescription: 'What to test' };
87
+ await expect(createCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
88
+ expect(mockConsola.error).toHaveBeenCalledWith('The --share-description and --share-expires-in-days flags require --share.');
89
+ });
90
+ it('should reject --share-expires-in-days without --share', async () => {
91
+ const options = { appId, platform: 'web', gitRef: 'main', shareExpiresInDays: 7 };
92
+ await expect(createCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
93
+ expect(mockConsola.error).toHaveBeenCalledWith('The --share-description and --share-expires-in-days flags require --share.');
94
+ });
95
+ it('should reject a non-positive shareExpiresInDays value', () => {
96
+ const schema = createCommand.options?.schema;
97
+ for (const shareExpiresInDays of [0, -1]) {
98
+ const result = schema?.safeParse({
99
+ appId: validAppId,
100
+ platform: 'web',
101
+ gitRef: 'main',
102
+ share: true,
103
+ shareExpiresInDays,
104
+ });
105
+ expect(result?.success).toBe(false);
106
+ expect(result?.error?.issues[0]?.message).toBe('Expires in days must be a positive integer.');
107
+ }
108
+ });
109
+ it('should accept a positive shareExpiresInDays value', () => {
110
+ const schema = createCommand.options?.schema;
111
+ const result = schema?.safeParse({
112
+ appId: validAppId,
113
+ platform: 'web',
114
+ gitRef: 'main',
115
+ share: true,
116
+ shareExpiresInDays: 7,
117
+ });
118
+ expect(result?.success).toBe(true);
119
+ expect(result?.data?.shareExpiresInDays).toBe(7);
120
+ });
121
+ });
@@ -0,0 +1,105 @@
1
+ import appBuildsService from '../../../services/app-builds.js';
2
+ import { getAppBuildShareUrls } from '../../../utils/app-build-shares.js';
3
+ import { withAuth } from '../../../utils/auth.js';
4
+ import { isInteractive } from '../../../utils/environment.js';
5
+ import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
6
+ import { defineCommand, defineOptions } from '@robingenz/zli';
7
+ import consola from 'consola';
8
+ import { z } from 'zod';
9
+ export default defineCommand({
10
+ description: 'Create a public share link for an app build.',
11
+ options: defineOptions(z.object({
12
+ appId: z
13
+ .uuid({
14
+ message: 'App ID must be a UUID.',
15
+ })
16
+ .optional()
17
+ .describe('App ID the build belongs to.'),
18
+ buildId: z
19
+ .uuid({
20
+ message: 'Build ID must be a UUID.',
21
+ })
22
+ .optional()
23
+ .describe('Build ID to share.'),
24
+ buildNumber: z.string().optional().describe('Build number to share (e.g., "1", "42").'),
25
+ description: z.string().optional().describe('Additional information for testers, e.g. what to test.'),
26
+ expiresInDays: z.coerce
27
+ .number()
28
+ .int()
29
+ .min(1, {
30
+ message: 'Expires in days must be a positive integer.',
31
+ })
32
+ .optional()
33
+ .describe('Number of days until the share link expires.'),
34
+ json: z.boolean().optional().describe('Output in JSON format.'),
35
+ })),
36
+ action: withAuth(async (options) => {
37
+ let { appId, buildId } = options;
38
+ const { buildNumber, description, expiresInDays, json } = options;
39
+ // Prompt for app ID if not provided
40
+ if (!appId) {
41
+ if (!isInteractive()) {
42
+ consola.error('You must provide an app ID when running in non-interactive environment.');
43
+ process.exit(1);
44
+ }
45
+ const organizationId = await promptOrganizationSelection();
46
+ appId = await promptAppSelection(organizationId);
47
+ }
48
+ // Convert build number to build ID if provided
49
+ if (!buildId && buildNumber) {
50
+ const builds = await appBuildsService.findAll({ appId, numberAsString: buildNumber });
51
+ if (builds.length === 0) {
52
+ consola.error(`Build #${buildNumber} not found.`);
53
+ process.exit(1);
54
+ }
55
+ buildId = builds[0]?.id;
56
+ }
57
+ // Prompt for build ID if not provided
58
+ if (!buildId) {
59
+ if (!isInteractive()) {
60
+ consola.error('You must provide a build ID when running in non-interactive environment.');
61
+ process.exit(1);
62
+ }
63
+ const builds = await appBuildsService.findAll({ appId });
64
+ if (builds.length === 0) {
65
+ consola.error('No builds found for this app.');
66
+ process.exit(1);
67
+ }
68
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
69
+ buildId = await prompt('Select the build you want to share:', {
70
+ type: 'select',
71
+ options: builds.map((build) => ({
72
+ label: `Build #${build.numberAsString || build.id} (${build.platform} - ${build.type})`,
73
+ value: build.id,
74
+ })),
75
+ });
76
+ if (!buildId) {
77
+ consola.error('You must select a build to share.');
78
+ process.exit(1);
79
+ }
80
+ }
81
+ // Ensure the build has succeeded before creating a share
82
+ const build = await appBuildsService.findOne({ appId, appBuildId: buildId, relations: 'job' });
83
+ if (build.job?.status !== 'succeeded') {
84
+ consola.error('The build has not succeeded yet. Only successful builds can be shared.');
85
+ process.exit(1);
86
+ }
87
+ // Compute the expiration date if provided
88
+ const expiresAt = expiresInDays !== undefined
89
+ ? new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000).toISOString()
90
+ : undefined;
91
+ const share = await appBuildsService.createShare({ appId, appBuildId: buildId, description, expiresAt });
92
+ const { qrCodeUrl, webUrl } = await getAppBuildShareUrls(share.id);
93
+ if (json) {
94
+ console.log(JSON.stringify({ id: share.id, qrCodeUrl, webUrl, expiresAt: share.expiresAt }, null, 2));
95
+ }
96
+ else {
97
+ consola.success('Build shared successfully.');
98
+ consola.info(`Share URL: ${webUrl}`);
99
+ consola.info(`QR Code URL: ${qrCodeUrl}`);
100
+ if (share.expiresAt) {
101
+ consola.info(`Expires At: ${share.expiresAt}`);
102
+ }
103
+ }
104
+ }),
105
+ });
@@ -0,0 +1,130 @@
1
+ import { DEFAULT_API_BASE_URL, DEFAULT_CONSOLE_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 shareCommand from './share.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-share', () => {
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 validAppId = '11111111-1111-4111-8111-111111111111';
24
+ const validBuildId = '22222222-2222-4222-8222-222222222222';
25
+ const shareId = 'share-abc';
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ mockUserConfig.read.mockReturnValue({ token: testToken });
29
+ mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
30
+ mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue(testToken);
31
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
32
+ throw new Error(`Process exited with code ${code}`);
33
+ });
34
+ vi.spyOn(console, 'log').mockImplementation(() => { });
35
+ });
36
+ afterEach(() => {
37
+ nock.cleanAll();
38
+ vi.restoreAllMocks();
39
+ });
40
+ it('should create a share and output the exact JSON shape', async () => {
41
+ const options = { appId, buildId, json: true };
42
+ const buildScope = nock(DEFAULT_API_BASE_URL)
43
+ .get(`/v1/apps/${appId}/builds/${buildId}`)
44
+ .query({ relations: 'job' })
45
+ .matchHeader('Authorization', `Bearer ${testToken}`)
46
+ .reply(200, { id: buildId, job: { id: 'job-1', status: 'succeeded' } });
47
+ const shareScope = nock(DEFAULT_API_BASE_URL)
48
+ .post(`/v1/apps/${appId}/builds/${buildId}/shares`, {})
49
+ .matchHeader('Authorization', `Bearer ${testToken}`)
50
+ .reply(201, {
51
+ id: shareId,
52
+ appBuildId: buildId,
53
+ description: null,
54
+ expiresAt: null,
55
+ createdAt: '2024-01-01T00:00:00Z',
56
+ updatedAt: '2024-01-01T00:00:00Z',
57
+ });
58
+ await shareCommand.action(options, undefined);
59
+ expect(buildScope.isDone()).toBe(true);
60
+ expect(shareScope.isDone()).toBe(true);
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
+ expect(mockConsola.success).not.toHaveBeenCalled();
65
+ });
66
+ it('should compute expiresAt from expiresInDays', async () => {
67
+ const expiresInDays = 7;
68
+ const options = { appId, buildId, expiresInDays, json: true };
69
+ nock(DEFAULT_API_BASE_URL)
70
+ .get(`/v1/apps/${appId}/builds/${buildId}`)
71
+ .query({ relations: 'job' })
72
+ .reply(200, { id: buildId, job: { id: 'job-1', status: 'succeeded' } });
73
+ let capturedBody = {};
74
+ nock(DEFAULT_API_BASE_URL)
75
+ .post(`/v1/apps/${appId}/builds/${buildId}/shares`, (body) => {
76
+ capturedBody = body;
77
+ return true;
78
+ })
79
+ .reply(201, {
80
+ id: shareId,
81
+ appBuildId: buildId,
82
+ description: null,
83
+ expiresAt: capturedBody.expiresAt ?? null,
84
+ createdAt: '2024-01-01T00:00:00Z',
85
+ updatedAt: '2024-01-01T00:00:00Z',
86
+ });
87
+ const before = Date.now();
88
+ await shareCommand.action(options, undefined);
89
+ const after = Date.now();
90
+ expect(capturedBody.expiresAt).toBeDefined();
91
+ const expiresAtMs = new Date(capturedBody.expiresAt).getTime();
92
+ const millisPerDay = 24 * 60 * 60 * 1000;
93
+ expect(expiresAtMs).toBeGreaterThanOrEqual(before + expiresInDays * millisPerDay);
94
+ expect(expiresAtMs).toBeLessThanOrEqual(after + expiresInDays * millisPerDay);
95
+ });
96
+ it('should error when the build has not succeeded', async () => {
97
+ const options = { appId, buildId, json: true };
98
+ const buildScope = nock(DEFAULT_API_BASE_URL)
99
+ .get(`/v1/apps/${appId}/builds/${buildId}`)
100
+ .query({ relations: 'job' })
101
+ .reply(200, { id: buildId, job: { id: 'job-1', status: 'in_progress' } });
102
+ await expect(shareCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
103
+ expect(buildScope.isDone()).toBe(true);
104
+ expect(mockConsola.error).toHaveBeenCalledWith('The build has not succeeded yet. Only successful builds can be shared.');
105
+ });
106
+ it('should require app ID in non-interactive mode', async () => {
107
+ const options = { buildId };
108
+ await expect(shareCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
109
+ expect(mockConsola.error).toHaveBeenCalledWith('You must provide an app ID when running in non-interactive environment.');
110
+ });
111
+ it('should require build ID in non-interactive mode', async () => {
112
+ const options = { appId };
113
+ await expect(shareCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
114
+ expect(mockConsola.error).toHaveBeenCalledWith('You must provide a build ID when running in non-interactive environment.');
115
+ });
116
+ it('should reject a non-positive expiresInDays value', () => {
117
+ const schema = shareCommand.options?.schema;
118
+ for (const expiresInDays of [0, -1]) {
119
+ const result = schema?.safeParse({ appId: validAppId, buildId: validBuildId, expiresInDays });
120
+ expect(result?.success).toBe(false);
121
+ expect(result?.error?.issues[0]?.message).toBe('Expires in days must be a positive integer.');
122
+ }
123
+ });
124
+ it('should accept a positive expiresInDays value', () => {
125
+ const schema = shareCommand.options?.schema;
126
+ const result = schema?.safeParse({ appId: validAppId, buildId: validBuildId, expiresInDays: 7 });
127
+ expect(result?.success).toBe(true);
128
+ expect(result?.data?.expiresInDays).toBe(7);
129
+ });
130
+ });
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ const config = defineConfig({
36
36
  'apps:builds:get': await import('./commands/apps/builds/get.js').then((mod) => mod.default),
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
+ 'apps:builds:share': await import('./commands/apps/builds/share.js').then((mod) => mod.default),
39
40
  'apps:builds:download': await import('./commands/apps/builds/download.js').then((mod) => mod.default),
40
41
  'apps:bundles:create': await import('./commands/apps/bundles/create.js').then((mod) => mod.default),
41
42
  'apps:bundles:delete': await import('./commands/apps/bundles/delete.js').then((mod) => mod.default),
@@ -14,6 +14,15 @@ class AppBuildsServiceImpl {
14
14
  });
15
15
  return response.data;
16
16
  }
17
+ async createShare(dto) {
18
+ const { appId, appBuildId, ...bodyData } = dto;
19
+ const response = await this.httpClient.post(`/v1/apps/${appId}/builds/${appBuildId}/shares`, bodyData, {
20
+ headers: {
21
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
22
+ },
23
+ });
24
+ return response.data;
25
+ }
17
26
  async findAll(dto) {
18
27
  const params = {};
19
28
  if (dto.limit !== undefined) {
@@ -0,0 +1,11 @@
1
+ import configService from '../services/config.js';
2
+ /**
3
+ * Build the public share page URL and the corresponding QR code image URL for an app build share.
4
+ */
5
+ export const getAppBuildShareUrls = async (shareId) => {
6
+ const consoleBaseUrl = await configService.getValueForKey('CONSOLE_BASE_URL');
7
+ const apiBaseUrl = await configService.getValueForKey('API_BASE_URL');
8
+ const webUrl = `${consoleBaseUrl}/app-build-shares/${shareId}`;
9
+ const qrCodeUrl = `${apiBaseUrl}/v1/qrcodes?content=${encodeURIComponent(webUrl)}&format=png`;
10
+ return { qrCodeUrl, webUrl };
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "4.15.0",
3
+ "version": "4.16.1",
4
4
  "description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
5
5
  "type": "module",
6
6
  "scripts": {