@capawesome/cli 3.7.0 → 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
 
@@ -28,7 +28,7 @@ export default defineCommand({
28
28
  let { appId, buildId } = options;
29
29
  // Check if the user is logged in
30
30
  if (!authorizationService.hasAuthorizationToken()) {
31
- consola.error('You must be logged in to run this command.');
31
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
32
32
  process.exit(1);
33
33
  }
34
34
  // Prompt for app ID if not provided
@@ -60,7 +60,7 @@ export default defineCommand({
60
60
  let { appId, platform, type, gitRef, environment, certificate, json } = options;
61
61
  // Check if the user is logged in
62
62
  if (!authorizationService.hasAuthorizationToken()) {
63
- consola.error('You must be logged in to run this command.');
63
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
64
64
  process.exit(1);
65
65
  }
66
66
  // Validate that detached flag cannot be used with artifact flags
@@ -41,7 +41,7 @@ export default defineCommand({
41
41
  let { appId, buildId } = options;
42
42
  // Check if the user is logged in
43
43
  if (!authorizationService.hasAuthorizationToken()) {
44
- consola.error('You must be logged in to run this command.');
44
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
45
45
  process.exit(1);
46
46
  }
47
47
  // Prompt for app ID if not provided
@@ -29,7 +29,7 @@ export default defineCommand({
29
29
  let { appId, buildId } = options;
30
30
  // Check if the user is logged in
31
31
  if (!authorizationService.hasAuthorizationToken()) {
32
- consola.error('You must be logged in to run this command.');
32
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
33
33
  process.exit(1);
34
34
  }
35
35
  // Prompt for app ID if not provided
@@ -5,18 +5,25 @@ 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, getLiveUpdatePluginAppIdFromConfig, getLiveUpdatePluginPublicKeyFromConfig, getWebDirFromConfig, } from '../../../utils/capacitor-config.js';
8
9
  import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
9
10
  import { createHash } from '../../../utils/hash.js';
10
11
  import { generateManifestJson } from '../../../utils/manifest.js';
12
+ import { findPackageJsonPath, getBuildScript } from '../../../utils/package-json.js';
11
13
  import { formatPrivateKey } from '../../../utils/private-key.js';
12
14
  import { prompt } from '../../../utils/prompt.js';
13
15
  import { createSignature } from '../../../utils/signature.js';
14
16
  import zip from '../../../utils/zip.js';
15
17
  import { defineCommand, defineOptions } from '@robingenz/zli';
18
+ import { exec } from 'child_process';
16
19
  import consola from 'consola';
17
20
  import { createReadStream } from 'fs';
21
+ import pathModule from 'path';
18
22
  import { hasTTY } from 'std-env';
23
+ import { promisify } from 'util';
19
24
  import { z } from 'zod';
25
+ // Promisified exec for running build scripts
26
+ const execAsync = promisify(exec);
20
27
  export default defineCommand({
21
28
  description: 'Create a new app bundle.',
22
29
  options: defineOptions(z.object({
@@ -100,7 +107,7 @@ export default defineCommand({
100
107
  let { androidEq, androidMax, androidMin, appId, artifactType, channel, commitMessage, commitRef, commitSha, customProperty, expiresInDays, iosEq, iosMax, iosMin, path, privateKey, rollout, url, } = options;
101
108
  // Check if the user is logged in
102
109
  if (!authorizationService.hasAuthorizationToken()) {
103
- consola.error('You must be logged in to run this command.');
110
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
104
111
  process.exit(1);
105
112
  }
106
113
  // Calculate the expiration date
@@ -110,22 +117,85 @@ export default defineCommand({
110
117
  expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
111
118
  expiresAt = expiresAtDate.toISOString();
112
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
+ }
113
125
  // Check that either a path or a url is provided
114
126
  if (!path && !url) {
115
- if (!hasTTY) {
116
- consola.error('You must provide either a path or a url when running in non-interactive environment.');
117
- process.exit(1);
127
+ // Try to auto-detect webDir from Capacitor configuration
128
+ if (capacitorConfigPath) {
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;
134
+ }
135
+ else {
136
+ consola.warn('No web asset directory found in Capacitor configuration (`webDir`).');
137
+ }
118
138
  }
119
- else {
120
- path = await prompt('Enter the path to the app bundle:', {
121
- type: 'text',
122
- });
123
- if (!path) {
124
- consola.error('You must provide a path to the app bundle.');
139
+ // If still no path, prompt the user
140
+ if (!path) {
141
+ if (!hasTTY) {
142
+ consola.error('You must provide either a path or a url when running in non-interactive environment.');
125
143
  process.exit(1);
126
144
  }
145
+ else {
146
+ path = await prompt('Enter the path to the app bundle:', {
147
+ type: 'text',
148
+ });
149
+ if (!path) {
150
+ consola.error('You must provide a path to the app bundle.');
151
+ process.exit(1);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ // Check for build scripts if a path is provided or detected
157
+ if (path && !url) {
158
+ const packageJsonPath = await findPackageJsonPath();
159
+ if (!packageJsonPath) {
160
+ consola.warn('No package.json file found.');
161
+ }
162
+ else {
163
+ const buildScript = await getBuildScript(packageJsonPath);
164
+ if (!buildScript) {
165
+ consola.warn('No build script (`capawesome:build` or `build`) found in package.json.');
166
+ }
167
+ else if (hasTTY) {
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,
171
+ });
172
+ if (shouldBuild) {
173
+ try {
174
+ consola.start(`Running \`${buildScript.name}\` script...`);
175
+ const { stdout, stderr } = await execAsync(`npm run ${buildScript.name}`);
176
+ if (stdout) {
177
+ console.log(stdout);
178
+ }
179
+ if (stderr) {
180
+ console.error(stderr);
181
+ }
182
+ consola.success('Build completed successfully.');
183
+ }
184
+ catch (error) {
185
+ consola.error('Build failed.');
186
+ if (error.stdout) {
187
+ console.log(error.stdout);
188
+ }
189
+ if (error.stderr) {
190
+ console.error(error.stderr);
191
+ }
192
+ process.exit(1);
193
+ }
194
+ }
195
+ }
127
196
  }
128
197
  }
198
+ // Validate the provided path
129
199
  if (path) {
130
200
  // Check if the path exists when a path is provided
131
201
  const pathExists = await fileExistsAtPath(path);
@@ -164,48 +234,63 @@ export default defineCommand({
164
234
  consola.error('It is not yet possible to provide a URL when creating a bundle with an artifact type of `manifest`.');
165
235
  process.exit(1);
166
236
  }
237
+ // Track if we found a Capacitor configuration but no app ID (for showing setup hint later)
167
238
  if (!appId) {
168
- if (!hasTTY) {
169
- consola.error('You must provide an app ID when running in non-interactive environment.');
170
- process.exit(1);
171
- }
172
- const organizations = await organizationsService.findAll();
173
- if (organizations.length === 0) {
174
- consola.error('You must create an organization before creating a bundle.');
175
- process.exit(1);
176
- }
177
- // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
178
- const organizationId = await prompt('Select the organization of the app for which you want to create a bundle.', {
179
- type: 'select',
180
- options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
181
- });
182
- if (!organizationId) {
183
- consola.error('You must select the organization of an app for which you want to create a bundle.');
184
- process.exit(1);
185
- }
186
- const apps = await appsService.findAll({
187
- organizationId,
188
- });
189
- if (apps.length === 0) {
190
- consola.error('You must create an app before creating a bundle.');
191
- process.exit(1);
239
+ // Try to auto-detect appId from Capacitor configuration
240
+ if (capacitorConfigPath) {
241
+ const configAppId = await getLiveUpdatePluginAppIdFromConfig(capacitorConfigPath);
242
+ if (configAppId) {
243
+ consola.success(`Auto-detected Capawesome Cloud app ID "${configAppId}" from Capacitor configuration.`);
244
+ appId = configAppId;
245
+ }
246
+ else {
247
+ consola.warn('No Capawesome Cloud app ID found in Capacitor configuration (`plugins.LiveUpdate.appId`).');
248
+ }
192
249
  }
193
- // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
194
- appId = await prompt('Which app do you want to deploy to:', {
195
- type: 'select',
196
- options: apps.map((app) => ({ label: app.name, value: app.id })),
197
- });
250
+ // If still no appId, prompt the user
198
251
  if (!appId) {
199
- consola.error('You must select an app to deploy to.');
200
- process.exit(1);
252
+ if (!hasTTY) {
253
+ consola.error('You must provide an app ID when running in non-interactive environment.');
254
+ process.exit(1);
255
+ }
256
+ const organizations = await organizationsService.findAll();
257
+ if (organizations.length === 0) {
258
+ consola.error('You must create an organization before creating a bundle.');
259
+ process.exit(1);
260
+ }
261
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
262
+ const organizationId = await prompt('Select the organization of the app for which you want to create a bundle.', {
263
+ type: 'select',
264
+ options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
265
+ });
266
+ if (!organizationId) {
267
+ consola.error('You must select the organization of an app for which you want to create a bundle.');
268
+ process.exit(1);
269
+ }
270
+ const apps = await appsService.findAll({
271
+ organizationId,
272
+ });
273
+ if (apps.length === 0) {
274
+ consola.error('You must create an app before creating a bundle.');
275
+ process.exit(1);
276
+ }
277
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
278
+ appId = await prompt('Which app do you want to deploy to:', {
279
+ type: 'select',
280
+ options: apps.map((app) => ({ label: app.name, value: app.id })),
281
+ });
282
+ if (!appId) {
283
+ consola.error('You must select an app to deploy to.');
284
+ process.exit(1);
285
+ }
201
286
  }
202
287
  }
203
288
  if (!channel && hasTTY) {
204
- const promptChannel = await prompt('Do you want to deploy to a specific channel?', {
205
- type: 'select',
206
- options: ['Yes', 'No'],
289
+ const shouldDeployToChannel = await prompt('Do you want to deploy to a specific channel?', {
290
+ type: 'confirm',
291
+ initial: false,
207
292
  });
208
- if (promptChannel === 'Yes') {
293
+ if (shouldDeployToChannel) {
209
294
  channel = await prompt('Enter the channel name:', {
210
295
  type: 'text',
211
296
  });
@@ -215,6 +300,13 @@ export default defineCommand({
215
300
  }
216
301
  }
217
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
+ }
218
310
  // Create the private key buffer
219
311
  let privateKeyBuffer;
220
312
  if (privateKey) {
@@ -242,9 +334,23 @@ export default defineCommand({
242
334
  process.exit(1);
243
335
  }
244
336
  }
337
+ // Get app details for confirmation
338
+ const app = await appsService.findOne({ appId });
339
+ const appName = app.name;
340
+ // Final confirmation before creating bundle
341
+ if (path && hasTTY) {
342
+ const relativePath = pathModule.relative(process.cwd(), path);
343
+ const confirmed = await prompt(`Are you sure you want to create a bundle from path "${relativePath}" for app "${appName}" (${appId})?`, {
344
+ type: 'confirm',
345
+ });
346
+ if (!confirmed) {
347
+ consola.info('Bundle creation cancelled.');
348
+ process.exit(0);
349
+ }
350
+ }
351
+ // Create the app bundle
245
352
  let appBundleId;
246
353
  try {
247
- // Create the app bundle
248
354
  consola.start('Creating bundle...');
249
355
  let checksum;
250
356
  let signature;
@@ -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
  });
@@ -41,7 +49,7 @@ describe('apps-bundles-create', () => {
41
49
  const options = { appId, path: './dist', artifactType: 'zip', rollout: 1 };
42
50
  mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
43
51
  await expect(createBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
44
- expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command.');
52
+ expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command. Please run the `login` command first.');
45
53
  });
46
54
  it('should create bundle with self-hosted URL', async () => {
47
55
  const appId = 'app-123';
@@ -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 () => {
@@ -16,7 +16,7 @@ export default defineCommand({
16
16
  action: async (options, args) => {
17
17
  let { appId, bundleId } = options;
18
18
  if (!authorizationService.hasAuthorizationToken()) {
19
- consola.error('You must be logged in to run this command.');
19
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
20
20
  process.exit(1);
21
21
  }
22
22
  // Prompt for missing arguments
@@ -48,7 +48,7 @@ export default defineCommand({
48
48
  action: async (options, args) => {
49
49
  let { androidMax, androidMin, androidEq, appId, bundleId, rollout, iosMax, iosMin, iosEq } = options;
50
50
  if (!authorizationService.hasAuthorizationToken()) {
51
- consola.error('You must be logged in to run this command.');
51
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
52
52
  process.exit(1);
53
53
  }
54
54
  // Prompt for missing arguments
@@ -38,7 +38,7 @@ describe('apps-bundles-update', () => {
38
38
  const options = { appId, bundleId };
39
39
  mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
40
40
  await expect(updateBundleCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
41
- expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command.');
41
+ expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command. Please run the `login` command first.');
42
42
  });
43
43
  it('should update bundle with provided options', async () => {
44
44
  const appId = 'app-123';
@@ -31,7 +31,7 @@ export default defineCommand({
31
31
  action: async (options, args) => {
32
32
  let { appId, bundleLimit, expiresInDays, ignoreErrors, name } = options;
33
33
  if (!authorizationService.hasAuthorizationToken()) {
34
- consola.error('You must be logged in to run this command.');
34
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
35
35
  process.exit(1);
36
36
  }
37
37
  // Calculate the expiration date
@@ -23,7 +23,7 @@ export default defineCommand({
23
23
  action: async (options, args) => {
24
24
  let { appId, channelId, name } = options;
25
25
  if (!authorizationService.hasAuthorizationToken()) {
26
- consola.error('You must be logged in to run this command.');
26
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
27
27
  process.exit(1);
28
28
  }
29
29
  if (!appId) {
@@ -14,7 +14,7 @@ export default defineCommand({
14
14
  action: async (options, args) => {
15
15
  let { appId, channelId, json, name } = options;
16
16
  if (!authorizationService.hasAuthorizationToken()) {
17
- consola.error('You must be logged in to run this command.');
17
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
18
18
  process.exit(1);
19
19
  }
20
20
  if (!appId) {
@@ -14,7 +14,7 @@ export default defineCommand({
14
14
  action: async (options, args) => {
15
15
  let { appId, json, limit, offset } = options;
16
16
  if (!authorizationService.hasAuthorizationToken()) {
17
- consola.error('You must be logged in to run this command.');
17
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
18
18
  process.exit(1);
19
19
  }
20
20
  if (!appId) {
@@ -33,7 +33,7 @@ describe('apps-channels-list', () => {
33
33
  const options = { appId };
34
34
  mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
35
35
  await expect(listChannelsCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
36
- expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command.');
36
+ expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command. Please run the `login` command first.');
37
37
  });
38
38
  it('should require appId', async () => {
39
39
  const options = { appId: undefined };
@@ -21,7 +21,7 @@ export default defineCommand({
21
21
  action: async (options, args) => {
22
22
  let { appId, channelId, bundleLimit, name } = options;
23
23
  if (!authorizationService.hasAuthorizationToken()) {
24
- consola.error('You must be logged in to run this command.');
24
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
25
25
  process.exit(1);
26
26
  }
27
27
  // Prompt app ID if not provided
@@ -38,7 +38,7 @@ describe('apps-channels-update', () => {
38
38
  const options = { appId, channelId };
39
39
  mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
40
40
  await expect(updateChannelCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
41
- expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command.');
41
+ expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command. Please run the `login` command first.');
42
42
  });
43
43
  it('should update channel with provided options', async () => {
44
44
  const appId = 'app-123';
@@ -15,7 +15,7 @@ export default defineCommand({
15
15
  action: async (options, args) => {
16
16
  let { name, organizationId } = options;
17
17
  if (!authorizationService.hasAuthorizationToken()) {
18
- consola.error('You must be logged in to run this command.');
18
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
19
19
  process.exit(1);
20
20
  }
21
21
  if (!organizationId) {
@@ -14,7 +14,7 @@ export default defineCommand({
14
14
  action: async (options, args) => {
15
15
  let { appId } = options;
16
16
  if (!authorizationService.hasAuthorizationToken()) {
17
- consola.error('You must be logged in to run this command.');
17
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
18
18
  process.exit(1);
19
19
  }
20
20
  if (!appId) {
@@ -28,7 +28,7 @@ export default defineCommand({
28
28
  let { appId, deploymentId } = options;
29
29
  // Check if the user is logged in
30
30
  if (!authorizationService.hasAuthorizationToken()) {
31
- consola.error('You must be logged in to run this command.');
31
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
32
32
  process.exit(1);
33
33
  }
34
34
  // Prompt for app ID if not provided
@@ -37,7 +37,7 @@ export default defineCommand({
37
37
  let { appId, buildId, destination } = options;
38
38
  // Check if the user is logged in
39
39
  if (!authorizationService.hasAuthorizationToken()) {
40
- consola.error('You must be logged in to run this command.');
40
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
41
41
  process.exit(1);
42
42
  }
43
43
  // Prompt for app ID if not provided
@@ -1,3 +1,4 @@
1
+ import appDeploymentsService from '../../../services/app-deployments.js';
1
2
  import appsService from '../../../services/apps.js';
2
3
  import authorizationService from '../../../services/authorization-service.js';
3
4
  import organizationsService from '../../../services/organizations.js';
@@ -8,7 +9,6 @@ import { defineCommand, defineOptions } from '@robingenz/zli';
8
9
  import consola from 'consola';
9
10
  import { hasTTY } from 'std-env';
10
11
  import { z } from 'zod';
11
- import appDeploymentsService from '../../../services/app-deployments.js';
12
12
  export default defineCommand({
13
13
  description: 'View the deployment logs of an app.',
14
14
  options: defineOptions(z.object({
@@ -29,7 +29,7 @@ export default defineCommand({
29
29
  let { appId, deploymentId } = options;
30
30
  // Check if the user is logged in
31
31
  if (!authorizationService.hasAuthorizationToken()) {
32
- consola.error('You must be logged in to run this command.');
32
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
33
33
  process.exit(1);
34
34
  }
35
35
  // Prompt for app ID if not provided
@@ -16,7 +16,7 @@ export default defineCommand({
16
16
  action: async (options, args) => {
17
17
  let { appId, deviceId } = options;
18
18
  if (!authorizationService.hasAuthorizationToken()) {
19
- consola.error('You must be logged in to run this command.');
19
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
20
20
  process.exit(1);
21
21
  }
22
22
  // Prompt for app ID if not provided
@@ -13,7 +13,7 @@ export default defineCommand({
13
13
  action: async (options, args) => {
14
14
  let { name } = options;
15
15
  if (!authorizationService.hasAuthorizationToken()) {
16
- consola.error('You must be logged in to run this command.');
16
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
17
17
  process.exit(1);
18
18
  }
19
19
  if (!name) {
@@ -67,7 +67,7 @@ describe('organizations-create', () => {
67
67
  const options = { name: organizationName };
68
68
  mockAuthorizationService.hasAuthorizationToken.mockReturnValue(false);
69
69
  await expect(createOrganizationCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
70
- expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command.');
70
+ expect(mockConsola.error).toHaveBeenCalledWith('You must be logged in to run this command. Please run the `login` command first.');
71
71
  });
72
72
  it('should handle API error during creation', async () => {
73
73
  const organizationName = 'Test Organization';
@@ -31,6 +31,14 @@ class AppsServiceImpl {
31
31
  });
32
32
  return response.data;
33
33
  }
34
+ async findOne(dto) {
35
+ const response = await this.httpClient.get(`/v1/apps/${dto.appId}`, {
36
+ headers: {
37
+ Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
38
+ },
39
+ });
40
+ return response.data;
41
+ }
34
42
  }
35
43
  const appsService = new AppsServiceImpl(httpClient);
36
44
  export default appsService;
@@ -0,0 +1,96 @@
1
+ import fs from 'fs';
2
+ import pathModule from 'path';
3
+ import { fileExistsAtPath } from './file.js';
4
+ /**
5
+ * Find the Capacitor config file in the current working directory.
6
+ * Looks for capacitor.config.json or capacitor.config.ts.
7
+ *
8
+ * @returns The path to the config file, or undefined if not found.
9
+ */
10
+ export const findCapacitorConfigPath = async () => {
11
+ const cwd = process.cwd();
12
+ const jsonPath = pathModule.join(cwd, 'capacitor.config.json');
13
+ const tsPath = pathModule.join(cwd, 'capacitor.config.ts');
14
+ if (await fileExistsAtPath(jsonPath)) {
15
+ return jsonPath;
16
+ }
17
+ if (await fileExistsAtPath(tsPath)) {
18
+ return tsPath;
19
+ }
20
+ return undefined;
21
+ };
22
+ /**
23
+ * Read and parse the Capacitor config file.
24
+ *
25
+ * @param configPath The path to the config file.
26
+ * @returns The parsed config object, or undefined if parsing fails.
27
+ */
28
+ export const readCapacitorConfig = async (configPath) => {
29
+ try {
30
+ if (configPath.endsWith('.json')) {
31
+ const content = await fs.promises.readFile(configPath, 'utf-8');
32
+ return JSON.parse(content);
33
+ }
34
+ else if (configPath.endsWith('.ts')) {
35
+ // For TypeScript config, parse as text and extract values
36
+ const content = await fs.promises.readFile(configPath, 'utf-8');
37
+ // Extract webDir using regex
38
+ const webDirMatch = content.match(/webDir:\s*['"]([^'"]+)['"]/);
39
+ const appIdMatch = content.match(/LiveUpdate:\s*{[^}]*appId:\s*['"]([^'"]+)['"]/s);
40
+ const publicKeyMatch = content.match(/LiveUpdate:\s*{[^}]*publicKey:\s*['"]([^'"]+)['"]/s);
41
+ const config = {};
42
+ if (webDirMatch) {
43
+ config.webDir = webDirMatch[1];
44
+ }
45
+ if (appIdMatch || publicKeyMatch) {
46
+ config.plugins = {
47
+ LiveUpdate: {
48
+ ...(appIdMatch && { appId: appIdMatch[1] }),
49
+ ...(publicKeyMatch && { publicKey: publicKeyMatch[1] }),
50
+ },
51
+ };
52
+ }
53
+ return Object.keys(config).length > 0 ? config : undefined;
54
+ }
55
+ }
56
+ catch (error) {
57
+ // Return undefined if parsing fails
58
+ }
59
+ return undefined;
60
+ };
61
+ /**
62
+ * Get the webDir from the Capacitor config.
63
+ * Returns the absolute path to the webDir.
64
+ *
65
+ * @param configPath The path to the config file.
66
+ * @returns The absolute path to the webDir, or undefined if not found.
67
+ */
68
+ export const getWebDirFromConfig = async (configPath) => {
69
+ const config = await readCapacitorConfig(configPath);
70
+ if (config?.webDir) {
71
+ // Resolve the webDir relative to the config file location
72
+ const configDir = pathModule.dirname(configPath);
73
+ return pathModule.resolve(configDir, config.webDir);
74
+ }
75
+ return undefined;
76
+ };
77
+ /**
78
+ * Get the LiveUpdate appId from the Capacitor config.
79
+ *
80
+ * @param configPath The path to the config file.
81
+ * @returns The appId, or undefined if not found.
82
+ */
83
+ export const getLiveUpdatePluginAppIdFromConfig = async (configPath) => {
84
+ const config = await readCapacitorConfig(configPath);
85
+ return config?.plugins?.LiveUpdate?.appId;
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,58 @@
1
+ import { fileExistsAtPath } from './file.js';
2
+ import fs from 'fs';
3
+ import pathModule from 'path';
4
+ /**
5
+ * Find the package.json file in the current working directory.
6
+ *
7
+ * @returns The path to the package.json file, or undefined if not found.
8
+ */
9
+ export const findPackageJsonPath = async () => {
10
+ const cwd = process.cwd();
11
+ const packageJsonPath = pathModule.join(cwd, 'package.json');
12
+ if (await fileExistsAtPath(packageJsonPath)) {
13
+ return packageJsonPath;
14
+ }
15
+ return undefined;
16
+ };
17
+ /**
18
+ * Read and parse the package.json file.
19
+ *
20
+ * @param packageJsonPath The path to the package.json file.
21
+ * @returns The parsed package.json object, or undefined if parsing fails.
22
+ */
23
+ export const readPackageJson = async (packageJsonPath) => {
24
+ try {
25
+ const content = await fs.promises.readFile(packageJsonPath, 'utf-8');
26
+ return JSON.parse(content);
27
+ }
28
+ catch (error) {
29
+ return undefined;
30
+ }
31
+ };
32
+ /**
33
+ * Get the build script from package.json.
34
+ * Prefers 'capawesome:build' over 'build' if both exist.
35
+ *
36
+ * @param packageJsonPath The path to the package.json file.
37
+ * @returns An object with the script name and command, or undefined if not found.
38
+ */
39
+ export const getBuildScript = async (packageJsonPath) => {
40
+ const packageJson = await readPackageJson(packageJsonPath);
41
+ if (!packageJson?.scripts) {
42
+ return undefined;
43
+ }
44
+ // Prefer 'capawesome:build' over 'build'
45
+ if (packageJson.scripts['capawesome:build']) {
46
+ return {
47
+ name: 'capawesome:build',
48
+ command: packageJson.scripts['capawesome:build'],
49
+ };
50
+ }
51
+ if (packageJson.scripts['build']) {
52
+ return {
53
+ name: 'build',
54
+ command: packageJson.scripts['build'],
55
+ };
56
+ }
57
+ return undefined;
58
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/cli",
3
- "version": "3.7.0",
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",