@capawesome/cli 3.7.0-dev.3ddf02d.1764834835 → 3.7.0-dev.445ea3a.1764843317
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/dist/commands/apps/bundles/create.js +39 -18
- package/dist/commands/apps/bundles/create.test.js +36 -8
- package/dist/commands/apps/liveupdates/setup.js +274 -0
- package/dist/index.js +1 -0
- package/dist/utils/capacitor-config-writer.js +123 -0
- package/dist/utils/capacitor-config.js +15 -3
- package/dist/utils/clipboard.js +34 -0
- package/dist/utils/openssl.js +40 -0
- package/dist/utils/package-json.js +37 -0
- package/dist/utils/package-manager.js +38 -0
- package/package.json +2 -2
|
@@ -5,7 +5,7 @@ import appsService from '../../../services/apps.js';
|
|
|
5
5
|
import authorizationService from '../../../services/authorization-service.js';
|
|
6
6
|
import organizationsService from '../../../services/organizations.js';
|
|
7
7
|
import { createBufferFromPath, createBufferFromReadStream, createBufferFromString, isPrivateKeyContent, } from '../../../utils/buffer.js';
|
|
8
|
-
import { findCapacitorConfigPath,
|
|
8
|
+
import { findCapacitorConfigPath, getLiveUpdatePluginAppIdFromConfig, getLiveUpdatePluginPublicKeyFromConfig, getWebDirFromConfig, } from '../../../utils/capacitor-config.js';
|
|
9
9
|
import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
|
|
10
10
|
import { createHash } from '../../../utils/hash.js';
|
|
11
11
|
import { generateManifestJson } from '../../../utils/manifest.js';
|
|
@@ -18,9 +18,11 @@ import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
|
18
18
|
import { exec } from 'child_process';
|
|
19
19
|
import consola from 'consola';
|
|
20
20
|
import { createReadStream } from 'fs';
|
|
21
|
+
import pathModule from 'path';
|
|
21
22
|
import { hasTTY } from 'std-env';
|
|
22
23
|
import { promisify } from 'util';
|
|
23
24
|
import { z } from 'zod';
|
|
25
|
+
// Promisified exec for running build scripts
|
|
24
26
|
const execAsync = promisify(exec);
|
|
25
27
|
export default defineCommand({
|
|
26
28
|
description: 'Create a new app bundle.',
|
|
@@ -115,21 +117,24 @@ export default defineCommand({
|
|
|
115
117
|
expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
|
|
116
118
|
expiresAt = expiresAtDate.toISOString();
|
|
117
119
|
}
|
|
120
|
+
// Try to auto-detect webDir from Capacitor configuration
|
|
121
|
+
const capacitorConfigPath = await findCapacitorConfigPath();
|
|
118
122
|
// Check that either a path or a url is provided
|
|
119
123
|
if (!path && !url) {
|
|
120
|
-
// Try to auto-detect webDir from Capacitor
|
|
121
|
-
const capacitorConfigPath = await findCapacitorConfigPath();
|
|
124
|
+
// Try to auto-detect webDir from Capacitor configuration
|
|
122
125
|
if (capacitorConfigPath) {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
+
const webDirPath = await getWebDirFromConfig(capacitorConfigPath);
|
|
127
|
+
if (webDirPath) {
|
|
128
|
+
const relativeWebDirPath = pathModule.relative(process.cwd(), webDirPath);
|
|
129
|
+
consola.success(`Auto-detected web asset directory "${relativeWebDirPath}" from Capacitor configuration.`);
|
|
130
|
+
path = webDirPath;
|
|
126
131
|
}
|
|
127
132
|
else {
|
|
128
|
-
consola.warn('No
|
|
133
|
+
consola.warn('No web asset directory found in Capacitor configuration (`webDir`).');
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
else {
|
|
132
|
-
consola.warn('No Capacitor
|
|
137
|
+
consola.warn('No Capacitor configuration found to auto-detect web asset directory.');
|
|
133
138
|
}
|
|
134
139
|
// If still no path, prompt the user
|
|
135
140
|
if (!path) {
|
|
@@ -157,16 +162,16 @@ export default defineCommand({
|
|
|
157
162
|
else {
|
|
158
163
|
const buildScript = await getBuildScript(packageJsonPath);
|
|
159
164
|
if (!buildScript) {
|
|
160
|
-
consola.warn('No build script found in package.json.');
|
|
165
|
+
consola.warn('No build script (`capawesome:build` or `build`) found in package.json.');
|
|
161
166
|
}
|
|
162
167
|
else if (hasTTY) {
|
|
163
|
-
const shouldBuild = await prompt('Do you want to rebuild your web assets before
|
|
168
|
+
const shouldBuild = await prompt('Do you want to rebuild your web assets before proceeding?', {
|
|
164
169
|
type: 'select',
|
|
165
170
|
options: ['Yes', 'No'],
|
|
166
171
|
});
|
|
167
172
|
if (shouldBuild === 'Yes') {
|
|
168
173
|
try {
|
|
169
|
-
consola.start(`Running
|
|
174
|
+
consola.start(`Running \`${buildScript.name}\` script...`);
|
|
170
175
|
const { stdout, stderr } = await execAsync(`npm run ${buildScript.name}`);
|
|
171
176
|
if (stdout) {
|
|
172
177
|
console.log(stdout);
|
|
@@ -229,20 +234,23 @@ export default defineCommand({
|
|
|
229
234
|
consola.error('It is not yet possible to provide a URL when creating a bundle with an artifact type of `manifest`.');
|
|
230
235
|
process.exit(1);
|
|
231
236
|
}
|
|
237
|
+
// Track if we found a Capacitor configuration but no app ID (for showing setup hint later)
|
|
238
|
+
let hasCapacitorConfigWithoutAppId = false;
|
|
232
239
|
if (!appId) {
|
|
233
|
-
// Try to auto-detect appId from Capacitor
|
|
234
|
-
const capacitorConfigPath = await findCapacitorConfigPath();
|
|
240
|
+
// Try to auto-detect appId from Capacitor configuration
|
|
235
241
|
if (capacitorConfigPath) {
|
|
236
|
-
const configAppId = await
|
|
242
|
+
const configAppId = await getLiveUpdatePluginAppIdFromConfig(capacitorConfigPath);
|
|
237
243
|
if (configAppId) {
|
|
244
|
+
consola.success(`Auto-detected Capawesome Cloud app ID "${configAppId}" from Capacitor configuration.`);
|
|
238
245
|
appId = configAppId;
|
|
239
246
|
}
|
|
240
247
|
else {
|
|
241
|
-
|
|
248
|
+
hasCapacitorConfigWithoutAppId = true;
|
|
249
|
+
consola.warn('No Capawesome Cloud app ID found in Capacitor configuration (`plugins.LiveUpdate.appId`).');
|
|
242
250
|
}
|
|
243
251
|
}
|
|
244
252
|
else {
|
|
245
|
-
consola.warn('No Capacitor
|
|
253
|
+
consola.warn('No Capacitor configuration found to auto-detect Capawesome Cloud app ID.');
|
|
246
254
|
}
|
|
247
255
|
// If still no appId, prompt the user
|
|
248
256
|
if (!appId) {
|
|
@@ -297,6 +305,13 @@ export default defineCommand({
|
|
|
297
305
|
}
|
|
298
306
|
}
|
|
299
307
|
}
|
|
308
|
+
// Check if public key is configured but no private key was provided
|
|
309
|
+
if (!privateKey && capacitorConfigPath) {
|
|
310
|
+
const publicKey = await getLiveUpdatePluginPublicKeyFromConfig(capacitorConfigPath);
|
|
311
|
+
if (publicKey) {
|
|
312
|
+
consola.warn('A public key for verifying the integrity of the bundles is configured in your Capacitor configuration, but no private key has been provided for signing this bundle.');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
300
315
|
// Create the private key buffer
|
|
301
316
|
let privateKeyBuffer;
|
|
302
317
|
if (privateKey) {
|
|
@@ -329,10 +344,11 @@ export default defineCommand({
|
|
|
329
344
|
const appName = app.name;
|
|
330
345
|
// Final confirmation before creating bundle
|
|
331
346
|
if (path && hasTTY) {
|
|
332
|
-
const
|
|
347
|
+
const relativePath = pathModule.relative(process.cwd(), path);
|
|
348
|
+
const confirmed = await prompt(`Are you sure you want to create a bundle from path "${relativePath}" for app "${appName}" (${appId})?`, {
|
|
333
349
|
type: 'confirm',
|
|
334
350
|
});
|
|
335
|
-
if (confirmed) {
|
|
351
|
+
if (!confirmed) {
|
|
336
352
|
consola.info('Bundle creation cancelled.');
|
|
337
353
|
process.exit(0);
|
|
338
354
|
}
|
|
@@ -405,6 +421,11 @@ export default defineCommand({
|
|
|
405
421
|
}
|
|
406
422
|
consola.success('Bundle successfully created.');
|
|
407
423
|
consola.info(`Bundle ID: ${response.id}`);
|
|
424
|
+
// Show setup hint if Capacitor configuration exists but no app ID was configured
|
|
425
|
+
if (hasCapacitorConfigWithoutAppId) {
|
|
426
|
+
console.log(); // New line for better readability
|
|
427
|
+
consola.info('To set up the Live Update SDK in your Capacitor app, run the `apps:liveupdates:setup` command.');
|
|
428
|
+
}
|
|
408
429
|
}
|
|
409
430
|
catch (error) {
|
|
410
431
|
if (appBundleId) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_API_BASE_URL } from '../../../config/consts.js';
|
|
2
2
|
import authorizationService from '../../../services/authorization-service.js';
|
|
3
|
+
import { findCapacitorConfigPath } from '../../../utils/capacitor-config.js';
|
|
3
4
|
import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
|
|
5
|
+
import { findPackageJsonPath } from '../../../utils/package-json.js';
|
|
4
6
|
import userConfig from '../../../utils/user-config.js';
|
|
5
7
|
import consola from 'consola';
|
|
6
8
|
import nock from 'nock';
|
|
@@ -15,6 +17,8 @@ vi.mock('@/utils/buffer.js');
|
|
|
15
17
|
vi.mock('@/utils/private-key.js');
|
|
16
18
|
vi.mock('@/utils/hash.js');
|
|
17
19
|
vi.mock('@/utils/signature.js');
|
|
20
|
+
vi.mock('@/utils/capacitor-config.js');
|
|
21
|
+
vi.mock('@/utils/package-json.js');
|
|
18
22
|
vi.mock('consola');
|
|
19
23
|
describe('apps-bundles-create', () => {
|
|
20
24
|
const mockUserConfig = vi.mocked(userConfig);
|
|
@@ -22,12 +26,16 @@ describe('apps-bundles-create', () => {
|
|
|
22
26
|
const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
|
|
23
27
|
const mockGetFilesInDirectoryAndSubdirectories = vi.mocked(getFilesInDirectoryAndSubdirectories);
|
|
24
28
|
const mockIsDirectory = vi.mocked(isDirectory);
|
|
29
|
+
const mockFindCapacitorConfigPath = vi.mocked(findCapacitorConfigPath);
|
|
30
|
+
const mockFindPackageJsonPath = vi.mocked(findPackageJsonPath);
|
|
25
31
|
const mockConsola = vi.mocked(consola);
|
|
26
32
|
beforeEach(() => {
|
|
27
33
|
vi.clearAllMocks();
|
|
28
34
|
mockUserConfig.read.mockReturnValue({ token: 'test-token' });
|
|
29
35
|
mockAuthorizationService.hasAuthorizationToken.mockReturnValue(true);
|
|
30
36
|
mockAuthorizationService.getCurrentAuthorizationToken.mockReturnValue('test-token');
|
|
37
|
+
mockFindCapacitorConfigPath.mockResolvedValue(undefined);
|
|
38
|
+
mockFindPackageJsonPath.mockResolvedValue(undefined);
|
|
31
39
|
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
32
40
|
throw new Error(`Process exited with code ${code}`);
|
|
33
41
|
});
|
|
@@ -67,7 +75,11 @@ describe('apps-bundles-create', () => {
|
|
|
67
75
|
vi.mocked(mockZip.default.isZipped).mockReturnValue(true);
|
|
68
76
|
vi.mocked(mockBuffer.createBufferFromPath).mockResolvedValue(testBuffer);
|
|
69
77
|
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
|
|
70
|
-
const
|
|
78
|
+
const appScope = nock(DEFAULT_API_BASE_URL)
|
|
79
|
+
.get(`/v1/apps/${appId}`)
|
|
80
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
81
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
82
|
+
const bundleScope = nock(DEFAULT_API_BASE_URL)
|
|
71
83
|
.post(`/v1/apps/${appId}/bundles`, {
|
|
72
84
|
appId,
|
|
73
85
|
url: bundleUrl,
|
|
@@ -78,7 +90,8 @@ describe('apps-bundles-create', () => {
|
|
|
78
90
|
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
79
91
|
.reply(201, { id: bundleId });
|
|
80
92
|
await createBundleCommand.action(options, undefined);
|
|
81
|
-
expect(
|
|
93
|
+
expect(appScope.isDone()).toBe(true);
|
|
94
|
+
expect(bundleScope.isDone()).toBe(true);
|
|
82
95
|
expect(mockConsola.success).toHaveBeenCalledWith('Bundle successfully created.');
|
|
83
96
|
expect(mockConsola.info).toHaveBeenCalledWith(`Bundle ID: ${bundleId}`);
|
|
84
97
|
});
|
|
@@ -129,12 +142,17 @@ describe('apps-bundles-create', () => {
|
|
|
129
142
|
artifactType: 'zip',
|
|
130
143
|
rollout: 1,
|
|
131
144
|
};
|
|
132
|
-
const
|
|
145
|
+
const appScope = nock(DEFAULT_API_BASE_URL)
|
|
146
|
+
.get(`/v1/apps/${appId}`)
|
|
147
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
148
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
149
|
+
const bundleScope = nock(DEFAULT_API_BASE_URL)
|
|
133
150
|
.post(`/v1/apps/${appId}/bundles`)
|
|
134
151
|
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
135
152
|
.reply(400, { message: 'Invalid bundle data' });
|
|
136
153
|
await expect(createBundleCommand.action(options, undefined)).rejects.toThrow();
|
|
137
|
-
expect(
|
|
154
|
+
expect(appScope.isDone()).toBe(true);
|
|
155
|
+
expect(bundleScope.isDone()).toBe(true);
|
|
138
156
|
});
|
|
139
157
|
it('should handle private key file path', async () => {
|
|
140
158
|
const appId = 'app-123';
|
|
@@ -175,7 +193,11 @@ describe('apps-bundles-create', () => {
|
|
|
175
193
|
vi.mocked(mockBuffer.createBufferFromString).mockReturnValue(testBuffer);
|
|
176
194
|
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
|
|
177
195
|
vi.mocked(mockSignature.createSignature).mockResolvedValue(testSignature);
|
|
178
|
-
const
|
|
196
|
+
const appScope = nock(DEFAULT_API_BASE_URL)
|
|
197
|
+
.get(`/v1/apps/${appId}`)
|
|
198
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
199
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
200
|
+
const bundleScope = nock(DEFAULT_API_BASE_URL)
|
|
179
201
|
.post(`/v1/apps/${appId}/bundles`, {
|
|
180
202
|
appId,
|
|
181
203
|
url: bundleUrl,
|
|
@@ -187,7 +209,8 @@ describe('apps-bundles-create', () => {
|
|
|
187
209
|
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
188
210
|
.reply(201, { id: bundleId });
|
|
189
211
|
await createBundleCommand.action(options, undefined);
|
|
190
|
-
expect(
|
|
212
|
+
expect(appScope.isDone()).toBe(true);
|
|
213
|
+
expect(bundleScope.isDone()).toBe(true);
|
|
191
214
|
expect(mockConsola.success).toHaveBeenCalledWith('Bundle successfully created.');
|
|
192
215
|
});
|
|
193
216
|
it('should handle private key plain text content', async () => {
|
|
@@ -223,7 +246,11 @@ describe('apps-bundles-create', () => {
|
|
|
223
246
|
vi.mocked(mockPrivateKey.formatPrivateKey).mockReturnValue('formatted-private-key');
|
|
224
247
|
vi.mocked(mockHash.createHash).mockResolvedValue(testHash);
|
|
225
248
|
vi.mocked(mockSignature.createSignature).mockResolvedValue(testSignature);
|
|
226
|
-
const
|
|
249
|
+
const appScope = nock(DEFAULT_API_BASE_URL)
|
|
250
|
+
.get(`/v1/apps/${appId}`)
|
|
251
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
252
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
253
|
+
const bundleScope = nock(DEFAULT_API_BASE_URL)
|
|
227
254
|
.post(`/v1/apps/${appId}/bundles`, {
|
|
228
255
|
appId,
|
|
229
256
|
url: bundleUrl,
|
|
@@ -235,7 +262,8 @@ describe('apps-bundles-create', () => {
|
|
|
235
262
|
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
236
263
|
.reply(201, { id: bundleId });
|
|
237
264
|
await createBundleCommand.action(options, undefined);
|
|
238
|
-
expect(
|
|
265
|
+
expect(appScope.isDone()).toBe(true);
|
|
266
|
+
expect(bundleScope.isDone()).toBe(true);
|
|
239
267
|
expect(mockConsola.success).toHaveBeenCalledWith('Bundle successfully created.');
|
|
240
268
|
});
|
|
241
269
|
it('should handle private key file not found', async () => {
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import appsService from '../../../services/apps.js';
|
|
2
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
3
|
+
import organizationsService from '../../../services/organizations.js';
|
|
4
|
+
import { updateCapacitorConfig } from '../../../utils/capacitor-config-writer.js';
|
|
5
|
+
import { findCapacitorConfigPath, readCapacitorConfig } from '../../../utils/capacitor-config.js';
|
|
6
|
+
import { copyToClipboard } from '../../../utils/clipboard.js';
|
|
7
|
+
import { fileExistsAtPath } from '../../../utils/file.js';
|
|
8
|
+
import { checkOpensslInstalled, generateKeyPair, readPublicKey } from '../../../utils/openssl.js';
|
|
9
|
+
import { findPackageJsonPath, getCapacitorMajorVersion, isPackageInstalled } from '../../../utils/package-json.js';
|
|
10
|
+
import { installPackage } from '../../../utils/package-manager.js';
|
|
11
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
12
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
13
|
+
import consola from 'consola';
|
|
14
|
+
import { hasTTY } from 'std-env';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
export default defineCommand({
|
|
17
|
+
description: 'Set up Live Updates in your Capacitor app.',
|
|
18
|
+
options: defineOptions(z.object({
|
|
19
|
+
appId: z.string().uuid().optional().describe('Capawesome Cloud app ID.'),
|
|
20
|
+
})),
|
|
21
|
+
action: async (options, args) => {
|
|
22
|
+
// Check if running in interactive mode
|
|
23
|
+
if (!hasTTY) {
|
|
24
|
+
consola.error('This command requires an interactive terminal. Please run it in an interactive environment.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
let { appId } = options;
|
|
28
|
+
// Step 1: Check if Capacitor config exists
|
|
29
|
+
const capacitorConfigPath = await findCapacitorConfigPath();
|
|
30
|
+
if (!capacitorConfigPath) {
|
|
31
|
+
consola.error('No Capacitor configuration found. Make sure you are in a Capacitor project.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
// Step 2: Read current config
|
|
35
|
+
let config = await readCapacitorConfig(capacitorConfigPath);
|
|
36
|
+
if (!config) {
|
|
37
|
+
config = {};
|
|
38
|
+
}
|
|
39
|
+
// Step 3: Select/Configure Capawesome Cloud App ID
|
|
40
|
+
// Try to get from config first
|
|
41
|
+
if (!appId && config?.plugins?.LiveUpdate?.appId) {
|
|
42
|
+
appId = config.plugins.LiveUpdate.appId;
|
|
43
|
+
consola.info(`Found existing Capawesome Cloud app ID in Capacitor configuration: ${appId}`);
|
|
44
|
+
}
|
|
45
|
+
// Prompt if still not found
|
|
46
|
+
if (!appId) {
|
|
47
|
+
// Check authorization
|
|
48
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
49
|
+
consola.error('You must be logged in. Please run the `login` command first.');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
// Prompt for organization
|
|
53
|
+
const organizations = await organizationsService.findAll();
|
|
54
|
+
if (organizations.length === 0) {
|
|
55
|
+
consola.error('You must create an organization before setting up Live Updates.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
59
|
+
const organizationId = await prompt('Select the organization of your app:', {
|
|
60
|
+
type: 'select',
|
|
61
|
+
options: organizations.map((org) => ({
|
|
62
|
+
label: org.name,
|
|
63
|
+
value: org.id,
|
|
64
|
+
})),
|
|
65
|
+
});
|
|
66
|
+
if (!organizationId) {
|
|
67
|
+
consola.error('You must select an organization.');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
// Prompt for app
|
|
71
|
+
const apps = await appsService.findAll({ organizationId });
|
|
72
|
+
if (apps.length === 0) {
|
|
73
|
+
consola.error('You must create an app before setting up Live Updates.');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
77
|
+
appId = await prompt('Select your app:', {
|
|
78
|
+
type: 'select',
|
|
79
|
+
options: apps.map((app) => ({
|
|
80
|
+
label: app.name,
|
|
81
|
+
value: app.id,
|
|
82
|
+
})),
|
|
83
|
+
});
|
|
84
|
+
if (!appId) {
|
|
85
|
+
consola.error('You must select an app.');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!config?.plugins?.LiveUpdate?.appId) {
|
|
90
|
+
// Update config with app ID
|
|
91
|
+
await updateCapacitorConfig(capacitorConfigPath, {
|
|
92
|
+
plugins: {
|
|
93
|
+
LiveUpdate: {
|
|
94
|
+
appId,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
consola.success(`Updated Capacitor configuration with Capawesome Cloud app ID: ${appId}`);
|
|
99
|
+
}
|
|
100
|
+
// Step 4: Install SDK (Optional)
|
|
101
|
+
const packageJsonPath = await findPackageJsonPath();
|
|
102
|
+
let sdkInstalled = false;
|
|
103
|
+
if (packageJsonPath) {
|
|
104
|
+
sdkInstalled = await isPackageInstalled(packageJsonPath, '@capawesome/capacitor-live-update');
|
|
105
|
+
if (sdkInstalled) {
|
|
106
|
+
consola.info('Live Update SDK is already installed.');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!sdkInstalled) {
|
|
110
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
111
|
+
const shouldInstall = await prompt('Do you want to install the Live Update SDK?', {
|
|
112
|
+
type: 'confirm',
|
|
113
|
+
initial: true,
|
|
114
|
+
});
|
|
115
|
+
if (shouldInstall) {
|
|
116
|
+
// Get Capacitor version to install matching plugin version
|
|
117
|
+
let packageName = '@capawesome/capacitor-live-update';
|
|
118
|
+
if (packageJsonPath) {
|
|
119
|
+
const capacitorMajorVersion = await getCapacitorMajorVersion(packageJsonPath);
|
|
120
|
+
if (capacitorMajorVersion) {
|
|
121
|
+
packageName = `@capawesome/capacitor-live-update@^${capacitorMajorVersion}.0.0`;
|
|
122
|
+
consola.info(`Installing plugin version ${capacitorMajorVersion} to match Capacitor ${capacitorMajorVersion}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
126
|
+
const packageManager = await prompt('Select your package manager:', {
|
|
127
|
+
type: 'select',
|
|
128
|
+
options: [
|
|
129
|
+
{ label: 'npm', value: 'npm' },
|
|
130
|
+
{ label: 'yarn', value: 'yarn' },
|
|
131
|
+
{ label: 'pnpm', value: 'pnpm' },
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
await installPackage(packageName, packageManager);
|
|
135
|
+
consola.success('Live Update SDK installed successfully.');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Step 5: Set Up Code Signing (Optional)
|
|
139
|
+
// Reload config to get latest updates
|
|
140
|
+
config = await readCapacitorConfig(capacitorConfigPath);
|
|
141
|
+
const hasPublicKey = !!config?.plugins?.LiveUpdate?.publicKey;
|
|
142
|
+
if (!hasPublicKey) {
|
|
143
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
144
|
+
const shouldSetupSigning = await prompt('Do you want to set up code signing? (Recommended for production, optional for testing)', {
|
|
145
|
+
type: 'confirm',
|
|
146
|
+
initial: false,
|
|
147
|
+
});
|
|
148
|
+
if (shouldSetupSigning) {
|
|
149
|
+
// Check OpenSSL
|
|
150
|
+
const hasOpenssl = await checkOpensslInstalled();
|
|
151
|
+
if (!hasOpenssl) {
|
|
152
|
+
consola.error('OpenSSL is not installed. Please install OpenSSL to generate key pairs.');
|
|
153
|
+
consola.info('Visit https://www.openssl.org for installation instructions.');
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
// Define key paths
|
|
157
|
+
const privateKeyPath = 'keypair.pem';
|
|
158
|
+
const publicKeyPath = 'publickey.crt';
|
|
159
|
+
// Check if files exist
|
|
160
|
+
const privateKeyExists = await fileExistsAtPath(privateKeyPath);
|
|
161
|
+
const publicKeyExists = await fileExistsAtPath(publicKeyPath);
|
|
162
|
+
let shouldGenerate = true;
|
|
163
|
+
if (privateKeyExists || publicKeyExists) {
|
|
164
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
165
|
+
const overwrite = await prompt('Key files already exist. Overwrite them?', {
|
|
166
|
+
type: 'confirm',
|
|
167
|
+
initial: false,
|
|
168
|
+
});
|
|
169
|
+
shouldGenerate = overwrite;
|
|
170
|
+
}
|
|
171
|
+
if (shouldGenerate) {
|
|
172
|
+
// Generate keys
|
|
173
|
+
consola.start('Generating RSA key pair...');
|
|
174
|
+
await generateKeyPair(privateKeyPath, publicKeyPath);
|
|
175
|
+
consola.success('Key pair generated successfully.');
|
|
176
|
+
// Read public key
|
|
177
|
+
const publicKey = await readPublicKey(publicKeyPath);
|
|
178
|
+
// Update config
|
|
179
|
+
await updateCapacitorConfig(capacitorConfigPath, {
|
|
180
|
+
plugins: {
|
|
181
|
+
LiveUpdate: {
|
|
182
|
+
publicKey,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
consola.success('Updated Capacitor configuration with public key.');
|
|
187
|
+
// Show important instructions
|
|
188
|
+
console.log(); // Blank line
|
|
189
|
+
consola.box(`IMPORTANT: Keep your private key safe!\n\n` +
|
|
190
|
+
`Private key location: ${privateKeyPath}\n` +
|
|
191
|
+
`Public key location: ${publicKeyPath}\n\n` +
|
|
192
|
+
`When creating bundles, use:\n` +
|
|
193
|
+
`npx @capawesome/cli apps:bundles:create --private-key ${privateKeyPath}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Step 6: Set Up Auto Rollback (Optional)
|
|
198
|
+
// Reload config again to get latest updates
|
|
199
|
+
config = await readCapacitorConfig(capacitorConfigPath);
|
|
200
|
+
const hasReadyTimeout = config?.plugins?.LiveUpdate?.readyTimeout !== undefined;
|
|
201
|
+
if (!hasReadyTimeout) {
|
|
202
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
203
|
+
const shouldSetupRollback = await prompt('Do you want to set up automatic rollbacks? (Recommended for production, optional for testing)', {
|
|
204
|
+
type: 'confirm',
|
|
205
|
+
initial: false,
|
|
206
|
+
});
|
|
207
|
+
if (shouldSetupRollback) {
|
|
208
|
+
const timeoutStr = await prompt('Enter ready timeout in milliseconds (recommended: 10000):', {
|
|
209
|
+
type: 'text',
|
|
210
|
+
initial: '10000',
|
|
211
|
+
});
|
|
212
|
+
const readyTimeout = parseInt(timeoutStr, 10);
|
|
213
|
+
if (isNaN(readyTimeout) || readyTimeout <= 0) {
|
|
214
|
+
consola.error('Invalid timeout value. Must be a positive number.');
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
// Update config
|
|
218
|
+
await updateCapacitorConfig(capacitorConfigPath, {
|
|
219
|
+
plugins: {
|
|
220
|
+
LiveUpdate: {
|
|
221
|
+
readyTimeout,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
consola.success(`Updated Capacitor configuration with ready timeout: ${readyTimeout}ms`);
|
|
226
|
+
// Show code snippet
|
|
227
|
+
console.log(); // Blank line
|
|
228
|
+
consola.box(`IMPORTANT: Add this code to your app!\n\n` +
|
|
229
|
+
`Call this as soon as possible during app startup:\n\n` +
|
|
230
|
+
`import { LiveUpdate } from '@capawesome/capacitor-live-update';\n\n` +
|
|
231
|
+
`LiveUpdate.ready();`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Step 7: Final Message
|
|
235
|
+
console.log(); // Blank line
|
|
236
|
+
consola.success('Live Updates setup completed!');
|
|
237
|
+
consola.info('Next steps:');
|
|
238
|
+
consola.info('1. Sync your Capacitor project: `npx cap sync`');
|
|
239
|
+
consola.info('2. Create your first bundle: `npx @capawesome/cli apps:bundles:create`');
|
|
240
|
+
consola.info('3. Download and apply the latest bundle in your app by calling the `sync(...)` method:');
|
|
241
|
+
console.log(); // Blank line
|
|
242
|
+
const syncCode = `import { LiveUpdate } from '@capawesome/capacitor-live-update';\n\n` +
|
|
243
|
+
`const sync = async () => {\n` +
|
|
244
|
+
` // Automatically download the latest update\n` +
|
|
245
|
+
` const { nextBundleId } = await LiveUpdate.sync();\n` +
|
|
246
|
+
` if (nextBundleId) {\n` +
|
|
247
|
+
` // Reload the app to apply the update\n` +
|
|
248
|
+
` await LiveUpdate.reload();\n` +
|
|
249
|
+
` }\n` +
|
|
250
|
+
`};`;
|
|
251
|
+
consola.box(syncCode);
|
|
252
|
+
// Ask if user wants to copy to clipboard
|
|
253
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
254
|
+
const shouldCopy = await prompt('Do you want to copy this code to your clipboard?', {
|
|
255
|
+
type: 'confirm',
|
|
256
|
+
initial: true,
|
|
257
|
+
});
|
|
258
|
+
if (shouldCopy) {
|
|
259
|
+
try {
|
|
260
|
+
await copyToClipboard(syncCode);
|
|
261
|
+
consola.success('Code copied to clipboard!');
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
consola.warn('Failed to copy to clipboard. Please copy the code manually.');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Show helpful resources
|
|
268
|
+
console.log(); // Blank line
|
|
269
|
+
consola.log('Learn more:');
|
|
270
|
+
consola.log('- Plugin Documentation: https://capawesome.io/plugins/live-update/');
|
|
271
|
+
consola.log('- Update Strategies: https://capawesome.io/cloud/live-updates/guides/update-strategies/');
|
|
272
|
+
consola.log('- Best Practices: https://capawesome.io/cloud/live-updates/guides/best-practices/');
|
|
273
|
+
},
|
|
274
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ const config = defineConfig({
|
|
|
39
39
|
'apps:deployments:cancel': await import('./commands/apps/deployments/cancel.js').then((mod) => mod.default),
|
|
40
40
|
'apps:deployments:logs': await import('./commands/apps/deployments/logs.js').then((mod) => mod.default),
|
|
41
41
|
'apps:devices:delete': await import('./commands/apps/devices/delete.js').then((mod) => mod.default),
|
|
42
|
+
'apps:liveupdates:setup': await import('./commands/apps/liveupdates/setup.js').then((mod) => mod.default),
|
|
42
43
|
'manifests:generate': await import('./commands/manifests/generate.js').then((mod) => mod.default),
|
|
43
44
|
'organizations:create': await import('./commands/organizations/create.js').then((mod) => mod.default),
|
|
44
45
|
},
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { readCapacitorConfig } from './capacitor-config.js';
|
|
3
|
+
import { writeFile } from './file.js';
|
|
4
|
+
/**
|
|
5
|
+
* Deep merge two objects.
|
|
6
|
+
*/
|
|
7
|
+
const deepMerge = (target, source) => {
|
|
8
|
+
const result = { ...target };
|
|
9
|
+
for (const key in source) {
|
|
10
|
+
if (source[key] !== undefined) {
|
|
11
|
+
if (typeof source[key] === 'object' && !Array.isArray(source[key]) && source[key] !== null) {
|
|
12
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
result[key] = source[key];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Convert a value to a JavaScript object notation string.
|
|
23
|
+
*/
|
|
24
|
+
const toJSObjectString = (value, indent = 0) => {
|
|
25
|
+
const indentStr = ' '.repeat(indent);
|
|
26
|
+
const nextIndentStr = ' '.repeat(indent + 1);
|
|
27
|
+
if (value === null) {
|
|
28
|
+
return 'null';
|
|
29
|
+
}
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
return 'undefined';
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
// Handle multi-line strings (like public keys)
|
|
35
|
+
if (value.includes('\n')) {
|
|
36
|
+
// Use template literal for multi-line strings
|
|
37
|
+
return `\`${value}\``;
|
|
38
|
+
}
|
|
39
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
42
|
+
return String(value);
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
if (value.length === 0) {
|
|
46
|
+
return '[]';
|
|
47
|
+
}
|
|
48
|
+
const items = value.map((item) => `${nextIndentStr}${toJSObjectString(item, indent + 1)}`).join(',\n');
|
|
49
|
+
return `[\n${items}\n${indentStr}]`;
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'object') {
|
|
52
|
+
const keys = Object.keys(value);
|
|
53
|
+
if (keys.length === 0) {
|
|
54
|
+
return '{}';
|
|
55
|
+
}
|
|
56
|
+
const items = keys
|
|
57
|
+
.map((key) => {
|
|
58
|
+
const val = toJSObjectString(value[key], indent + 1);
|
|
59
|
+
return `${nextIndentStr}${key}: ${val}`;
|
|
60
|
+
})
|
|
61
|
+
.join(',\n');
|
|
62
|
+
return `{\n${items}\n${indentStr}}`;
|
|
63
|
+
}
|
|
64
|
+
return 'undefined';
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Write a Capacitor config to a JSON file.
|
|
68
|
+
*/
|
|
69
|
+
export const writeCapacitorConfigJson = async (configPath, config) => {
|
|
70
|
+
const jsonContent = JSON.stringify(config, null, 2);
|
|
71
|
+
await writeFile(configPath, jsonContent);
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Write a Capacitor config to a TypeScript file.
|
|
75
|
+
*/
|
|
76
|
+
export const writeCapacitorConfigTs = async (configPath, config) => {
|
|
77
|
+
// Read existing file to preserve reference comments
|
|
78
|
+
const existingContent = await fs.promises.readFile(configPath, 'utf-8');
|
|
79
|
+
// Extract reference comments
|
|
80
|
+
const referenceComments = [];
|
|
81
|
+
const lines = existingContent.split('\n');
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (line.trim().startsWith('///')) {
|
|
84
|
+
referenceComments.push(line.trim());
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Generate TypeScript content
|
|
88
|
+
const configObject = toJSObjectString(config, 0);
|
|
89
|
+
const tsContent = [
|
|
90
|
+
...referenceComments,
|
|
91
|
+
...(referenceComments.length > 0 ? [''] : []),
|
|
92
|
+
"import type { CapacitorConfig } from '@capacitor/cli';",
|
|
93
|
+
'',
|
|
94
|
+
'const config: CapacitorConfig = ' + configObject + ';',
|
|
95
|
+
'',
|
|
96
|
+
'export default config;',
|
|
97
|
+
'',
|
|
98
|
+
].join('\n');
|
|
99
|
+
await writeFile(configPath, tsContent);
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Update a Capacitor config file with partial updates.
|
|
103
|
+
* Performs a deep merge with existing config.
|
|
104
|
+
*/
|
|
105
|
+
export const updateCapacitorConfig = async (configPath, updates) => {
|
|
106
|
+
// Read current config
|
|
107
|
+
let currentConfig = await readCapacitorConfig(configPath);
|
|
108
|
+
if (!currentConfig) {
|
|
109
|
+
currentConfig = {};
|
|
110
|
+
}
|
|
111
|
+
// Merge updates
|
|
112
|
+
const mergedConfig = deepMerge(currentConfig, updates);
|
|
113
|
+
// Write based on file type
|
|
114
|
+
if (configPath.endsWith('.json')) {
|
|
115
|
+
await writeCapacitorConfigJson(configPath, mergedConfig);
|
|
116
|
+
}
|
|
117
|
+
else if (configPath.endsWith('.ts')) {
|
|
118
|
+
await writeCapacitorConfigTs(configPath, mergedConfig);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
throw new Error('Unsupported config file format');
|
|
122
|
+
}
|
|
123
|
+
};
|
|
@@ -37,14 +37,16 @@ export const readCapacitorConfig = async (configPath) => {
|
|
|
37
37
|
// Extract webDir using regex
|
|
38
38
|
const webDirMatch = content.match(/webDir:\s*['"]([^'"]+)['"]/);
|
|
39
39
|
const appIdMatch = content.match(/LiveUpdate:\s*{[^}]*appId:\s*['"]([^'"]+)['"]/s);
|
|
40
|
+
const publicKeyMatch = content.match(/LiveUpdate:\s*{[^}]*publicKey:\s*['"]([^'"]+)['"]/s);
|
|
40
41
|
const config = {};
|
|
41
42
|
if (webDirMatch) {
|
|
42
43
|
config.webDir = webDirMatch[1];
|
|
43
44
|
}
|
|
44
|
-
if (appIdMatch) {
|
|
45
|
+
if (appIdMatch || publicKeyMatch) {
|
|
45
46
|
config.plugins = {
|
|
46
47
|
LiveUpdate: {
|
|
47
|
-
appId: appIdMatch[1],
|
|
48
|
+
...(appIdMatch && { appId: appIdMatch[1] }),
|
|
49
|
+
...(publicKeyMatch && { publicKey: publicKeyMatch[1] }),
|
|
48
50
|
},
|
|
49
51
|
};
|
|
50
52
|
}
|
|
@@ -78,7 +80,17 @@ export const getWebDirFromConfig = async (configPath) => {
|
|
|
78
80
|
* @param configPath The path to the config file.
|
|
79
81
|
* @returns The appId, or undefined if not found.
|
|
80
82
|
*/
|
|
81
|
-
export const
|
|
83
|
+
export const getLiveUpdatePluginAppIdFromConfig = async (configPath) => {
|
|
82
84
|
const config = await readCapacitorConfig(configPath);
|
|
83
85
|
return config?.plugins?.LiveUpdate?.appId;
|
|
84
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* Get the LiveUpdate publicKey from the Capacitor config.
|
|
89
|
+
*
|
|
90
|
+
* @param configPath The path to the config file.
|
|
91
|
+
* @returns The publicKey, or undefined if not found.
|
|
92
|
+
*/
|
|
93
|
+
export const getLiveUpdatePluginPublicKeyFromConfig = async (configPath) => {
|
|
94
|
+
const config = await readCapacitorConfig(configPath);
|
|
95
|
+
return config?.plugins?.LiveUpdate?.publicKey;
|
|
96
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
/**
|
|
5
|
+
* Copy text to the system clipboard.
|
|
6
|
+
* Works on macOS, Linux, and Windows.
|
|
7
|
+
*
|
|
8
|
+
* @param text The text to copy to the clipboard.
|
|
9
|
+
*/
|
|
10
|
+
export const copyToClipboard = async (text) => {
|
|
11
|
+
const platform = process.platform;
|
|
12
|
+
try {
|
|
13
|
+
if (platform === 'darwin') {
|
|
14
|
+
// macOS
|
|
15
|
+
await execAsync(`echo "${text.replace(/"/g, '\\"')}" | pbcopy`);
|
|
16
|
+
}
|
|
17
|
+
else if (platform === 'win32') {
|
|
18
|
+
// Windows
|
|
19
|
+
await execAsync(`echo "${text.replace(/"/g, '\\"')}" | clip`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Linux - try xclip first, fall back to xsel
|
|
23
|
+
try {
|
|
24
|
+
await execAsync(`echo "${text.replace(/"/g, '\\"')}" | xclip -selection clipboard`);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
await execAsync(`echo "${text.replace(/"/g, '\\"')}" | xsel --clipboard --input`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error('Failed to copy to clipboard. Make sure clipboard utilities are installed.');
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
/**
|
|
6
|
+
* Check if OpenSSL is installed on the system.
|
|
7
|
+
*
|
|
8
|
+
* @returns True if OpenSSL is installed, false otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export const checkOpensslInstalled = async () => {
|
|
11
|
+
try {
|
|
12
|
+
await execAsync('openssl version');
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Generate an RSA key pair using OpenSSL.
|
|
21
|
+
*
|
|
22
|
+
* @param privateKeyPath Path where the private key will be saved.
|
|
23
|
+
* @param publicKeyPath Path where the public key will be saved.
|
|
24
|
+
*/
|
|
25
|
+
export const generateKeyPair = async (privateKeyPath, publicKeyPath) => {
|
|
26
|
+
// Generate private key
|
|
27
|
+
await execAsync(`openssl genrsa -out ${privateKeyPath} 2048`);
|
|
28
|
+
// Extract public key from private key
|
|
29
|
+
await execAsync(`openssl rsa -in ${privateKeyPath} -pubout -out ${publicKeyPath}`);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Read a public key from a file.
|
|
33
|
+
*
|
|
34
|
+
* @param publicKeyPath Path to the public key file.
|
|
35
|
+
* @returns The public key content as a string.
|
|
36
|
+
*/
|
|
37
|
+
export const readPublicKey = async (publicKeyPath) => {
|
|
38
|
+
const content = await fs.promises.readFile(publicKeyPath, 'utf-8');
|
|
39
|
+
return content.trim();
|
|
40
|
+
};
|
|
@@ -56,3 +56,40 @@ export const getBuildScript = async (packageJsonPath) => {
|
|
|
56
56
|
}
|
|
57
57
|
return undefined;
|
|
58
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Check if a package is installed.
|
|
61
|
+
* Checks both dependencies and devDependencies.
|
|
62
|
+
*
|
|
63
|
+
* @param packageJsonPath The path to the package.json file.
|
|
64
|
+
* @param packageName The name of the package to check.
|
|
65
|
+
* @returns True if the package is installed, false otherwise.
|
|
66
|
+
*/
|
|
67
|
+
export const isPackageInstalled = async (packageJsonPath, packageName) => {
|
|
68
|
+
const packageJson = await readPackageJson(packageJsonPath);
|
|
69
|
+
if (!packageJson) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return !!(packageJson.dependencies?.[packageName] || packageJson.devDependencies?.[packageName]);
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Get the Capacitor CLI version from package.json.
|
|
76
|
+
*
|
|
77
|
+
* @param packageJsonPath The path to the package.json file.
|
|
78
|
+
* @returns The Capacitor major version number, or undefined if not found.
|
|
79
|
+
*/
|
|
80
|
+
export const getCapacitorMajorVersion = async (packageJsonPath) => {
|
|
81
|
+
const packageJson = await readPackageJson(packageJsonPath);
|
|
82
|
+
if (!packageJson) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const capacitorVersion = packageJson.dependencies?.['@capacitor/core'] || packageJson.devDependencies?.['@capacitor/core'];
|
|
86
|
+
if (!capacitorVersion) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
// Extract major version from version string (e.g., "^5.0.0" -> 5, "~6.1.0" -> 6)
|
|
90
|
+
const match = capacitorVersion.match(/(\d+)/);
|
|
91
|
+
if (match && match[1]) {
|
|
92
|
+
return parseInt(match[1], 10);
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import consola from 'consola';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
/**
|
|
6
|
+
* Install a package using the specified package manager.
|
|
7
|
+
*
|
|
8
|
+
* @param packageName The name of the package to install.
|
|
9
|
+
* @param packageManager The package manager to use (npm, yarn, or pnpm).
|
|
10
|
+
*/
|
|
11
|
+
export const installPackage = async (packageName, packageManager) => {
|
|
12
|
+
const commands = {
|
|
13
|
+
npm: `npm install ${packageName}`,
|
|
14
|
+
yarn: `yarn add ${packageName}`,
|
|
15
|
+
pnpm: `pnpm add ${packageName}`,
|
|
16
|
+
};
|
|
17
|
+
const command = commands[packageManager];
|
|
18
|
+
consola.start(`Installing ${packageName} using ${packageManager}...`);
|
|
19
|
+
try {
|
|
20
|
+
const { stdout, stderr } = await execAsync(command);
|
|
21
|
+
if (stdout) {
|
|
22
|
+
console.log(stdout);
|
|
23
|
+
}
|
|
24
|
+
if (stderr) {
|
|
25
|
+
console.error(stderr);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
consola.error(`Failed to install ${packageName}.`);
|
|
30
|
+
if (error.stdout) {
|
|
31
|
+
console.log(error.stdout);
|
|
32
|
+
}
|
|
33
|
+
if (error.stderr) {
|
|
34
|
+
console.error(error.stderr);
|
|
35
|
+
}
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capawesome/cli",
|
|
3
|
-
"version": "3.7.0-dev.
|
|
3
|
+
"version": "3.7.0-dev.445ea3a.1764843317",
|
|
4
4
|
"description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"fmt": "npm run prettier -- --write",
|
|
14
14
|
"prettier": "prettier \"**/*.{css,html,ts,js}\"",
|
|
15
15
|
"sentry:releases:new": "sentry-cli releases new capawesome-team-cli@$npm_package_version --org genz-it-solutions-gmbh --project capawesome-team-cli",
|
|
16
|
-
"sentry:releases:set-commits": "sentry-cli releases set-commits capawesome-team-cli@$npm_package_version --auto --org genz-it-solutions-gmbh --project capawesome-team-cli",
|
|
16
|
+
"sentry:releases:set-commits": "sentry-cli releases set-commits capawesome-team-cli@$npm_package_version --auto --org genz-it-solutions-gmbh --project capawesome-team-cli --ignore-missing",
|
|
17
17
|
"sentry:releases:finalize": "sentry-cli releases finalize capawesome-team-cli@$npm_package_version --org genz-it-solutions-gmbh --project capawesome-team-cli",
|
|
18
18
|
"release": "commit-and-tag-version",
|
|
19
19
|
"prepublishOnly": "npm run build && npm run sentry:releases:new && npm run sentry:releases:set-commits",
|