@capawesome/cli 3.7.0-dev.3ddf02d.1764834835 → 3.7.0-dev.426865e.1764845864

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.
@@ -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, getLiveUpdateAppIdFromConfig, getWebDirFromConfig, } from '../../../utils/capacitor-config.js';
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 config
121
- const capacitorConfigPath = await findCapacitorConfigPath();
124
+ // Try to auto-detect webDir from Capacitor configuration
122
125
  if (capacitorConfigPath) {
123
- const webDir = await getWebDirFromConfig(capacitorConfigPath);
124
- if (webDir) {
125
- path = webDir;
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 webDir found in Capacitor config.');
133
+ consola.warn('No web asset directory found in Capacitor configuration (`webDir`).');
129
134
  }
130
135
  }
131
136
  else {
132
- consola.warn('No Capacitor config found to auto-detect webDir.');
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 creating the bundle?', {
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 ${buildScript.name} script...`);
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,21 @@ 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)
232
238
  if (!appId) {
233
- // Try to auto-detect appId from Capacitor config
234
- const capacitorConfigPath = await findCapacitorConfigPath();
239
+ // Try to auto-detect appId from Capacitor configuration
235
240
  if (capacitorConfigPath) {
236
- const configAppId = await getLiveUpdateAppIdFromConfig(capacitorConfigPath);
241
+ const configAppId = await getLiveUpdatePluginAppIdFromConfig(capacitorConfigPath);
237
242
  if (configAppId) {
243
+ consola.success(`Auto-detected Capawesome Cloud app ID "${configAppId}" from Capacitor configuration.`);
238
244
  appId = configAppId;
239
245
  }
240
246
  else {
241
- consola.warn('No LiveUpdate appId found in Capacitor config.');
247
+ consola.warn('No Capawesome Cloud app ID found in Capacitor configuration (`plugins.LiveUpdate.appId`).');
242
248
  }
243
249
  }
244
250
  else {
245
- consola.warn('No Capacitor config found to auto-detect appId.');
251
+ consola.warn('No Capacitor configuration found to auto-detect Capawesome Cloud app ID.');
246
252
  }
247
253
  // If still no appId, prompt the user
248
254
  if (!appId) {
@@ -297,6 +303,13 @@ export default defineCommand({
297
303
  }
298
304
  }
299
305
  }
306
+ // Check if public key is configured but no private key was provided
307
+ if (!privateKey && capacitorConfigPath) {
308
+ const publicKey = await getLiveUpdatePluginPublicKeyFromConfig(capacitorConfigPath);
309
+ if (publicKey) {
310
+ 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.');
311
+ }
312
+ }
300
313
  // Create the private key buffer
301
314
  let privateKeyBuffer;
302
315
  if (privateKey) {
@@ -329,10 +342,11 @@ export default defineCommand({
329
342
  const appName = app.name;
330
343
  // Final confirmation before creating bundle
331
344
  if (path && hasTTY) {
332
- const confirmed = await prompt(`Are you sure you want to create a bundle from path ${path} for app "${appName}"?`, {
345
+ const relativePath = pathModule.relative(process.cwd(), path);
346
+ const confirmed = await prompt(`Are you sure you want to create a bundle from path "${relativePath}" for app "${appName}" (${appId})?`, {
333
347
  type: 'confirm',
334
348
  });
335
- if (confirmed) {
349
+ if (!confirmed) {
336
350
  consola.info('Bundle creation cancelled.');
337
351
  process.exit(0);
338
352
  }
@@ -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 scope = nock(DEFAULT_API_BASE_URL)
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(scope.isDone()).toBe(true);
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 scope = nock(DEFAULT_API_BASE_URL)
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(scope.isDone()).toBe(true);
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 scope = nock(DEFAULT_API_BASE_URL)
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(scope.isDone()).toBe(true);
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 scope = nock(DEFAULT_API_BASE_URL)
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(scope.isDone()).toBe(true);
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 getLiveUpdateAppIdFromConfig = async (configPath) => {
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.7.0-dev.3ddf02d.1764834835",
3
+ "version": "3.7.0-dev.426865e.1764845864",
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",