@capawesome/cli 4.6.0 → 4.8.0-dev.efa0850.1775645973

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/commands/apps/builds/create.js +160 -128
  3. package/dist/commands/apps/bundles/create.js +4 -2
  4. package/dist/commands/apps/bundles/delete.js +2 -3
  5. package/dist/commands/apps/bundles/update.js +2 -3
  6. package/dist/commands/apps/certificates/create.js +3 -19
  7. package/dist/commands/apps/certificates/delete.js +28 -5
  8. package/dist/commands/apps/certificates/get.js +28 -5
  9. package/dist/commands/apps/certificates/update.js +3 -1
  10. package/dist/commands/apps/create.js +23 -4
  11. package/dist/commands/apps/deployments/create.js +5 -77
  12. package/dist/commands/apps/devices/forcechannel.js +9 -7
  13. package/dist/commands/apps/devices/unforcechannel.js +9 -7
  14. package/dist/commands/apps/link.js +34 -0
  15. package/dist/commands/apps/link.test.js +94 -0
  16. package/dist/commands/apps/liveupdates/bundle.js +12 -2
  17. package/dist/commands/apps/liveupdates/create.js +293 -0
  18. package/dist/commands/apps/liveupdates/create.test.js +300 -0
  19. package/dist/commands/apps/liveupdates/generate-manifest.js +17 -1
  20. package/dist/commands/apps/liveupdates/generate-manifest.test.js +21 -1
  21. package/dist/commands/apps/liveupdates/register.js +10 -15
  22. package/dist/commands/apps/liveupdates/upload.js +25 -16
  23. package/dist/commands/apps/transfer.js +47 -0
  24. package/dist/commands/apps/transfer.test.js +123 -0
  25. package/dist/commands/apps/unlink.js +35 -0
  26. package/dist/commands/apps/unlink.test.js +99 -0
  27. package/dist/commands/manifests/generate.js +1 -1
  28. package/dist/index.js +13 -5
  29. package/dist/services/app-build-sources.js +120 -0
  30. package/dist/services/app-certificates.js +0 -1
  31. package/dist/services/app-devices.js +8 -0
  32. package/dist/services/apps.js +25 -0
  33. package/dist/services/authorization-service.js +5 -1
  34. package/dist/services/jobs.js +13 -0
  35. package/dist/types/app-build-source.js +1 -0
  36. package/dist/types/index.js +1 -0
  37. package/dist/utils/custom-properties.js +22 -0
  38. package/dist/utils/error.js +6 -0
  39. package/dist/utils/file.js +12 -1
  40. package/dist/utils/git.js +92 -0
  41. package/dist/utils/git.test.js +130 -0
  42. package/dist/utils/job.js +77 -0
  43. package/dist/utils/prompt.js +1 -1
  44. package/dist/utils/zip.js +19 -2
  45. package/package.json +2 -1
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_CONSOLE_BASE_URL } from '../../config/consts.js';
1
2
  import appsService from '../../services/apps.js';
2
3
  import { withAuth } from '../../utils/auth.js';
3
4
  import { isInteractive } from '../../utils/environment.js';
@@ -8,11 +9,14 @@ import { z } from 'zod';
8
9
  export default defineCommand({
9
10
  description: 'Create a new app.',
10
11
  options: defineOptions(z.object({
12
+ json: z.boolean().optional().describe('Output in JSON format.'),
13
+ link: z.boolean().optional().describe('Connect the created app to the local git repository.'),
11
14
  name: z.string().optional().describe('Name of the app.'),
12
15
  organizationId: z.string().optional().describe('ID of the organization to create the app in.'),
13
- })),
16
+ yes: z.boolean().optional().describe('Skip all confirmation prompts.'),
17
+ }), { y: 'yes' }),
14
18
  action: withAuth(async (options, args) => {
15
- let { name, organizationId } = options;
19
+ let { json, name, organizationId } = options;
16
20
  if (!organizationId) {
17
21
  if (!isInteractive()) {
18
22
  consola.error('You must provide the organization ID when running in non-interactive environment.');
@@ -28,7 +32,22 @@ export default defineCommand({
28
32
  name = await prompt('Enter the name of the app:', { type: 'text' });
29
33
  }
30
34
  const response = await appsService.create({ name, organizationId });
31
- consola.info(`App ID: ${response.id}`);
32
- consola.success('App created successfully.');
35
+ if (!json) {
36
+ consola.info(`App ID: ${response.id}`);
37
+ consola.info(`App URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${response.id}`);
38
+ consola.success('App created successfully.');
39
+ }
40
+ let shouldLink = options.link ?? false;
41
+ if (!shouldLink && !options.yes && !json && isInteractive()) {
42
+ shouldLink = await prompt('Do you want to connect a git repository?', {
43
+ type: 'confirm',
44
+ });
45
+ }
46
+ if (shouldLink) {
47
+ await (await import('../../commands/apps/link.js').then((mod) => mod.default)).action({ appId: response.id }, undefined);
48
+ }
49
+ if (json) {
50
+ console.log(JSON.stringify({ id: response.id }, null, 2));
51
+ }
33
52
  }),
34
53
  });
@@ -2,16 +2,15 @@ import { DEFAULT_CONSOLE_BASE_URL } from '../../../config/consts.js';
2
2
  import appBuildsService from '../../../services/app-builds.js';
3
3
  import appDeploymentsService from '../../../services/app-deployments.js';
4
4
  import appDestinationsService from '../../../services/app-destinations.js';
5
- import { unescapeAnsi } from '../../../utils/ansi.js';
6
5
  import { withAuth } from '../../../utils/auth.js';
7
6
  import { isInteractive } from '../../../utils/environment.js';
7
+ import { waitForJobCompletion } from '../../../utils/job.js';
8
8
  import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
9
- import { wait } from '../../../utils/wait.js';
10
9
  import { defineCommand, defineOptions } from '@robingenz/zli';
11
10
  import consola from 'consola';
12
11
  import { z } from 'zod';
13
12
  export default defineCommand({
14
- description: 'Create a new app deployment.',
13
+ description: 'Create a new app deployment on Capawesome Cloud.',
15
14
  options: defineOptions(z.object({
16
15
  appId: z
17
16
  .uuid({
@@ -159,80 +158,9 @@ export default defineCommand({
159
158
  // Wait for deployment job to complete by default, unless --detached flag is set
160
159
  const shouldWait = !options.detached && build.platform !== 'web';
161
160
  if (shouldWait) {
162
- let lastPrintedLogNumber = 0;
163
- let isWaitingForStart = true;
164
- // Poll deployment status until completion
165
- while (true) {
166
- try {
167
- const deployment = await appDeploymentsService.findOne({
168
- appId,
169
- appDeploymentId: response.id,
170
- relations: 'job,job.jobLogs',
171
- });
172
- if (!deployment.job) {
173
- await wait(3000);
174
- continue;
175
- }
176
- const jobStatus = deployment.job.status;
177
- // Show spinner while queued or pending
178
- if (jobStatus === 'queued' || jobStatus === 'pending') {
179
- if (isWaitingForStart) {
180
- consola.start(`Waiting for deployment to start (status: ${jobStatus})...`);
181
- }
182
- await wait(3000);
183
- continue;
184
- }
185
- // Stop spinner when job moves to in_progress
186
- if (isWaitingForStart && jobStatus === 'in_progress') {
187
- isWaitingForStart = false;
188
- consola.success('Deployment started...');
189
- }
190
- // Print new logs
191
- if (deployment.job.jobLogs && deployment.job.jobLogs.length > 0) {
192
- const newLogs = deployment.job.jobLogs
193
- .filter((log) => log.number > lastPrintedLogNumber)
194
- .sort((a, b) => a.number - b.number);
195
- for (const log of newLogs) {
196
- console.log(unescapeAnsi(log.payload));
197
- lastPrintedLogNumber = log.number;
198
- }
199
- }
200
- // Handle terminal states
201
- if (jobStatus === 'succeeded' ||
202
- jobStatus === 'failed' ||
203
- jobStatus === 'canceled' ||
204
- jobStatus === 'rejected' ||
205
- jobStatus === 'timed_out') {
206
- console.log(); // New line for better readability
207
- if (jobStatus === 'succeeded') {
208
- consola.success('Deployment completed successfully.');
209
- process.exit(0);
210
- }
211
- else if (jobStatus === 'failed') {
212
- consola.error('Deployment failed.');
213
- process.exit(1);
214
- }
215
- else if (jobStatus === 'canceled') {
216
- consola.warn('Deployment was canceled.');
217
- process.exit(1);
218
- }
219
- else if (jobStatus === 'rejected') {
220
- consola.error('Deployment was rejected.');
221
- process.exit(1);
222
- }
223
- else if (jobStatus === 'timed_out') {
224
- consola.error('Deployment timed out.');
225
- process.exit(1);
226
- }
227
- }
228
- // Wait before next poll (3 seconds)
229
- await wait(3000);
230
- }
231
- catch (error) {
232
- consola.error('Error polling deployment status:', error);
233
- process.exit(1);
234
- }
235
- }
161
+ await waitForJobCompletion({ jobId: response.jobId });
162
+ consola.success('Deployment completed successfully.');
163
+ process.exit(0);
236
164
  }
237
165
  }),
238
166
  });
@@ -10,11 +10,11 @@ export default defineCommand({
10
10
  description: 'Force a device to use a specific channel.',
11
11
  options: defineOptions(z.object({
12
12
  appId: z.string().uuid({ message: 'App ID must be a UUID.' }).optional().describe('ID of the app.'),
13
- deviceId: z.string().optional().describe('ID of the device.'),
13
+ deviceId: z.array(z.string()).optional().describe('ID of the device. Can be specified multiple times.'),
14
14
  channel: z.string().optional().describe('Name of the channel to force.'),
15
15
  })),
16
16
  action: withAuth(async (options, args) => {
17
- let { appId, deviceId, channel } = options;
17
+ let { appId, deviceId: deviceIds, channel } = options;
18
18
  if (!appId) {
19
19
  if (!isInteractive()) {
20
20
  consola.error('You must provide an app ID when running in non-interactive environment.');
@@ -23,14 +23,15 @@ export default defineCommand({
23
23
  const organizationId = await promptOrganizationSelection();
24
24
  appId = await promptAppSelection(organizationId);
25
25
  }
26
- if (!deviceId) {
26
+ if (!deviceIds || deviceIds.length === 0) {
27
27
  if (!isInteractive()) {
28
28
  consola.error('You must provide the device ID when running in non-interactive environment.');
29
29
  process.exit(1);
30
30
  }
31
- deviceId = await prompt('Enter the device ID:', {
31
+ const deviceId = await prompt('Enter the device ID:', {
32
32
  type: 'text',
33
33
  });
34
+ deviceIds = [deviceId];
34
35
  }
35
36
  if (!channel) {
36
37
  if (!isInteractive()) {
@@ -56,11 +57,12 @@ export default defineCommand({
56
57
  consola.error('Channel ID not found.');
57
58
  process.exit(1);
58
59
  }
59
- await appDevicesService.update({
60
+ await appDevicesService.updateMany({
60
61
  appId,
61
- deviceId,
62
+ deviceIds,
62
63
  forcedAppChannelId: channelId,
63
64
  });
64
- consola.success('Device forced to channel successfully.');
65
+ const deviceCount = deviceIds.length;
66
+ consola.success(`${deviceCount === 1 ? 'Device' : `${deviceCount} devices`} forced to channel successfully.`);
65
67
  }),
66
68
  });
@@ -9,10 +9,10 @@ export default defineCommand({
9
9
  description: 'Remove the forced channel from a device.',
10
10
  options: defineOptions(z.object({
11
11
  appId: z.string().uuid({ message: 'App ID must be a UUID.' }).optional().describe('ID of the app.'),
12
- deviceId: z.string().optional().describe('ID of the device.'),
12
+ deviceId: z.array(z.string()).optional().describe('ID of the device. Can be specified multiple times.'),
13
13
  })),
14
14
  action: withAuth(async (options, args) => {
15
- let { appId, deviceId } = options;
15
+ let { appId, deviceId: deviceIds } = options;
16
16
  if (!appId) {
17
17
  if (!isInteractive()) {
18
18
  consola.error('You must provide an app ID when running in non-interactive environment.');
@@ -21,20 +21,22 @@ export default defineCommand({
21
21
  const organizationId = await promptOrganizationSelection();
22
22
  appId = await promptAppSelection(organizationId);
23
23
  }
24
- if (!deviceId) {
24
+ if (!deviceIds || deviceIds.length === 0) {
25
25
  if (!isInteractive()) {
26
26
  consola.error('You must provide the device ID when running in non-interactive environment.');
27
27
  process.exit(1);
28
28
  }
29
- deviceId = await prompt('Enter the device ID:', {
29
+ const deviceId = await prompt('Enter the device ID:', {
30
30
  type: 'text',
31
31
  });
32
+ deviceIds = [deviceId];
32
33
  }
33
- await appDevicesService.update({
34
+ await appDevicesService.updateMany({
34
35
  appId,
35
- deviceId,
36
+ deviceIds,
36
37
  forcedAppChannelId: null,
37
38
  });
38
- consola.success('Forced channel removed from device successfully.');
39
+ const deviceCount = deviceIds.length;
40
+ consola.success(`Forced channel removed from ${deviceCount === 1 ? 'device' : `${deviceCount} devices`} successfully.`);
39
41
  }),
40
42
  });
@@ -0,0 +1,34 @@
1
+ import appsService from '../../services/apps.js';
2
+ import { withAuth } from '../../utils/auth.js';
3
+ import { isInteractive } from '../../utils/environment.js';
4
+ import { getGitRemoteInfo } from '../../utils/git.js';
5
+ import { 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: 'Connect a git repository to an app.',
11
+ options: defineOptions(z.object({
12
+ appId: z.string().optional().describe('ID of the app.'),
13
+ })),
14
+ action: withAuth(async (options, args) => {
15
+ let { appId } = options;
16
+ if (!appId) {
17
+ if (!isInteractive()) {
18
+ consola.error('You must provide the app ID when running in non-interactive environment.');
19
+ process.exit(1);
20
+ }
21
+ const organizationId = await promptOrganizationSelection();
22
+ appId = await promptAppSelection(organizationId);
23
+ }
24
+ const gitRemoteInfo = getGitRemoteInfo();
25
+ await appsService.linkRepository({
26
+ appId,
27
+ ownerSlug: gitRemoteInfo.ownerSlug,
28
+ provider: gitRemoteInfo.provider,
29
+ repositorySlug: gitRemoteInfo.repositorySlug,
30
+ projectSlug: gitRemoteInfo.projectSlug,
31
+ });
32
+ consola.success('Repository connected successfully.');
33
+ }),
34
+ });
@@ -0,0 +1,94 @@
1
+ import { DEFAULT_API_BASE_URL } from '../../config/consts.js';
2
+ import authorizationService from '../../services/authorization-service.js';
3
+ import { promptAppSelection, promptOrganizationSelection } from '../../utils/prompt.js';
4
+ import userConfig from '../../utils/user-config.js';
5
+ import consola from 'consola';
6
+ import nock from 'nock';
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
+ import linkCommand from './link.js';
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: () => true,
15
+ }));
16
+ vi.mock('@/utils/git.js', () => ({
17
+ getGitRemoteInfo: () => ({
18
+ ownerSlug: 'capawesome-team',
19
+ provider: 'github',
20
+ repositorySlug: 'cli',
21
+ }),
22
+ }));
23
+ describe('apps-link', () => {
24
+ const mockUserConfig = vi.mocked(userConfig);
25
+ const mockPromptOrganizationSelection = vi.mocked(promptOrganizationSelection);
26
+ const mockPromptAppSelection = vi.mocked(promptAppSelection);
27
+ const mockConsola = vi.mocked(consola);
28
+ const mockAuthorizationService = vi.mocked(authorizationService);
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ mockUserConfig.read.mockReturnValue({ token: 'test-token' });
32
+ mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
33
+ mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
34
+ vi.spyOn(process, 'exit').mockImplementation((code) => {
35
+ throw new Error(`Process exited with code ${code}`);
36
+ });
37
+ });
38
+ afterEach(() => {
39
+ nock.cleanAll();
40
+ vi.restoreAllMocks();
41
+ });
42
+ it('should link repository with provided app ID', async () => {
43
+ const appId = 'app-123';
44
+ const testToken = 'test-token';
45
+ const options = { appId };
46
+ const scope = nock(DEFAULT_API_BASE_URL)
47
+ .put(`/v1/apps/${appId}/repository`, {
48
+ ownerSlug: 'capawesome-team',
49
+ provider: 'github',
50
+ repositorySlug: 'cli',
51
+ })
52
+ .matchHeader('Authorization', `Bearer ${testToken}`)
53
+ .reply(200, { id: appId, name: 'Test App' });
54
+ await linkCommand.action(options, undefined);
55
+ expect(scope.isDone()).toBe(true);
56
+ expect(mockConsola.success).toHaveBeenCalledWith('Repository connected successfully.');
57
+ });
58
+ it('should prompt for organization and app when app ID is not provided', async () => {
59
+ const appId = 'app-123';
60
+ const orgId = 'org-1';
61
+ const testToken = 'test-token';
62
+ const options = {};
63
+ mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
64
+ mockPromptAppSelection.mockResolvedValueOnce(appId);
65
+ const scope = nock(DEFAULT_API_BASE_URL)
66
+ .put(`/v1/apps/${appId}/repository`, {
67
+ ownerSlug: 'capawesome-team',
68
+ provider: 'github',
69
+ repositorySlug: 'cli',
70
+ })
71
+ .matchHeader('Authorization', `Bearer ${testToken}`)
72
+ .reply(200, { id: appId, name: 'Test App' });
73
+ await linkCommand.action(options, undefined);
74
+ expect(scope.isDone()).toBe(true);
75
+ expect(mockPromptOrganizationSelection).toHaveBeenCalled();
76
+ expect(mockPromptAppSelection).toHaveBeenCalledWith(orgId);
77
+ expect(mockConsola.success).toHaveBeenCalledWith('Repository connected successfully.');
78
+ });
79
+ it('should handle API error', async () => {
80
+ const appId = 'app-123';
81
+ const testToken = 'test-token';
82
+ const options = { appId };
83
+ const scope = nock(DEFAULT_API_BASE_URL)
84
+ .put(`/v1/apps/${appId}/repository`, {
85
+ ownerSlug: 'capawesome-team',
86
+ provider: 'github',
87
+ repositorySlug: 'cli',
88
+ })
89
+ .matchHeader('Authorization', `Bearer ${testToken}`)
90
+ .reply(400, { message: 'Git provider not connected' });
91
+ await expect(linkCommand.action(options, undefined)).rejects.toThrow();
92
+ expect(scope.isDone()).toBe(true);
93
+ });
94
+ });
@@ -1,5 +1,5 @@
1
1
  import { isInteractive } from '../../../utils/environment.js';
2
- import { fileExistsAtPath, isDirectory } from '../../../utils/file.js';
2
+ import { directoryContainsSourceMaps, directoryContainsSymlinks, fileExistsAtPath, isDirectory } from '../../../utils/file.js';
3
3
  import { generateManifestJson } from '../../../utils/manifest.js';
4
4
  import { prompt } from '../../../utils/prompt.js';
5
5
  import zip from '../../../utils/zip.js';
@@ -9,7 +9,7 @@ import fs from 'fs';
9
9
  import pathModule from 'path';
10
10
  import { z } from 'zod';
11
11
  export default defineCommand({
12
- description: 'Generate manifest file and compress web assets into a bundle.zip file.',
12
+ description: 'Generate manifest file and compress locally built web assets into a bundle.zip file.',
13
13
  options: defineOptions(z.object({
14
14
  inputPath: z.string().optional().describe('Path to the web assets directory.'),
15
15
  outputPath: z
@@ -62,6 +62,16 @@ export default defineCommand({
62
62
  consola.error(`Directory must contain an index.html file: ${inputPath}`);
63
63
  process.exit(1);
64
64
  }
65
+ // Check for symlinks
66
+ const containsSymlinks = await directoryContainsSymlinks(inputPath);
67
+ if (containsSymlinks) {
68
+ consola.warn('Symbolic links were detected in the specified path. Symbolic links are skipped during bundling.');
69
+ }
70
+ // Check for source maps
71
+ const containsSourceMaps = await directoryContainsSourceMaps(inputPath);
72
+ if (containsSourceMaps) {
73
+ consola.warn('Source map files were detected in the specified path. Source maps should not be distributed to end users as they expose your original source code and increase the download size. Consider excluding source map files from your build output.');
74
+ }
65
75
  // 2. Output path resolution
66
76
  if (!outputPath) {
67
77
  outputPath = './bundle.zip';