@capawesome/cli 3.7.0-dev.f4e106a.1764835462 → 3.8.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
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [3.8.0](https://github.com/capawesome-team/cli/compare/v3.7.0...v3.8.0) (2025-12-04)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **apps:bundles:create:** auto-detect app ID and web assets directory ([#102](https://github.com/capawesome-team/cli/issues/102)) ([0e60df6](https://github.com/capawesome-team/cli/commit/0e60df66fcbe1effcb406b24e5a751a21badfa9d))
|
|
11
|
+
* **apps:bundles:create:** print warning if private key is missing ([ae29ad9](https://github.com/capawesome-team/cli/commit/ae29ad96ba85db353c81b85be2bee4c6ac928dc0))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* update error message to guide users to login first ([e5ea710](https://github.com/capawesome-team/cli/commit/e5ea710208df82d918e6f191d256d377569350f8))
|
|
17
|
+
|
|
5
18
|
## [3.7.0](https://github.com/capawesome-team/cli/compare/v3.6.0...v3.7.0) (2025-12-02)
|
|
6
19
|
|
|
7
20
|
|
|
@@ -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';
|
|
@@ -22,6 +22,7 @@ import pathModule from 'path';
|
|
|
22
22
|
import { hasTTY } from 'std-env';
|
|
23
23
|
import { promisify } from 'util';
|
|
24
24
|
import { z } from 'zod';
|
|
25
|
+
// Promisified exec for running build scripts
|
|
25
26
|
const execAsync = promisify(exec);
|
|
26
27
|
export default defineCommand({
|
|
27
28
|
description: 'Create a new app bundle.',
|
|
@@ -116,23 +117,25 @@ export default defineCommand({
|
|
|
116
117
|
expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
|
|
117
118
|
expiresAt = expiresAtDate.toISOString();
|
|
118
119
|
}
|
|
120
|
+
// Try to auto-detect webDir from Capacitor configuration
|
|
121
|
+
const capacitorConfigPath = await findCapacitorConfigPath();
|
|
122
|
+
if (!capacitorConfigPath) {
|
|
123
|
+
consola.warn('No Capacitor configuration found to auto-detect web asset directory or app ID.');
|
|
124
|
+
}
|
|
119
125
|
// Check that either a path or a url is provided
|
|
120
126
|
if (!path && !url) {
|
|
121
|
-
// Try to auto-detect webDir from Capacitor
|
|
122
|
-
const capacitorConfigPath = await findCapacitorConfigPath();
|
|
127
|
+
// Try to auto-detect webDir from Capacitor configuration
|
|
123
128
|
if (capacitorConfigPath) {
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
const webDirPath = await getWebDirFromConfig(capacitorConfigPath);
|
|
130
|
+
if (webDirPath) {
|
|
131
|
+
const relativeWebDirPath = pathModule.relative(process.cwd(), webDirPath);
|
|
132
|
+
consola.success(`Auto-detected web asset directory "${relativeWebDirPath}" from Capacitor configuration.`);
|
|
133
|
+
path = webDirPath;
|
|
128
134
|
}
|
|
129
135
|
else {
|
|
130
|
-
consola.warn('No web asset directory found in Capacitor
|
|
136
|
+
consola.warn('No web asset directory found in Capacitor configuration (`webDir`).');
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
|
-
else {
|
|
134
|
-
consola.warn('No Capacitor config found to auto-detect web asset directory.');
|
|
135
|
-
}
|
|
136
139
|
// If still no path, prompt the user
|
|
137
140
|
if (!path) {
|
|
138
141
|
if (!hasTTY) {
|
|
@@ -159,16 +162,16 @@ export default defineCommand({
|
|
|
159
162
|
else {
|
|
160
163
|
const buildScript = await getBuildScript(packageJsonPath);
|
|
161
164
|
if (!buildScript) {
|
|
162
|
-
consola.warn('No build script found in package.json.');
|
|
165
|
+
consola.warn('No build script (`capawesome:build` or `build`) found in package.json.');
|
|
163
166
|
}
|
|
164
167
|
else if (hasTTY) {
|
|
165
|
-
const shouldBuild = await prompt('Do you want to
|
|
166
|
-
type: '
|
|
167
|
-
|
|
168
|
+
const shouldBuild = await prompt('Do you want to run the build script before creating the bundle to ensure the latest assets are included?', {
|
|
169
|
+
type: 'confirm',
|
|
170
|
+
initial: true,
|
|
168
171
|
});
|
|
169
|
-
if (shouldBuild
|
|
172
|
+
if (shouldBuild) {
|
|
170
173
|
try {
|
|
171
|
-
consola.start(`Running
|
|
174
|
+
consola.start(`Running \`${buildScript.name}\` script...`);
|
|
172
175
|
const { stdout, stderr } = await execAsync(`npm run ${buildScript.name}`);
|
|
173
176
|
if (stdout) {
|
|
174
177
|
console.log(stdout);
|
|
@@ -231,22 +234,19 @@ export default defineCommand({
|
|
|
231
234
|
consola.error('It is not yet possible to provide a URL when creating a bundle with an artifact type of `manifest`.');
|
|
232
235
|
process.exit(1);
|
|
233
236
|
}
|
|
237
|
+
// Track if we found a Capacitor configuration but no app ID (for showing setup hint later)
|
|
234
238
|
if (!appId) {
|
|
235
|
-
// Try to auto-detect appId from Capacitor
|
|
236
|
-
const capacitorConfigPath = await findCapacitorConfigPath();
|
|
239
|
+
// Try to auto-detect appId from Capacitor configuration
|
|
237
240
|
if (capacitorConfigPath) {
|
|
238
|
-
const configAppId = await
|
|
241
|
+
const configAppId = await getLiveUpdatePluginAppIdFromConfig(capacitorConfigPath);
|
|
239
242
|
if (configAppId) {
|
|
240
|
-
|
|
243
|
+
consola.success(`Auto-detected Capawesome Cloud app ID "${configAppId}" from Capacitor configuration.`);
|
|
241
244
|
appId = configAppId;
|
|
242
245
|
}
|
|
243
246
|
else {
|
|
244
|
-
consola.warn('No Capawesome Cloud app ID found in Capacitor
|
|
247
|
+
consola.warn('No Capawesome Cloud app ID found in Capacitor configuration (`plugins.LiveUpdate.appId`).');
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
|
-
else {
|
|
248
|
-
consola.warn('No Capacitor config found to auto-detect Capawesome Cloud app ID.');
|
|
249
|
-
}
|
|
250
250
|
// If still no appId, prompt the user
|
|
251
251
|
if (!appId) {
|
|
252
252
|
if (!hasTTY) {
|
|
@@ -286,11 +286,11 @@ export default defineCommand({
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
if (!channel && hasTTY) {
|
|
289
|
-
const
|
|
290
|
-
type: '
|
|
291
|
-
|
|
289
|
+
const shouldDeployToChannel = await prompt('Do you want to deploy to a specific channel?', {
|
|
290
|
+
type: 'confirm',
|
|
291
|
+
initial: false,
|
|
292
292
|
});
|
|
293
|
-
if (
|
|
293
|
+
if (shouldDeployToChannel) {
|
|
294
294
|
channel = await prompt('Enter the channel name:', {
|
|
295
295
|
type: 'text',
|
|
296
296
|
});
|
|
@@ -300,6 +300,13 @@ export default defineCommand({
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
+
// Check if public key is configured but no private key was provided
|
|
304
|
+
if (!privateKey && capacitorConfigPath) {
|
|
305
|
+
const publicKey = await getLiveUpdatePluginPublicKeyFromConfig(capacitorConfigPath);
|
|
306
|
+
if (publicKey) {
|
|
307
|
+
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.');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
303
310
|
// Create the private key buffer
|
|
304
311
|
let privateKeyBuffer;
|
|
305
312
|
if (privateKey) {
|
|
@@ -333,10 +340,10 @@ export default defineCommand({
|
|
|
333
340
|
// Final confirmation before creating bundle
|
|
334
341
|
if (path && hasTTY) {
|
|
335
342
|
const relativePath = pathModule.relative(process.cwd(), path);
|
|
336
|
-
const confirmed = await prompt(`Are you sure you want to create a bundle from path ${relativePath} for app "${appName}"?`, {
|
|
343
|
+
const confirmed = await prompt(`Are you sure you want to create a bundle from path "${relativePath}" for app "${appName}" (${appId})?`, {
|
|
337
344
|
type: 'confirm',
|
|
338
345
|
});
|
|
339
|
-
if (confirmed) {
|
|
346
|
+
if (!confirmed) {
|
|
340
347
|
consola.info('Bundle creation cancelled.');
|
|
341
348
|
process.exit(0);
|
|
342
349
|
}
|
|
@@ -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 () => {
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capawesome/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
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",
|