@capawesome/cli 3.11.0 → 4.0.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 +34 -0
- package/dist/commands/apps/builds/cancel.js +1 -1
- package/dist/commands/apps/builds/create.js +58 -50
- package/dist/commands/apps/builds/download.js +27 -3
- package/dist/commands/apps/bundles/create.js +5 -449
- package/dist/commands/apps/bundles/delete.js +3 -68
- package/dist/commands/apps/bundles/update.js +3 -66
- package/dist/commands/apps/channels/create.js +5 -8
- package/dist/commands/apps/channels/create.test.js +6 -9
- package/dist/commands/apps/channels/delete.js +3 -2
- package/dist/commands/apps/channels/get.js +2 -12
- package/dist/commands/apps/channels/get.test.js +1 -2
- package/dist/commands/apps/channels/list.js +2 -10
- package/dist/commands/apps/channels/list.test.js +2 -3
- package/dist/commands/apps/channels/pause.js +85 -0
- package/dist/commands/apps/channels/resume.js +85 -0
- package/dist/commands/apps/channels/update.js +4 -7
- package/dist/commands/apps/channels/update.test.js +2 -4
- package/dist/commands/apps/create.js +1 -1
- package/dist/commands/apps/delete.js +3 -2
- package/dist/commands/apps/deployments/cancel.js +1 -1
- package/dist/commands/apps/deployments/create.js +82 -31
- package/dist/commands/apps/devices/delete.js +3 -2
- package/dist/commands/apps/environments/create.js +1 -1
- package/dist/commands/apps/environments/delete.js +3 -2
- package/dist/commands/apps/liveupdates/bundle.js +117 -0
- package/dist/commands/apps/liveupdates/generate-manifest.js +39 -0
- package/dist/commands/{manifests/generate.test.js → apps/liveupdates/generate-manifest.test.js} +6 -6
- package/dist/commands/apps/liveupdates/register.js +291 -0
- package/dist/commands/apps/{bundles/create.test.js → liveupdates/register.test.js} +123 -111
- package/dist/commands/apps/liveupdates/rollback.js +171 -0
- package/dist/commands/apps/liveupdates/rollout.js +147 -0
- package/dist/commands/apps/liveupdates/upload.js +420 -0
- package/dist/commands/apps/liveupdates/upload.test.js +325 -0
- package/dist/commands/manifests/generate.js +2 -27
- package/dist/commands/organizations/create.js +1 -1
- package/dist/index.js +8 -0
- package/dist/services/app-builds.js +9 -2
- package/dist/services/app-channels.js +19 -0
- package/dist/services/app-deployments.js +24 -14
- package/dist/services/config.js +2 -0
- package/dist/utils/app-environments.js +2 -1
- package/dist/utils/time-format.js +26 -0
- package/package.json +3 -3
- package/dist/commands/apps/bundles/delete.test.js +0 -142
- package/dist/commands/apps/bundles/update.test.js +0 -144
- package/dist/utils/capacitor-config.js +0 -96
- package/dist/utils/package-json.js +0 -58
|
@@ -13,7 +13,8 @@ export default defineCommand({
|
|
|
13
13
|
appId: z.string().optional().describe('ID of the app.'),
|
|
14
14
|
environmentId: z.string().optional().describe('ID of the environment. Either the ID or name must be provided.'),
|
|
15
15
|
name: z.string().optional().describe('Name of the environment. Either the ID or name must be provided.'),
|
|
16
|
-
|
|
16
|
+
yes: z.boolean().optional().describe('Skip confirmation prompt.'),
|
|
17
|
+
}), { y: 'yes' }),
|
|
17
18
|
action: async (options, args) => {
|
|
18
19
|
let { appId, environmentId, name } = options;
|
|
19
20
|
if (!authorizationService.hasAuthorizationToken()) {
|
|
@@ -69,7 +70,7 @@ export default defineCommand({
|
|
|
69
70
|
});
|
|
70
71
|
environmentId = selectedEnvironmentId;
|
|
71
72
|
}
|
|
72
|
-
if (isInteractive()) {
|
|
73
|
+
if (!options.yes && isInteractive()) {
|
|
73
74
|
const confirmed = await prompt('Are you sure you want to delete this environment?', {
|
|
74
75
|
type: 'confirm',
|
|
75
76
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
2
|
+
import { fileExistsAtPath, isDirectory } from '../../../utils/file.js';
|
|
3
|
+
import { generateManifestJson } from '../../../utils/manifest.js';
|
|
4
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
5
|
+
import zip from '../../../utils/zip.js';
|
|
6
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
7
|
+
import consola from 'consola';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import pathModule from 'path';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
export default defineCommand({
|
|
12
|
+
description: 'Generate manifest file and compress web assets into a bundle.zip file.',
|
|
13
|
+
options: defineOptions(z.object({
|
|
14
|
+
inputPath: z.string().optional().describe('Path to the web assets directory.'),
|
|
15
|
+
outputPath: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.default('./bundle.zip')
|
|
19
|
+
.describe('Output path for the generated artifact file (default: ./bundle.zip).'),
|
|
20
|
+
overwrite: z
|
|
21
|
+
.boolean()
|
|
22
|
+
.optional()
|
|
23
|
+
.default(false)
|
|
24
|
+
.describe('Overwrite output file if it already exists (default: false).'),
|
|
25
|
+
skipManifest: z.boolean().optional().default(false).describe('Skip manifest file generation (default: false).'),
|
|
26
|
+
})),
|
|
27
|
+
action: async (options, args) => {
|
|
28
|
+
let { inputPath, outputPath, overwrite, skipManifest } = options;
|
|
29
|
+
// 1. Input path resolution
|
|
30
|
+
if (!inputPath) {
|
|
31
|
+
if (!isInteractive()) {
|
|
32
|
+
consola.error('You must provide an input path when running in non-interactive environment.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
consola.warn('Make sure you have built your web assets before creating a bundle (e.g., `npm run build`).');
|
|
36
|
+
const response = await prompt('Enter the path to the web assets directory (e.g., `dist` or `www`):', {
|
|
37
|
+
type: 'text',
|
|
38
|
+
});
|
|
39
|
+
inputPath = response;
|
|
40
|
+
}
|
|
41
|
+
// Convert to absolute path
|
|
42
|
+
inputPath = pathModule.resolve(inputPath);
|
|
43
|
+
// Validate input path exists
|
|
44
|
+
const inputExists = await fileExistsAtPath(inputPath);
|
|
45
|
+
if (!inputExists) {
|
|
46
|
+
consola.error(`Input path does not exist: ${inputPath}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
// Validate input is a directory
|
|
50
|
+
const inputIsDirectory = await isDirectory(inputPath);
|
|
51
|
+
if (!inputIsDirectory) {
|
|
52
|
+
consola.error(`Input path must be a directory, not a file: ${inputPath}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
// Validate directory contains index.html
|
|
56
|
+
const indexHtmlPath = pathModule.join(inputPath, 'index.html');
|
|
57
|
+
const hasIndexHtml = await fileExistsAtPath(indexHtmlPath);
|
|
58
|
+
if (!hasIndexHtml) {
|
|
59
|
+
consola.error(`Directory must contain an index.html file: ${inputPath}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
// 2. Output path resolution
|
|
63
|
+
if (!outputPath) {
|
|
64
|
+
outputPath = './bundle.zip';
|
|
65
|
+
}
|
|
66
|
+
outputPath = pathModule.resolve(outputPath);
|
|
67
|
+
// 3. Check if output exists and handle overwrite
|
|
68
|
+
const outputExists = await fileExistsAtPath(outputPath);
|
|
69
|
+
if (outputExists) {
|
|
70
|
+
if (!overwrite) {
|
|
71
|
+
if (!isInteractive()) {
|
|
72
|
+
consola.error(`Output file already exists: ${outputPath}. Use --overwrite flag to skip confirmation or run in interactive mode.`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const shouldOverwrite = await prompt('Output file already exists. Overwrite?', {
|
|
76
|
+
type: 'confirm',
|
|
77
|
+
initial: false,
|
|
78
|
+
});
|
|
79
|
+
if (!shouldOverwrite) {
|
|
80
|
+
consola.info('Operation cancelled.');
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Validate parent directory exists
|
|
86
|
+
const outputDir = pathModule.dirname(outputPath);
|
|
87
|
+
const outputDirExists = await fileExistsAtPath(outputDir);
|
|
88
|
+
if (!outputDirExists) {
|
|
89
|
+
consola.error(`Output directory does not exist: ${outputDir}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
// 4. Generate bundle
|
|
93
|
+
consola.start('Generating bundle...');
|
|
94
|
+
try {
|
|
95
|
+
// Generate manifest (unless skipped)
|
|
96
|
+
if (!skipManifest) {
|
|
97
|
+
consola.info('Generating manifest file...');
|
|
98
|
+
await generateManifestJson(inputPath);
|
|
99
|
+
}
|
|
100
|
+
// Compress directory
|
|
101
|
+
consola.info('Compressing directory...');
|
|
102
|
+
const buffer = await zip.zipFolder(inputPath);
|
|
103
|
+
// Write output
|
|
104
|
+
consola.info(`Writing output to ${outputPath}...`);
|
|
105
|
+
await fs.promises.writeFile(outputPath, buffer);
|
|
106
|
+
// Success output
|
|
107
|
+
consola.success(`Bundle created successfully!`);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
consola.error('Failed to generate bundle:');
|
|
111
|
+
if (error instanceof Error) {
|
|
112
|
+
consola.error(error.message);
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
2
|
+
import { fileExistsAtPath } from '../../../utils/file.js';
|
|
3
|
+
import { generateManifestJson } from '../../../utils/manifest.js';
|
|
4
|
+
import { prompt } 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: 'Generate a manifest file.',
|
|
10
|
+
options: defineOptions(z.object({
|
|
11
|
+
path: z.string().optional().describe('Path to the web assets folder (e.g. `www` or `dist`).'),
|
|
12
|
+
})),
|
|
13
|
+
action: async (options, args) => {
|
|
14
|
+
let path = options.path;
|
|
15
|
+
if (!path) {
|
|
16
|
+
if (!isInteractive()) {
|
|
17
|
+
consola.error('You must provide the path to the web assets folder when running in non-interactive environment.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
consola.warn('Make sure you have built your web assets before generating the manifest (e.g., `npm run build`).');
|
|
21
|
+
path = await prompt('Enter the path to the web assets folder (e.g., `dist` or `www`):', {
|
|
22
|
+
type: 'text',
|
|
23
|
+
});
|
|
24
|
+
if (!path) {
|
|
25
|
+
consola.error('You must provide a path to the web assets folder.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Check if the path exists
|
|
30
|
+
const pathExists = await fileExistsAtPath(path);
|
|
31
|
+
if (!pathExists) {
|
|
32
|
+
consola.error(`The path does not exist.`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
// Generate the manifest file
|
|
36
|
+
await generateManifestJson(path);
|
|
37
|
+
consola.success('Manifest file generated.');
|
|
38
|
+
},
|
|
39
|
+
});
|
package/dist/commands/{manifests/generate.test.js → apps/liveupdates/generate-manifest.test.js}
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { fileExistsAtPath } from '
|
|
2
|
-
import { generateManifestJson } from '
|
|
3
|
-
import { prompt } from '
|
|
1
|
+
import { fileExistsAtPath } from '../../../utils/file.js';
|
|
2
|
+
import { generateManifestJson } from '../../../utils/manifest.js';
|
|
3
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
4
4
|
import consola from 'consola';
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
import generateManifestCommand from './generate.js';
|
|
6
|
+
import generateManifestCommand from './generate-manifest.js';
|
|
7
7
|
// Mock dependencies
|
|
8
8
|
vi.mock('@/utils/file.js');
|
|
9
9
|
vi.mock('@/utils/manifest.js');
|
|
@@ -12,7 +12,7 @@ vi.mock('consola');
|
|
|
12
12
|
vi.mock('@/utils/environment.js', () => ({
|
|
13
13
|
isInteractive: () => true,
|
|
14
14
|
}));
|
|
15
|
-
describe('
|
|
15
|
+
describe('apps-liveupdates-generatemanifest', () => {
|
|
16
16
|
const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
|
|
17
17
|
const mockGenerateManifestJson = vi.mocked(generateManifestJson);
|
|
18
18
|
const mockPrompt = vi.mocked(prompt);
|
|
@@ -41,7 +41,7 @@ describe('manifests-generate', () => {
|
|
|
41
41
|
mockFileExistsAtPath.mockResolvedValue(true);
|
|
42
42
|
mockGenerateManifestJson.mockResolvedValue(undefined);
|
|
43
43
|
await generateManifestCommand.action(options, undefined);
|
|
44
|
-
expect(mockPrompt).toHaveBeenCalledWith('Enter the path to the web assets folder:', {
|
|
44
|
+
expect(mockPrompt).toHaveBeenCalledWith('Enter the path to the web assets folder (e.g., `dist` or `www`):', {
|
|
45
45
|
type: 'text',
|
|
46
46
|
});
|
|
47
47
|
expect(mockFileExistsAtPath).toHaveBeenCalledWith('./www');
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { DEFAULT_CONSOLE_BASE_URL } from '../../../config/consts.js';
|
|
2
|
+
import appBundlesService from '../../../services/app-bundles.js';
|
|
3
|
+
import appsService from '../../../services/apps.js';
|
|
4
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
5
|
+
import organizationsService from '../../../services/organizations.js';
|
|
6
|
+
import { createBufferFromPath, createBufferFromString, isPrivateKeyContent } from '../../../utils/buffer.js';
|
|
7
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
8
|
+
import { fileExistsAtPath } from '../../../utils/file.js';
|
|
9
|
+
import { createHash } from '../../../utils/hash.js';
|
|
10
|
+
import { formatPrivateKey } from '../../../utils/private-key.js';
|
|
11
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
12
|
+
import { createSignature } from '../../../utils/signature.js';
|
|
13
|
+
import zip from '../../../utils/zip.js';
|
|
14
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
15
|
+
import consola from 'consola';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
export default defineCommand({
|
|
18
|
+
description: 'Register a self-hosted bundle URL.',
|
|
19
|
+
options: defineOptions(z.object({
|
|
20
|
+
androidMax: z.coerce
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('The maximum Android version code (`versionCode`) that the bundle supports.'),
|
|
24
|
+
androidMin: z.coerce
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('The minimum Android version code (`versionCode`) that the bundle supports.'),
|
|
28
|
+
androidEq: z.coerce
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('The exact Android version code (`versionCode`) that the bundle does not support.'),
|
|
32
|
+
appId: z
|
|
33
|
+
.string({
|
|
34
|
+
message: 'App ID must be a UUID.',
|
|
35
|
+
})
|
|
36
|
+
.uuid({
|
|
37
|
+
message: 'App ID must be a UUID.',
|
|
38
|
+
})
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('App ID to deploy to.'),
|
|
41
|
+
channel: z.string().optional().describe('Channel to associate the bundle with.'),
|
|
42
|
+
commitMessage: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('The commit message related to the bundle. Deprecated, use `--git-ref` instead.'),
|
|
46
|
+
commitRef: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('The commit ref related to the bundle. Deprecated, use `--git-ref` instead.'),
|
|
50
|
+
commitSha: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('The commit sha related to the bundle. Deprecated, use `--git-ref` instead.'),
|
|
54
|
+
customProperty: z
|
|
55
|
+
.array(z.string().min(1).max(100))
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('A custom property to assign to the bundle. Must be in the format `key=value`. Can be specified multiple times.'),
|
|
58
|
+
expiresInDays: z.coerce
|
|
59
|
+
.number({
|
|
60
|
+
message: 'Expiration days must be an integer.',
|
|
61
|
+
})
|
|
62
|
+
.int({
|
|
63
|
+
message: 'Expiration days must be an integer.',
|
|
64
|
+
})
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('The number of days until the bundle is automatically deleted.'),
|
|
67
|
+
gitRef: z
|
|
68
|
+
.string()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe('The Git reference (branch, tag, or commit SHA) to associate with the bundle.'),
|
|
71
|
+
iosMax: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('The maximum iOS bundle version (`CFBundleVersion`) that the bundle supports.'),
|
|
75
|
+
iosMin: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('The minimum iOS bundle version (`CFBundleVersion`) that the bundle supports.'),
|
|
79
|
+
iosEq: z
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('The exact iOS bundle version (`CFBundleVersion`) that the bundle does not support.'),
|
|
83
|
+
path: z.string().optional().describe('Path to zip file for code signing only.'),
|
|
84
|
+
privateKey: z
|
|
85
|
+
.string()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('The private key to sign the bundle with. Can be a file path to a .pem file or the private key content as plain text.'),
|
|
88
|
+
rolloutPercentage: z.coerce
|
|
89
|
+
.number()
|
|
90
|
+
.int({
|
|
91
|
+
message: 'Percentage must be an integer.',
|
|
92
|
+
})
|
|
93
|
+
.min(0, {
|
|
94
|
+
message: 'Percentage must be at least 0.',
|
|
95
|
+
})
|
|
96
|
+
.max(100, {
|
|
97
|
+
message: 'Percentage must be at most 100.',
|
|
98
|
+
})
|
|
99
|
+
.optional()
|
|
100
|
+
.describe('The percentage of devices to deploy the bundle to. Must be an integer between 0 and 100.'),
|
|
101
|
+
url: z.string().optional().describe('The url to the self-hosted bundle file.'),
|
|
102
|
+
yes: z.boolean().optional().describe('Skip confirmation prompts.'),
|
|
103
|
+
}), { y: 'yes' }),
|
|
104
|
+
action: async (options, args) => {
|
|
105
|
+
let { androidEq, androidMax, androidMin, appId, channel, commitMessage, commitRef, commitSha, customProperty, expiresInDays, gitRef, iosEq, iosMax, iosMin, path, privateKey, rolloutPercentage, url, } = options;
|
|
106
|
+
// Check if the user is logged in
|
|
107
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
108
|
+
consola.error('You must be logged in to run this command. Please run the `login` command first.');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
// Calculate the expiration date
|
|
112
|
+
let expiresAt;
|
|
113
|
+
if (expiresInDays) {
|
|
114
|
+
const expiresAtDate = new Date();
|
|
115
|
+
expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
|
|
116
|
+
expiresAt = expiresAtDate.toISOString();
|
|
117
|
+
}
|
|
118
|
+
// Prompt for url if not provided
|
|
119
|
+
if (!url) {
|
|
120
|
+
if (!isInteractive()) {
|
|
121
|
+
consola.error('You must provide a url when running in non-interactive environment.');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
url = await prompt('Enter the URL to the self-hosted bundle file:', {
|
|
126
|
+
type: 'text',
|
|
127
|
+
});
|
|
128
|
+
if (!url) {
|
|
129
|
+
consola.error('You must provide a url to the self-hosted bundle file.');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Prompt for appId if not provided
|
|
135
|
+
if (!appId) {
|
|
136
|
+
if (!isInteractive()) {
|
|
137
|
+
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
const organizations = await organizationsService.findAll();
|
|
141
|
+
if (organizations.length === 0) {
|
|
142
|
+
consola.error('You must create an organization before registering a bundle.');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
146
|
+
const organizationId = await prompt('Select the organization of the app for which you want to register a bundle.', {
|
|
147
|
+
type: 'select',
|
|
148
|
+
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
|
|
149
|
+
});
|
|
150
|
+
if (!organizationId) {
|
|
151
|
+
consola.error('You must select the organization of an app for which you want to register a bundle.');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const apps = await appsService.findAll({
|
|
155
|
+
organizationId,
|
|
156
|
+
});
|
|
157
|
+
if (apps.length === 0) {
|
|
158
|
+
consola.error('You must create an app before registering a bundle.');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
162
|
+
appId = await prompt('Which app do you want to deploy to:', {
|
|
163
|
+
type: 'select',
|
|
164
|
+
options: apps.map((app) => ({ label: app.name, value: app.id })),
|
|
165
|
+
});
|
|
166
|
+
if (!appId) {
|
|
167
|
+
consola.error('You must select an app to deploy to.');
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Prompt for channel if interactive
|
|
172
|
+
if (!channel && !options.yes && isInteractive()) {
|
|
173
|
+
const shouldDeployToChannel = await prompt('Do you want to deploy to a specific channel?', {
|
|
174
|
+
type: 'confirm',
|
|
175
|
+
initial: false,
|
|
176
|
+
});
|
|
177
|
+
if (shouldDeployToChannel) {
|
|
178
|
+
channel = await prompt('Enter the channel name:', {
|
|
179
|
+
type: 'text',
|
|
180
|
+
});
|
|
181
|
+
if (!channel) {
|
|
182
|
+
consola.error('The channel name must be at least one character long.');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Handle checksum and signature generation if path is provided
|
|
188
|
+
let checksum;
|
|
189
|
+
let signature;
|
|
190
|
+
if (path) {
|
|
191
|
+
// Validate that path is a zip file
|
|
192
|
+
if (!zip.isZipped(path)) {
|
|
193
|
+
consola.error('The path must be a zip file when providing a URL.');
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
// Check if the path exists
|
|
197
|
+
const pathExists = await fileExistsAtPath(path);
|
|
198
|
+
if (!pathExists) {
|
|
199
|
+
consola.error(`The path does not exist.`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
// Create the file buffer
|
|
203
|
+
const fileBuffer = await createBufferFromPath(path);
|
|
204
|
+
// Generate checksum
|
|
205
|
+
checksum = await createHash(fileBuffer);
|
|
206
|
+
// Handle private key for signing
|
|
207
|
+
if (privateKey) {
|
|
208
|
+
let privateKeyBuffer;
|
|
209
|
+
if (isPrivateKeyContent(privateKey)) {
|
|
210
|
+
// Handle plain text private key content
|
|
211
|
+
const formattedPrivateKey = formatPrivateKey(privateKey);
|
|
212
|
+
privateKeyBuffer = createBufferFromString(formattedPrivateKey);
|
|
213
|
+
}
|
|
214
|
+
else if (privateKey.endsWith('.pem')) {
|
|
215
|
+
// Handle file path
|
|
216
|
+
const fileExists = await fileExistsAtPath(privateKey);
|
|
217
|
+
if (fileExists) {
|
|
218
|
+
const keyBuffer = await createBufferFromPath(privateKey);
|
|
219
|
+
const keyContent = keyBuffer.toString('utf8');
|
|
220
|
+
const formattedPrivateKey = formatPrivateKey(keyContent);
|
|
221
|
+
privateKeyBuffer = createBufferFromString(formattedPrivateKey);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
consola.error('Private key file not found.');
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
consola.error('Private key must be either a path to a .pem file or the private key content as plain text.');
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
// Sign the bundle
|
|
233
|
+
signature = await createSignature(privateKeyBuffer, fileBuffer);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Get app details for confirmation
|
|
237
|
+
const app = await appsService.findOne({ appId });
|
|
238
|
+
const appName = app.name;
|
|
239
|
+
// Final confirmation before registering bundle
|
|
240
|
+
if (!options.yes && isInteractive()) {
|
|
241
|
+
const confirmed = await prompt(`Are you sure you want to register bundle from URL "${url}" for app "${appName}" (${appId})?`, {
|
|
242
|
+
type: 'confirm',
|
|
243
|
+
});
|
|
244
|
+
if (!confirmed) {
|
|
245
|
+
consola.info('Bundle registration cancelled.');
|
|
246
|
+
process.exit(0);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Create the app bundle
|
|
250
|
+
consola.start('Registering bundle...');
|
|
251
|
+
const response = await appBundlesService.create({
|
|
252
|
+
appId,
|
|
253
|
+
artifactType: 'zip',
|
|
254
|
+
channelName: channel,
|
|
255
|
+
checksum,
|
|
256
|
+
eqAndroidAppVersionCode: androidEq,
|
|
257
|
+
eqIosAppVersionCode: iosEq,
|
|
258
|
+
gitCommitMessage: commitMessage,
|
|
259
|
+
gitCommitRef: commitRef,
|
|
260
|
+
gitCommitSha: commitSha,
|
|
261
|
+
gitRef,
|
|
262
|
+
customProperties: parseCustomProperties(customProperty),
|
|
263
|
+
expiresAt,
|
|
264
|
+
url,
|
|
265
|
+
maxAndroidAppVersionCode: androidMax,
|
|
266
|
+
maxIosAppVersionCode: iosMax,
|
|
267
|
+
minAndroidAppVersionCode: androidMin,
|
|
268
|
+
minIosAppVersionCode: iosMin,
|
|
269
|
+
rolloutPercentage: (rolloutPercentage ?? 100) / 100,
|
|
270
|
+
signature,
|
|
271
|
+
});
|
|
272
|
+
consola.info(`Bundle Artifact ID: ${response.id}`);
|
|
273
|
+
if (response.appDeploymentId) {
|
|
274
|
+
consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${response.appDeploymentId}`);
|
|
275
|
+
}
|
|
276
|
+
consola.success('Live Update successfully registered.');
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const parseCustomProperties = (customProperty) => {
|
|
280
|
+
let customProperties;
|
|
281
|
+
if (customProperty) {
|
|
282
|
+
customProperties = {};
|
|
283
|
+
for (const property of customProperty) {
|
|
284
|
+
const [key, value] = property.split('=');
|
|
285
|
+
if (key && value) {
|
|
286
|
+
customProperties[key] = value;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return customProperties;
|
|
291
|
+
};
|