@capawesome/cli 4.6.0-dev.bda9e9d.1774345600 → 4.7.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 +24 -0
- package/dist/commands/apps/builds/create.js +98 -12
- package/dist/commands/apps/bundles/create.js +4 -2
- package/dist/commands/apps/bundles/delete.js +2 -3
- package/dist/commands/apps/bundles/update.js +2 -3
- package/dist/commands/apps/certificates/delete.js +28 -5
- package/dist/commands/apps/certificates/get.js +28 -5
- package/dist/commands/apps/deployments/create.js +5 -77
- package/dist/commands/apps/devices/forcechannel.js +9 -7
- package/dist/commands/apps/devices/unforcechannel.js +9 -7
- package/dist/commands/apps/link.js +34 -0
- package/dist/commands/apps/link.test.js +94 -0
- package/dist/commands/apps/liveupdates/bundle.js +7 -2
- package/dist/commands/apps/liveupdates/create.js +148 -44
- package/dist/commands/apps/liveupdates/create.test.js +300 -0
- package/dist/commands/apps/liveupdates/generate-manifest.js +12 -1
- package/dist/commands/apps/liveupdates/generate-manifest.test.js +21 -1
- package/dist/commands/apps/liveupdates/register.js +10 -15
- package/dist/commands/apps/liveupdates/upload.js +18 -16
- package/dist/commands/apps/transfer.js +47 -0
- package/dist/commands/apps/transfer.test.js +123 -0
- package/dist/commands/apps/unlink.js +35 -0
- package/dist/commands/apps/unlink.test.js +99 -0
- package/dist/commands/manifests/generate.js +1 -1
- package/dist/index.js +3 -0
- package/dist/services/app-build-sources.js +120 -0
- package/dist/services/app-devices.js +8 -0
- package/dist/services/apps.js +25 -0
- package/dist/services/authorization-service.js +5 -1
- package/dist/services/jobs.js +13 -0
- package/dist/types/app-build-source.js +1 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/custom-properties.js +22 -0
- package/dist/utils/file.js +8 -1
- package/dist/utils/git.js +91 -0
- package/dist/utils/git.test.js +130 -0
- package/dist/utils/{build.js → job.js} +26 -23
- package/dist/utils/prompt.js +1 -1
- package/dist/utils/zip.js +19 -2
- package/package.json +2 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../services/authorization-service.js';
|
|
3
|
+
import { prompt, 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 transferAppCommand from './transfer.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
|
+
describe('apps-transfer', () => {
|
|
17
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
18
|
+
const mockPrompt = vi.mocked(prompt);
|
|
19
|
+
const mockPromptOrganizationSelection = vi.mocked(promptOrganizationSelection);
|
|
20
|
+
const mockPromptAppSelection = vi.mocked(promptAppSelection);
|
|
21
|
+
const mockConsola = vi.mocked(consola);
|
|
22
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
26
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
27
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
28
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
29
|
+
throw new Error(`Process exited with code ${code}`);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
nock.cleanAll();
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
it('should transfer app with provided options after confirmation', async () => {
|
|
37
|
+
const appId = 'app-123';
|
|
38
|
+
const organizationId = 'org-456';
|
|
39
|
+
const testToken = 'test-token';
|
|
40
|
+
const options = { appId, organizationId };
|
|
41
|
+
mockPrompt.mockResolvedValueOnce(true);
|
|
42
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
43
|
+
.post(`/v1/apps/${appId}/transfer`, { organizationId })
|
|
44
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
45
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
46
|
+
await transferAppCommand.action(options, undefined);
|
|
47
|
+
expect(scope.isDone()).toBe(true);
|
|
48
|
+
expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to transfer this app?', {
|
|
49
|
+
type: 'confirm',
|
|
50
|
+
});
|
|
51
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App transferred successfully.');
|
|
52
|
+
});
|
|
53
|
+
it('should skip confirmation prompt when --yes is provided', async () => {
|
|
54
|
+
const appId = 'app-123';
|
|
55
|
+
const organizationId = 'org-456';
|
|
56
|
+
const testToken = 'test-token';
|
|
57
|
+
const options = { appId, organizationId, yes: true };
|
|
58
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
59
|
+
.post(`/v1/apps/${appId}/transfer`, { organizationId })
|
|
60
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
61
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
62
|
+
await transferAppCommand.action(options, undefined);
|
|
63
|
+
expect(scope.isDone()).toBe(true);
|
|
64
|
+
expect(mockPrompt).not.toHaveBeenCalled();
|
|
65
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App transferred successfully.');
|
|
66
|
+
});
|
|
67
|
+
it('should not transfer app when confirmation is declined', async () => {
|
|
68
|
+
const appId = 'app-123';
|
|
69
|
+
const organizationId = 'org-456';
|
|
70
|
+
const options = { appId, organizationId };
|
|
71
|
+
mockPrompt.mockResolvedValueOnce(false);
|
|
72
|
+
await transferAppCommand.action(options, undefined);
|
|
73
|
+
expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to transfer this app?', {
|
|
74
|
+
type: 'confirm',
|
|
75
|
+
});
|
|
76
|
+
expect(mockConsola.success).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
it('should prompt for app selection when appId not provided', async () => {
|
|
79
|
+
const orgId = 'org-1';
|
|
80
|
+
const appId = 'app-1';
|
|
81
|
+
const targetOrgId = 'org-2';
|
|
82
|
+
const testToken = 'test-token';
|
|
83
|
+
const options = { organizationId: targetOrgId };
|
|
84
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
85
|
+
.post(`/v1/apps/${appId}/transfer`, { organizationId: targetOrgId })
|
|
86
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
87
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
88
|
+
mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
|
|
89
|
+
mockPromptAppSelection.mockResolvedValueOnce(appId);
|
|
90
|
+
mockPrompt.mockResolvedValueOnce(true);
|
|
91
|
+
await transferAppCommand.action(options, undefined);
|
|
92
|
+
expect(scope.isDone()).toBe(true);
|
|
93
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App transferred successfully.');
|
|
94
|
+
});
|
|
95
|
+
it('should prompt for organization selection when organizationId not provided', async () => {
|
|
96
|
+
const appId = 'app-123';
|
|
97
|
+
const targetOrgId = 'org-456';
|
|
98
|
+
const testToken = 'test-token';
|
|
99
|
+
const options = { appId };
|
|
100
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
101
|
+
.post(`/v1/apps/${appId}/transfer`, { organizationId: targetOrgId })
|
|
102
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
103
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
104
|
+
mockPromptOrganizationSelection.mockResolvedValueOnce(targetOrgId);
|
|
105
|
+
mockPrompt.mockResolvedValueOnce(true);
|
|
106
|
+
await transferAppCommand.action(options, undefined);
|
|
107
|
+
expect(scope.isDone()).toBe(true);
|
|
108
|
+
expect(mockConsola.success).toHaveBeenCalledWith('App transferred successfully.');
|
|
109
|
+
});
|
|
110
|
+
it('should handle API error during transfer', async () => {
|
|
111
|
+
const appId = 'app-123';
|
|
112
|
+
const organizationId = 'org-456';
|
|
113
|
+
const testToken = 'test-token';
|
|
114
|
+
const options = { appId, organizationId };
|
|
115
|
+
mockPrompt.mockResolvedValueOnce(true);
|
|
116
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
117
|
+
.post(`/v1/apps/${appId}/transfer`, { organizationId })
|
|
118
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
119
|
+
.reply(400, { message: 'Target organization has reached its app limit.' });
|
|
120
|
+
await expect(transferAppCommand.action(options, undefined)).rejects.toThrow();
|
|
121
|
+
expect(scope.isDone()).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import appsService from '../../services/apps.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: 'Disconnect a git repository from an app.',
|
|
10
|
+
options: defineOptions(z.object({
|
|
11
|
+
appId: z.string().optional().describe('ID of the app.'),
|
|
12
|
+
yes: z.boolean().optional().describe('Skip confirmation prompt.'),
|
|
13
|
+
}), { y: 'yes' }),
|
|
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
|
+
if (!options.yes && isInteractive()) {
|
|
25
|
+
const confirmed = await prompt('Are you sure you want to disconnect the repository?', {
|
|
26
|
+
type: 'confirm',
|
|
27
|
+
});
|
|
28
|
+
if (!confirmed) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
await appsService.unlinkRepository({ appId });
|
|
33
|
+
consola.success('Repository disconnected successfully.');
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { DEFAULT_API_BASE_URL } from '../../config/consts.js';
|
|
2
|
+
import authorizationService from '../../services/authorization-service.js';
|
|
3
|
+
import { prompt, 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 unlinkCommand from './unlink.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
|
+
describe('apps-unlink', () => {
|
|
17
|
+
const mockUserConfig = vi.mocked(userConfig);
|
|
18
|
+
const mockPrompt = vi.mocked(prompt);
|
|
19
|
+
const mockPromptOrganizationSelection = vi.mocked(promptOrganizationSelection);
|
|
20
|
+
const mockPromptAppSelection = vi.mocked(promptAppSelection);
|
|
21
|
+
const mockConsola = vi.mocked(consola);
|
|
22
|
+
const mockAuthorizationService = vi.mocked(authorizationService);
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
26
|
+
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
27
|
+
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
28
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
29
|
+
throw new Error(`Process exited with code ${code}`);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
nock.cleanAll();
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
it('should unlink repository with provided app ID and --yes flag', async () => {
|
|
37
|
+
const appId = 'app-123';
|
|
38
|
+
const testToken = 'test-token';
|
|
39
|
+
const options = { appId, yes: true };
|
|
40
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
41
|
+
.delete(`/v1/apps/${appId}/repository`)
|
|
42
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
43
|
+
.reply(204);
|
|
44
|
+
await unlinkCommand.action(options, undefined);
|
|
45
|
+
expect(scope.isDone()).toBe(true);
|
|
46
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Repository disconnected successfully.');
|
|
47
|
+
});
|
|
48
|
+
it('should prompt for confirmation when --yes is not provided', async () => {
|
|
49
|
+
const appId = 'app-123';
|
|
50
|
+
const testToken = 'test-token';
|
|
51
|
+
const options = { appId };
|
|
52
|
+
mockPrompt.mockResolvedValueOnce(true);
|
|
53
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
54
|
+
.delete(`/v1/apps/${appId}/repository`)
|
|
55
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
56
|
+
.reply(204);
|
|
57
|
+
await unlinkCommand.action(options, undefined);
|
|
58
|
+
expect(scope.isDone()).toBe(true);
|
|
59
|
+
expect(mockPrompt).toHaveBeenCalledWith('Are you sure you want to disconnect the repository?', {
|
|
60
|
+
type: 'confirm',
|
|
61
|
+
});
|
|
62
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Repository disconnected successfully.');
|
|
63
|
+
});
|
|
64
|
+
it('should abort when user declines confirmation', async () => {
|
|
65
|
+
const appId = 'app-123';
|
|
66
|
+
const options = { appId };
|
|
67
|
+
mockPrompt.mockResolvedValueOnce(false);
|
|
68
|
+
await unlinkCommand.action(options, undefined);
|
|
69
|
+
expect(mockConsola.success).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
it('should prompt for organization and app when app ID is not provided', async () => {
|
|
72
|
+
const appId = 'app-123';
|
|
73
|
+
const orgId = 'org-1';
|
|
74
|
+
const testToken = 'test-token';
|
|
75
|
+
const options = { yes: true };
|
|
76
|
+
mockPromptOrganizationSelection.mockResolvedValueOnce(orgId);
|
|
77
|
+
mockPromptAppSelection.mockResolvedValueOnce(appId);
|
|
78
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
79
|
+
.delete(`/v1/apps/${appId}/repository`)
|
|
80
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
81
|
+
.reply(204);
|
|
82
|
+
await unlinkCommand.action(options, undefined);
|
|
83
|
+
expect(scope.isDone()).toBe(true);
|
|
84
|
+
expect(mockPromptOrganizationSelection).toHaveBeenCalled();
|
|
85
|
+
expect(mockPromptAppSelection).toHaveBeenCalledWith(orgId);
|
|
86
|
+
expect(mockConsola.success).toHaveBeenCalledWith('Repository disconnected successfully.');
|
|
87
|
+
});
|
|
88
|
+
it('should handle API error', async () => {
|
|
89
|
+
const appId = 'app-123';
|
|
90
|
+
const testToken = 'test-token';
|
|
91
|
+
const options = { appId, yes: true };
|
|
92
|
+
const scope = nock(DEFAULT_API_BASE_URL)
|
|
93
|
+
.delete(`/v1/apps/${appId}/repository`)
|
|
94
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
95
|
+
.reply(400, { message: 'No repository linked to this app' });
|
|
96
|
+
await expect(unlinkCommand.action(options, undefined)).rejects.toThrow();
|
|
97
|
+
expect(scope.isDone()).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -2,7 +2,7 @@ import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
|
2
2
|
import consola from 'consola';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
export default defineCommand({
|
|
5
|
-
description: 'Generate a manifest file.',
|
|
5
|
+
description: 'Generate a manifest file. Deprecated, use `apps:liveupdates:generate-manifest` instead.',
|
|
6
6
|
options: defineOptions(z.object({
|
|
7
7
|
path: z.string().optional().describe('Path to the web assets folder (e.g. `www` or `dist`).'),
|
|
8
8
|
})),
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,9 @@ 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:link': await import('./commands/apps/link.js').then((mod) => mod.default),
|
|
27
|
+
'apps:transfer': await import('./commands/apps/transfer.js').then((mod) => mod.default),
|
|
28
|
+
'apps:unlink': await import('./commands/apps/unlink.js').then((mod) => mod.default),
|
|
26
29
|
'apps:builds:cancel': await import('./commands/apps/builds/cancel.js').then((mod) => mod.default),
|
|
27
30
|
'apps:builds:create': await import('./commands/apps/builds/create.js').then((mod) => mod.default),
|
|
28
31
|
'apps:builds:logs': await import('./commands/apps/builds/logs.js').then((mod) => mod.default),
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { MAX_CONCURRENT_PART_UPLOADS } from '../config/index.js';
|
|
2
|
+
import authorizationService from '../services/authorization-service.js';
|
|
3
|
+
import httpClient from '../utils/http-client.js';
|
|
4
|
+
import FormData from 'form-data';
|
|
5
|
+
class AppBuildSourcesServiceImpl {
|
|
6
|
+
httpClient;
|
|
7
|
+
constructor(httpClient) {
|
|
8
|
+
this.httpClient = httpClient;
|
|
9
|
+
}
|
|
10
|
+
async createFromFile(dto, onProgress) {
|
|
11
|
+
const response = await this.httpClient.post(`/v1/apps/${dto.appId}/build-sources`, { fileSizeInBytes: dto.fileSizeInBytes }, {
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
await this.upload({
|
|
17
|
+
appBuildSourceId: response.data.id,
|
|
18
|
+
appId: dto.appId,
|
|
19
|
+
buffer: dto.buffer,
|
|
20
|
+
name: dto.name,
|
|
21
|
+
}, onProgress);
|
|
22
|
+
return response.data;
|
|
23
|
+
}
|
|
24
|
+
async createFromUrl(dto) {
|
|
25
|
+
const response = await this.httpClient.post(`/v1/apps/${dto.appId}/build-sources`, { fileUrl: dto.fileUrl }, {
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
return response.data;
|
|
31
|
+
}
|
|
32
|
+
async completeUpload(dto) {
|
|
33
|
+
return this.httpClient
|
|
34
|
+
.post(`/v1/apps/${dto.appId}/build-sources/${dto.appBuildSourceId}/upload?action=mpu-complete&uploadId=${dto.uploadId}`, {
|
|
35
|
+
parts: dto.parts,
|
|
36
|
+
}, {
|
|
37
|
+
headers: {
|
|
38
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
.then((response) => response.data);
|
|
42
|
+
}
|
|
43
|
+
async createUpload(dto) {
|
|
44
|
+
const response = await this.httpClient.post(`/v1/apps/${dto.appId}/build-sources/${dto.appBuildSourceId}/upload?action=mpu-create`, {}, {
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return response.data;
|
|
50
|
+
}
|
|
51
|
+
async createUploadPart(dto) {
|
|
52
|
+
const formData = new FormData();
|
|
53
|
+
formData.append('blob', dto.buffer, { filename: dto.name });
|
|
54
|
+
formData.append('partNumber', dto.partNumber.toString());
|
|
55
|
+
return this.httpClient
|
|
56
|
+
.put(`/v1/apps/${dto.appId}/build-sources/${dto.appBuildSourceId}/upload?action=mpu-uploadpart&uploadId=${dto.uploadId}`, formData, {
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
59
|
+
...formData.getHeaders(),
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
.then((response) => response.data);
|
|
63
|
+
}
|
|
64
|
+
async createUploadParts(dto, onProgress) {
|
|
65
|
+
const uploadedParts = [];
|
|
66
|
+
const partSize = 10 * 1024 * 1024; // 10 MB
|
|
67
|
+
const totalParts = Math.ceil(dto.buffer.byteLength / partSize);
|
|
68
|
+
let partNumber = 0;
|
|
69
|
+
const uploadNextPart = async () => {
|
|
70
|
+
if (partNumber >= totalParts) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
partNumber++;
|
|
74
|
+
onProgress?.(partNumber, totalParts);
|
|
75
|
+
const start = (partNumber - 1) * partSize;
|
|
76
|
+
const end = Math.min(start + partSize, dto.buffer.byteLength);
|
|
77
|
+
const partBuffer = dto.buffer.subarray(start, end);
|
|
78
|
+
const uploadedPart = await this.createUploadPart({
|
|
79
|
+
appBuildSourceId: dto.appBuildSourceId,
|
|
80
|
+
appId: dto.appId,
|
|
81
|
+
buffer: partBuffer,
|
|
82
|
+
name: dto.name,
|
|
83
|
+
partNumber,
|
|
84
|
+
uploadId: dto.uploadId,
|
|
85
|
+
});
|
|
86
|
+
uploadedParts.push(uploadedPart);
|
|
87
|
+
await uploadNextPart();
|
|
88
|
+
};
|
|
89
|
+
const uploadPartPromises = Array.from({ length: MAX_CONCURRENT_PART_UPLOADS });
|
|
90
|
+
for (let i = 0; i < MAX_CONCURRENT_PART_UPLOADS; i++) {
|
|
91
|
+
uploadPartPromises[i] = uploadNextPart();
|
|
92
|
+
}
|
|
93
|
+
await Promise.all(uploadPartPromises);
|
|
94
|
+
return uploadedParts.sort((a, b) => a.partNumber - b.partNumber);
|
|
95
|
+
}
|
|
96
|
+
async upload(dto, onProgress) {
|
|
97
|
+
// 1. Create a multipart upload
|
|
98
|
+
const { uploadId } = await this.createUpload({
|
|
99
|
+
appBuildSourceId: dto.appBuildSourceId,
|
|
100
|
+
appId: dto.appId,
|
|
101
|
+
});
|
|
102
|
+
// 2. Upload the file in parts
|
|
103
|
+
const parts = await this.createUploadParts({
|
|
104
|
+
appBuildSourceId: dto.appBuildSourceId,
|
|
105
|
+
appId: dto.appId,
|
|
106
|
+
buffer: dto.buffer,
|
|
107
|
+
name: dto.name,
|
|
108
|
+
uploadId,
|
|
109
|
+
}, onProgress);
|
|
110
|
+
// 3. Complete the upload
|
|
111
|
+
await this.completeUpload({
|
|
112
|
+
appBuildSourceId: dto.appBuildSourceId,
|
|
113
|
+
appId: dto.appId,
|
|
114
|
+
parts,
|
|
115
|
+
uploadId,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const appBuildSourcesService = new AppBuildSourcesServiceImpl(httpClient);
|
|
120
|
+
export default appBuildSourcesService;
|
|
@@ -55,6 +55,14 @@ class AppDevicesServiceImpl {
|
|
|
55
55
|
},
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
+
async updateMany(data) {
|
|
59
|
+
const ids = data.deviceIds.join(',');
|
|
60
|
+
await this.httpClient.patch(`/v1/apps/${data.appId}/devices?ids=${ids}`, { forcedAppChannelId: data.forcedAppChannelId }, {
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
58
66
|
}
|
|
59
67
|
const appDevicesService = new AppDevicesServiceImpl(httpClient);
|
|
60
68
|
export default appDevicesService;
|
package/dist/services/apps.js
CHANGED
|
@@ -42,6 +42,31 @@ class AppsServiceImpl {
|
|
|
42
42
|
});
|
|
43
43
|
return response.data;
|
|
44
44
|
}
|
|
45
|
+
async linkRepository(dto) {
|
|
46
|
+
const { appId, ...bodyData } = dto;
|
|
47
|
+
const response = await this.httpClient.put(`/v1/apps/${appId}/repository`, bodyData, {
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return response.data;
|
|
53
|
+
}
|
|
54
|
+
async transfer(dto) {
|
|
55
|
+
const { appId, ...bodyData } = dto;
|
|
56
|
+
const response = await this.httpClient.post(`/v1/apps/${appId}/transfer`, bodyData, {
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return response.data;
|
|
62
|
+
}
|
|
63
|
+
async unlinkRepository(dto) {
|
|
64
|
+
await this.httpClient.delete(`/v1/apps/${dto.appId}/repository`, {
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
45
70
|
}
|
|
46
71
|
const appsService = new AppsServiceImpl(httpClient);
|
|
47
72
|
export default appsService;
|
|
@@ -5,7 +5,11 @@ class AuthorizationServiceImpl {
|
|
|
5
5
|
this.userConfig = userConfig;
|
|
6
6
|
}
|
|
7
7
|
getCurrentAuthorizationToken() {
|
|
8
|
-
|
|
8
|
+
const token = this.userConfig.read().token || process.env.CAPAWESOME_CLOUD_TOKEN || process.env.CAPAWESOME_TOKEN || null;
|
|
9
|
+
// Trim to remove newline characters that may be included when pasting a token,
|
|
10
|
+
// which would cause an invalid character error in the Authorization header.
|
|
11
|
+
const trimmedToken = token?.trim();
|
|
12
|
+
return trimmedToken || null;
|
|
9
13
|
}
|
|
10
14
|
hasAuthorizationToken() {
|
|
11
15
|
return !!this.getCurrentAuthorizationToken();
|
package/dist/services/jobs.js
CHANGED
|
@@ -5,6 +5,19 @@ class JobsServiceImpl {
|
|
|
5
5
|
constructor(httpClient) {
|
|
6
6
|
this.httpClient = httpClient;
|
|
7
7
|
}
|
|
8
|
+
async findOne(dto) {
|
|
9
|
+
const params = {};
|
|
10
|
+
if (dto.relations) {
|
|
11
|
+
params.relations = dto.relations;
|
|
12
|
+
}
|
|
13
|
+
const response = await this.httpClient.get(`/v1/jobs/${dto.jobId}`, {
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
16
|
+
},
|
|
17
|
+
params,
|
|
18
|
+
});
|
|
19
|
+
return response.data;
|
|
20
|
+
}
|
|
8
21
|
async update(options) {
|
|
9
22
|
const { jobId, dto } = options;
|
|
10
23
|
const response = await this.httpClient.patch(`/v1/jobs/${jobId}`, dto, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/index.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const parseCustomProperties = (customProperty) => {
|
|
2
|
+
if (!customProperty) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const customProperties = {};
|
|
6
|
+
for (const property of customProperty) {
|
|
7
|
+
if (!property) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const separatorIndex = property.indexOf('=');
|
|
11
|
+
if (separatorIndex === -1) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const key = property.slice(0, separatorIndex).trim();
|
|
15
|
+
const value = property.slice(separatorIndex + 1).trim();
|
|
16
|
+
if (!key || !value) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
customProperties[key] = value;
|
|
20
|
+
}
|
|
21
|
+
return Object.keys(customProperties).length > 0 ? customProperties : undefined;
|
|
22
|
+
};
|
package/dist/utils/file.js
CHANGED
|
@@ -7,7 +7,10 @@ export const getFilesInDirectoryAndSubdirectories = async (path) => {
|
|
|
7
7
|
const dirEntries = await fs.promises.readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
8
8
|
for (const dirEntry of dirEntries) {
|
|
9
9
|
const fullPath = pathModule.join(directory, dirEntry.name);
|
|
10
|
-
if (dirEntry.
|
|
10
|
+
if (dirEntry.isSymbolicLink()) {
|
|
11
|
+
// Skip symlinks
|
|
12
|
+
}
|
|
13
|
+
else if (dirEntry.isDirectory()) {
|
|
11
14
|
await walk(fullPath);
|
|
12
15
|
}
|
|
13
16
|
else {
|
|
@@ -35,6 +38,10 @@ export const getFilesInDirectoryAndSubdirectories = async (path) => {
|
|
|
35
38
|
await walk(path);
|
|
36
39
|
return files;
|
|
37
40
|
};
|
|
41
|
+
export const directoryContainsSymlinks = async (path) => {
|
|
42
|
+
const dirEntries = await fs.promises.readdir(path, { withFileTypes: true, recursive: true }).catch(() => []);
|
|
43
|
+
return dirEntries.some((dirEntry) => dirEntry.isSymbolicLink());
|
|
44
|
+
};
|
|
38
45
|
export const directoryContainsSourceMaps = async (path) => {
|
|
39
46
|
const files = await getFilesInDirectoryAndSubdirectories(path);
|
|
40
47
|
return files.some((file) => file.name.endsWith('.js.map') || file.name.endsWith('.css.map'));
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
const HOSTNAME_TO_PROVIDER = {
|
|
3
|
+
'github.com': 'github',
|
|
4
|
+
'gitlab.com': 'gitlab',
|
|
5
|
+
'bitbucket.org': 'bitbucket',
|
|
6
|
+
'dev.azure.com': 'azure',
|
|
7
|
+
'ssh.dev.azure.com': 'azure',
|
|
8
|
+
};
|
|
9
|
+
export const getGitRemoteInfo = () => {
|
|
10
|
+
const remoteUrl = getGitRemoteUrl();
|
|
11
|
+
return parseGitRemoteUrl(remoteUrl);
|
|
12
|
+
};
|
|
13
|
+
const getGitRemoteUrl = () => {
|
|
14
|
+
try {
|
|
15
|
+
return execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error('Could not read the git remote URL. Make sure you are inside a git repository with an origin remote.');
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export const parseGitRemoteUrl = (remoteUrl) => {
|
|
22
|
+
// Azure DevOps HTTPS: https://dev.azure.com/{org}/{project}/_git/{repo}
|
|
23
|
+
const azureHttpsMatch = remoteUrl.match(/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/);
|
|
24
|
+
if (azureHttpsMatch && azureHttpsMatch[1] && azureHttpsMatch[2] && azureHttpsMatch[3]) {
|
|
25
|
+
return {
|
|
26
|
+
ownerSlug: azureHttpsMatch[1],
|
|
27
|
+
provider: 'azure',
|
|
28
|
+
repositorySlug: azureHttpsMatch[3],
|
|
29
|
+
projectSlug: azureHttpsMatch[2],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Azure DevOps SSH: git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
|
|
33
|
+
const azureSshMatch = remoteUrl.match(/ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
34
|
+
if (azureSshMatch && azureSshMatch[1] && azureSshMatch[2] && azureSshMatch[3]) {
|
|
35
|
+
return {
|
|
36
|
+
ownerSlug: azureSshMatch[1],
|
|
37
|
+
provider: 'azure',
|
|
38
|
+
repositorySlug: azureSshMatch[3],
|
|
39
|
+
projectSlug: azureSshMatch[2],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Visual Studio HTTPS: https://{org}.visualstudio.com/{project}/_git/{repo}
|
|
43
|
+
const vsHttpsMatch = remoteUrl.match(/([^/]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/);
|
|
44
|
+
if (vsHttpsMatch && vsHttpsMatch[1] && vsHttpsMatch[2] && vsHttpsMatch[3]) {
|
|
45
|
+
return {
|
|
46
|
+
ownerSlug: vsHttpsMatch[1],
|
|
47
|
+
provider: 'azure',
|
|
48
|
+
repositorySlug: vsHttpsMatch[3],
|
|
49
|
+
projectSlug: vsHttpsMatch[2],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// SSH: git@{host}:{owner}[/{subgroup}]/{repo}.git
|
|
53
|
+
const sshMatch = remoteUrl.match(/git@([^:]+):([^/]+)(?:\/([^/]+))?\/([^/]+?)(?:\.git)?$/);
|
|
54
|
+
if (sshMatch && sshMatch[1] && sshMatch[2] && sshMatch[4]) {
|
|
55
|
+
const hostname = sshMatch[1];
|
|
56
|
+
const provider = HOSTNAME_TO_PROVIDER[hostname];
|
|
57
|
+
if (!provider) {
|
|
58
|
+
throw new Error(`Unsupported git provider for hostname "${hostname}".`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ownerSlug: sshMatch[2],
|
|
62
|
+
provider,
|
|
63
|
+
repositorySlug: sshMatch[4],
|
|
64
|
+
projectSlug: sshMatch[3],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// HTTPS: https://[user@]{host}/{owner}[/{subgroup}]/{repo}.git
|
|
68
|
+
try {
|
|
69
|
+
const url = new URL(remoteUrl);
|
|
70
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
71
|
+
const hostname = url.hostname;
|
|
72
|
+
const provider = HOSTNAME_TO_PROVIDER[hostname];
|
|
73
|
+
if (!provider) {
|
|
74
|
+
throw new Error(`Unsupported git provider for hostname "${hostname}".`);
|
|
75
|
+
}
|
|
76
|
+
const pathSegments = url.pathname.split('/').filter(Boolean);
|
|
77
|
+
const repositorySlug = pathSegments.pop()?.replace(/\.git$/, '');
|
|
78
|
+
const ownerSlug = pathSegments.shift();
|
|
79
|
+
const projectSlug = pathSegments.length > 0 ? pathSegments.join('/') : undefined;
|
|
80
|
+
if (ownerSlug && repositorySlug) {
|
|
81
|
+
return { ownerSlug, provider, repositorySlug, projectSlug };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof Error && error.message.startsWith('Unsupported git provider')) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new Error('Could not parse git remote URL.');
|
|
91
|
+
};
|