@capawesome/cli 4.11.0 → 4.12.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 +9 -0
- package/dist/commands/apps/builds/create.js +18 -1
- package/dist/commands/apps/builds/failure-summary.js +73 -0
- package/dist/commands/apps/deployments/create.js +13 -1
- package/dist/commands/apps/deployments/failure-summary.js +63 -0
- package/dist/commands/apps/environments/get.js +62 -0
- package/dist/commands/apps/environments/set.js +26 -14
- package/dist/commands/apps/environments/unset.js +26 -14
- package/dist/commands/apps/liveupdates/generate-signing-key.js +59 -14
- package/dist/index.js +3 -0
- package/dist/services/app-environments.js +11 -0
- package/dist/services/jobs.js +8 -0
- package/dist/utils/job-failure-summary.js +58 -0
- package/dist/utils/job.js +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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
|
+
## [4.12.0](https://github.com/capawesome-team/cli/compare/v4.11.0...v4.12.0) (2026-06-08)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add AI-powered failure summary for builds and deployments ([#167](https://github.com/capawesome-team/cli/issues/167)) ([ff87668](https://github.com/capawesome-team/cli/commit/ff876683036f884cbe453bc6c00fe964d035f57c))
|
|
11
|
+
* **environments:** add `get` command and `--name` option to `set`/`unset` ([#168](https://github.com/capawesome-team/cli/issues/168)) ([64fe273](https://github.com/capawesome-team/cli/commit/64fe273b2db3533f7d7929c6db4806720f1d9e50))
|
|
12
|
+
* **liveupdates:** support Cordova app type in `generate-signing-key` ([#166](https://github.com/capawesome-team/cli/issues/166)) ([ea81c0f](https://github.com/capawesome-team/cli/commit/ea81c0fc86d89f1c81a18b3c59e9f642eee03f07))
|
|
13
|
+
|
|
5
14
|
## [4.11.0](https://github.com/capawesome-team/cli/compare/v4.10.0...v4.11.0) (2026-06-02)
|
|
6
15
|
|
|
7
16
|
|
|
@@ -8,6 +8,7 @@ import { parseKeyValuePairs } from '../../../utils/app-environments.js';
|
|
|
8
8
|
import { withAuth } from '../../../utils/auth.js';
|
|
9
9
|
import { createBufferFromPath } from '../../../utils/buffer.js';
|
|
10
10
|
import { isInteractive } from '../../../utils/environment.js';
|
|
11
|
+
import { offerJobFailureSummary } from '../../../utils/job-failure-summary.js';
|
|
11
12
|
import { isDirectory, isReadable } from '../../../utils/file.js';
|
|
12
13
|
import { waitForJobCompletion } from '../../../utils/job.js';
|
|
13
14
|
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
@@ -44,6 +45,10 @@ export default defineCommand({
|
|
|
44
45
|
.optional()
|
|
45
46
|
.describe('Exit immediately after creating the build without waiting for completion.'),
|
|
46
47
|
environment: z.string().optional().describe('The name of the environment to use for the build.'),
|
|
48
|
+
failureSummary: z
|
|
49
|
+
.boolean()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Request an AI-powered failure summary (Capawesome Cloud Assist) if the build fails.'),
|
|
47
52
|
gitRef: z.string().optional().describe('The Git reference (branch, tag, or commit SHA) to build.'),
|
|
48
53
|
ipa: z
|
|
49
54
|
.union([z.boolean(), z.string()])
|
|
@@ -94,6 +99,11 @@ export default defineCommand({
|
|
|
94
99
|
consola.error('The --detached flag cannot be used with --channel or --destination flags.');
|
|
95
100
|
process.exit(1);
|
|
96
101
|
}
|
|
102
|
+
// Validate that detached flag cannot be used with failure summary
|
|
103
|
+
if (options.detached && options.failureSummary) {
|
|
104
|
+
consola.error('The --detached flag cannot be used with --failure-summary.');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
97
107
|
// Validate that channel and destination cannot be used together
|
|
98
108
|
if (options.channel && options.destination) {
|
|
99
109
|
consola.error('The --channel and --destination flags cannot be used together.');
|
|
@@ -328,7 +338,14 @@ export default defineCommand({
|
|
|
328
338
|
// Wait for build job to complete by default, unless --detached flag is set
|
|
329
339
|
const shouldWait = !options.detached;
|
|
330
340
|
if (shouldWait) {
|
|
331
|
-
await waitForJobCompletion({
|
|
341
|
+
await waitForJobCompletion({
|
|
342
|
+
jobId: response.jobId,
|
|
343
|
+
onFailed: () => offerJobFailureSummary({
|
|
344
|
+
jobId: response.jobId,
|
|
345
|
+
requested: !!options.failureSummary,
|
|
346
|
+
command: `npx @capawesome/cli apps:builds:failure-summary --app-id ${appId} --build-id ${response.id}`,
|
|
347
|
+
}),
|
|
348
|
+
});
|
|
332
349
|
const appBuild = await appBuildsService.findOne({
|
|
333
350
|
appId,
|
|
334
351
|
appBuildId: response.id,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import appBuildsService from '../../../services/app-builds.js';
|
|
2
|
+
import { withAuth } from '../../../utils/auth.js';
|
|
3
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
4
|
+
import { printJobFailureSummary } from '../../../utils/job-failure-summary.js';
|
|
5
|
+
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
6
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
7
|
+
import consola from 'consola';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
description: 'Explain why an app build failed using Capawesome Cloud Assist (AI).',
|
|
11
|
+
options: defineOptions(z.object({
|
|
12
|
+
appId: z
|
|
13
|
+
.uuid({
|
|
14
|
+
message: 'App ID must be a UUID.',
|
|
15
|
+
})
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('App ID of the build to summarize.'),
|
|
18
|
+
buildId: z
|
|
19
|
+
.uuid({
|
|
20
|
+
message: 'Build ID must be a UUID.',
|
|
21
|
+
})
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Build ID to summarize.'),
|
|
24
|
+
buildNumber: z.string().optional().describe('Build number to summarize (e.g., "1", "42").'),
|
|
25
|
+
})),
|
|
26
|
+
action: withAuth(async (options) => {
|
|
27
|
+
let { appId, buildId, buildNumber } = options;
|
|
28
|
+
// Prompt for app ID if not provided
|
|
29
|
+
if (!appId) {
|
|
30
|
+
if (!isInteractive()) {
|
|
31
|
+
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const organizationId = await promptOrganizationSelection();
|
|
35
|
+
appId = await promptAppSelection(organizationId);
|
|
36
|
+
}
|
|
37
|
+
// Convert build number to build ID if provided
|
|
38
|
+
if (!buildId && buildNumber) {
|
|
39
|
+
const builds = await appBuildsService.findAll({ appId, numberAsString: buildNumber });
|
|
40
|
+
if (builds.length === 0) {
|
|
41
|
+
consola.error(`Build #${buildNumber} not found.`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
buildId = builds[0]?.id;
|
|
45
|
+
}
|
|
46
|
+
// Prompt for build ID if not provided
|
|
47
|
+
if (!buildId) {
|
|
48
|
+
if (!isInteractive()) {
|
|
49
|
+
consola.error('You must provide a build ID when running in non-interactive environment.');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const appBuilds = await appBuildsService.findAll({ appId });
|
|
53
|
+
if (appBuilds.length === 0) {
|
|
54
|
+
consola.error('There are no builds for this app.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
58
|
+
buildId = await prompt('Which build do you want a failure summary for?', {
|
|
59
|
+
type: 'select',
|
|
60
|
+
options: appBuilds.map((build) => ({
|
|
61
|
+
label: `Build #${build.numberAsString} (${build.platform} - ${build.type})`,
|
|
62
|
+
value: build.id,
|
|
63
|
+
})),
|
|
64
|
+
});
|
|
65
|
+
if (!buildId) {
|
|
66
|
+
consola.error('You must select a build.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const build = await appBuildsService.findOne({ appId, appBuildId: buildId });
|
|
71
|
+
await printJobFailureSummary({ jobId: build.jobId });
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
@@ -4,6 +4,7 @@ import appDeploymentsService from '../../../services/app-deployments.js';
|
|
|
4
4
|
import appDestinationsService from '../../../services/app-destinations.js';
|
|
5
5
|
import { withAuth } from '../../../utils/auth.js';
|
|
6
6
|
import { isInteractive } from '../../../utils/environment.js';
|
|
7
|
+
import { offerJobFailureSummary } from '../../../utils/job-failure-summary.js';
|
|
7
8
|
import { waitForJobCompletion } from '../../../utils/job.js';
|
|
8
9
|
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
9
10
|
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
@@ -31,6 +32,10 @@ export default defineCommand({
|
|
|
31
32
|
.boolean()
|
|
32
33
|
.optional()
|
|
33
34
|
.describe('Exit immediately after creating the deployment without waiting for completion.'),
|
|
35
|
+
failureSummary: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Request an AI-powered failure summary (Capawesome Cloud Assist) if the deployment fails.'),
|
|
34
39
|
})),
|
|
35
40
|
action: withAuth(async (options) => {
|
|
36
41
|
let { appId, buildId, buildNumber, channel, destination } = options;
|
|
@@ -158,7 +163,14 @@ export default defineCommand({
|
|
|
158
163
|
// Wait for deployment job to complete by default, unless --detached flag is set
|
|
159
164
|
const shouldWait = !options.detached && build.platform !== 'web';
|
|
160
165
|
if (shouldWait) {
|
|
161
|
-
await waitForJobCompletion({
|
|
166
|
+
await waitForJobCompletion({
|
|
167
|
+
jobId: response.jobId,
|
|
168
|
+
onFailed: () => offerJobFailureSummary({
|
|
169
|
+
jobId: response.jobId,
|
|
170
|
+
requested: !!options.failureSummary,
|
|
171
|
+
command: `npx @capawesome/cli apps:deployments:failure-summary --app-id ${appId} --deployment-id ${response.id}`,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
162
174
|
consola.success('Deployment completed successfully.');
|
|
163
175
|
process.exit(0);
|
|
164
176
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import appDeploymentsService from '../../../services/app-deployments.js';
|
|
2
|
+
import { withAuth } from '../../../utils/auth.js';
|
|
3
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
4
|
+
import { printJobFailureSummary } from '../../../utils/job-failure-summary.js';
|
|
5
|
+
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
6
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
7
|
+
import consola from 'consola';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
export default defineCommand({
|
|
10
|
+
description: 'Explain why an app deployment failed using Capawesome Cloud Assist (AI).',
|
|
11
|
+
options: defineOptions(z.object({
|
|
12
|
+
appId: z
|
|
13
|
+
.uuid({
|
|
14
|
+
message: 'App ID must be a UUID.',
|
|
15
|
+
})
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('App ID of the deployment to summarize.'),
|
|
18
|
+
deploymentId: z
|
|
19
|
+
.uuid({
|
|
20
|
+
message: 'Deployment ID must be a UUID.',
|
|
21
|
+
})
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Deployment ID to summarize.'),
|
|
24
|
+
})),
|
|
25
|
+
action: withAuth(async (options) => {
|
|
26
|
+
let { appId, deploymentId } = options;
|
|
27
|
+
// Prompt for app ID if not provided
|
|
28
|
+
if (!appId) {
|
|
29
|
+
if (!isInteractive()) {
|
|
30
|
+
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const organizationId = await promptOrganizationSelection();
|
|
34
|
+
appId = await promptAppSelection(organizationId);
|
|
35
|
+
}
|
|
36
|
+
// Prompt for deployment ID if not provided
|
|
37
|
+
if (!deploymentId) {
|
|
38
|
+
if (!isInteractive()) {
|
|
39
|
+
consola.error('You must provide a deployment ID when running in non-interactive environment.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const appDeployments = await appDeploymentsService.findAll({ appId });
|
|
43
|
+
if (appDeployments.length === 0) {
|
|
44
|
+
consola.error('There are no deployments for this app.');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
48
|
+
deploymentId = await prompt('Which deployment do you want a failure summary for?', {
|
|
49
|
+
type: 'select',
|
|
50
|
+
options: appDeployments.map((deployment) => ({
|
|
51
|
+
label: `Deployment ${deployment.id}`,
|
|
52
|
+
value: deployment.id,
|
|
53
|
+
})),
|
|
54
|
+
});
|
|
55
|
+
if (!deploymentId) {
|
|
56
|
+
consola.error('You must select a deployment.');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const deployment = await appDeploymentsService.findOne({ appId, appDeploymentId: deploymentId });
|
|
61
|
+
await printJobFailureSummary({ jobId: deployment.jobId });
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import appEnvironmentsService from '../../../services/app-environments.js';
|
|
2
|
+
import { withAuth } from '../../../utils/auth.js';
|
|
3
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
4
|
+
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
5
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
6
|
+
import consola from 'consola';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
export default defineCommand({
|
|
9
|
+
description: 'Get an existing environment.',
|
|
10
|
+
options: defineOptions(z.object({
|
|
11
|
+
appId: z.string().optional().describe('ID of the app.'),
|
|
12
|
+
environmentId: z.string().optional().describe('ID of the environment. Either the ID or name must be provided.'),
|
|
13
|
+
name: z.string().optional().describe('Name of the environment. Either the ID or name must be provided.'),
|
|
14
|
+
json: z.boolean().optional().describe('Output in JSON format.'),
|
|
15
|
+
})),
|
|
16
|
+
action: withAuth(async (options, args) => {
|
|
17
|
+
let { appId, environmentId, name, json } = options;
|
|
18
|
+
if (!appId) {
|
|
19
|
+
if (!isInteractive()) {
|
|
20
|
+
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const organizationId = await promptOrganizationSelection();
|
|
24
|
+
appId = await promptAppSelection(organizationId);
|
|
25
|
+
}
|
|
26
|
+
if (!environmentId && !name) {
|
|
27
|
+
if (!isInteractive()) {
|
|
28
|
+
consola.error('You must provide either the environment ID or name when running in non-interactive environment.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const environments = await appEnvironmentsService.findAll({ appId });
|
|
32
|
+
if (!environments.length) {
|
|
33
|
+
consola.error('No environments found for this app. Create one first.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
37
|
+
environmentId = await prompt('Select the environment:', {
|
|
38
|
+
type: 'select',
|
|
39
|
+
options: environments.map((env) => ({ label: env.name, value: env.id })),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
let environment;
|
|
43
|
+
if (environmentId) {
|
|
44
|
+
environment = await appEnvironmentsService.findOneById({ appId, id: environmentId });
|
|
45
|
+
}
|
|
46
|
+
else if (name) {
|
|
47
|
+
const environments = await appEnvironmentsService.findAll({ appId, name });
|
|
48
|
+
environment = environments[0];
|
|
49
|
+
}
|
|
50
|
+
if (!environment) {
|
|
51
|
+
consola.error('Environment not found.');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
if (json) {
|
|
55
|
+
console.log(JSON.stringify(environment, null, 2));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.table(environment);
|
|
59
|
+
consola.success('Environment retrieved successfully.');
|
|
60
|
+
}
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
@@ -12,7 +12,8 @@ export default defineCommand({
|
|
|
12
12
|
description: 'Set environment variables and secrets.',
|
|
13
13
|
options: defineOptions(z.object({
|
|
14
14
|
appId: z.string().optional().describe('ID of the app.'),
|
|
15
|
-
environmentId: z.string().optional().describe('ID of the environment.'),
|
|
15
|
+
environmentId: z.string().optional().describe('ID of the environment. Either the ID or name must be provided.'),
|
|
16
|
+
name: z.string().optional().describe('Name of the environment. Either the ID or name must be provided.'),
|
|
16
17
|
variable: z
|
|
17
18
|
.array(z.string())
|
|
18
19
|
.optional()
|
|
@@ -25,7 +26,7 @@ export default defineCommand({
|
|
|
25
26
|
secretFile: z.string().optional().describe('Path to a file containing environment secrets in .env format.'),
|
|
26
27
|
})),
|
|
27
28
|
action: withAuth(async (options, args) => {
|
|
28
|
-
let { appId, environmentId, variable, variableFile, secret, secretFile } = options;
|
|
29
|
+
let { appId, environmentId, name, variable, variableFile, secret, secretFile } = options;
|
|
29
30
|
if (!appId) {
|
|
30
31
|
if (!isInteractive()) {
|
|
31
32
|
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
@@ -35,20 +36,31 @@ export default defineCommand({
|
|
|
35
36
|
appId = await promptAppSelection(organizationId);
|
|
36
37
|
}
|
|
37
38
|
if (!environmentId) {
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
if (name) {
|
|
40
|
+
const environments = await appEnvironmentsService.findAll({ appId, name });
|
|
41
|
+
const environment = environments[0];
|
|
42
|
+
if (!environment) {
|
|
43
|
+
consola.error('Environment not found.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
environmentId = environment.id;
|
|
41
47
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
else {
|
|
49
|
+
if (!isInteractive()) {
|
|
50
|
+
consola.error('You must provide either the environment ID or name when running in non-interactive environment.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const environments = await appEnvironmentsService.findAll({ appId });
|
|
54
|
+
if (!environments.length) {
|
|
55
|
+
consola.error('No environments found for this app. Create one first.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
59
|
+
environmentId = await prompt('Select the environment:', {
|
|
60
|
+
type: 'select',
|
|
61
|
+
options: environments.map((env) => ({ label: env.name, value: env.id })),
|
|
62
|
+
});
|
|
46
63
|
}
|
|
47
|
-
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
48
|
-
environmentId = await prompt('Select the environment:', {
|
|
49
|
-
type: 'select',
|
|
50
|
-
options: environments.map((env) => ({ label: env.name, value: env.id })),
|
|
51
|
-
});
|
|
52
64
|
}
|
|
53
65
|
// Parse variables from inline and file
|
|
54
66
|
const variablesMap = new Map();
|
|
@@ -9,7 +9,8 @@ export default defineCommand({
|
|
|
9
9
|
description: 'Unset environment variables and secrets.',
|
|
10
10
|
options: defineOptions(z.object({
|
|
11
11
|
appId: z.string().optional().describe('ID of the app.'),
|
|
12
|
-
environmentId: z.string().optional().describe('ID of the environment.'),
|
|
12
|
+
environmentId: z.string().optional().describe('ID of the environment. Either the ID or name must be provided.'),
|
|
13
|
+
name: z.string().optional().describe('Name of the environment. Either the ID or name must be provided.'),
|
|
13
14
|
variable: z
|
|
14
15
|
.array(z.string())
|
|
15
16
|
.optional()
|
|
@@ -20,7 +21,7 @@ export default defineCommand({
|
|
|
20
21
|
.describe('Key of the environment secret to unset. Can be specified multiple times.'),
|
|
21
22
|
})),
|
|
22
23
|
action: withAuth(async (options, args) => {
|
|
23
|
-
let { appId, environmentId, variable: variableKeys, secret: secretKeys } = options;
|
|
24
|
+
let { appId, environmentId, name, variable: variableKeys, secret: secretKeys } = options;
|
|
24
25
|
if (!appId) {
|
|
25
26
|
if (!isInteractive()) {
|
|
26
27
|
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
@@ -30,20 +31,31 @@ export default defineCommand({
|
|
|
30
31
|
appId = await promptAppSelection(organizationId);
|
|
31
32
|
}
|
|
32
33
|
if (!environmentId) {
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
if (name) {
|
|
35
|
+
const environments = await appEnvironmentsService.findAll({ appId, name });
|
|
36
|
+
const environment = environments[0];
|
|
37
|
+
if (!environment) {
|
|
38
|
+
consola.error('Environment not found.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
environmentId = environment.id;
|
|
36
42
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
else {
|
|
44
|
+
if (!isInteractive()) {
|
|
45
|
+
consola.error('You must provide either the environment ID or name when running in non-interactive environment.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const environments = await appEnvironmentsService.findAll({ appId });
|
|
49
|
+
if (!environments.length) {
|
|
50
|
+
consola.error('No environments found for this app. Create one first.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
54
|
+
environmentId = await prompt('Select the environment:', {
|
|
55
|
+
type: 'select',
|
|
56
|
+
options: environments.map((env) => ({ label: env.name, value: env.id })),
|
|
57
|
+
});
|
|
41
58
|
}
|
|
42
|
-
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
43
|
-
environmentId = await prompt('Select the environment:', {
|
|
44
|
-
type: 'select',
|
|
45
|
-
options: environments.map((env) => ({ label: env.name, value: env.id })),
|
|
46
|
-
});
|
|
47
59
|
}
|
|
48
60
|
if (!variableKeys?.length && !secretKeys?.length) {
|
|
49
61
|
consola.error('You must provide at least one variable key or secret key to unset.');
|
|
@@ -3,9 +3,16 @@ import consola from 'consola';
|
|
|
3
3
|
import { promises as fs } from 'fs';
|
|
4
4
|
import pathModule from 'path';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
7
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
8
|
+
const APP_TYPES = ['capacitor', 'cordova'];
|
|
6
9
|
export default defineCommand({
|
|
7
10
|
description: 'Generate a new code signing key pair for Live Updates.',
|
|
8
11
|
options: defineOptions(z.object({
|
|
12
|
+
appType: z
|
|
13
|
+
.enum(APP_TYPES)
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('The app type to configure code signing for. Either `capacitor` or `cordova`.'),
|
|
9
16
|
publicKeyPath: z
|
|
10
17
|
.string()
|
|
11
18
|
.optional()
|
|
@@ -54,21 +61,14 @@ export default defineCommand({
|
|
|
54
61
|
consola.log('Private key saved to: ' + absolutePrivateKeyPath);
|
|
55
62
|
consola.log('');
|
|
56
63
|
consola.warn('IMPORTANT: Keep your private key safe and never commit it to version control!');
|
|
64
|
+
const appType = await resolveAppType(options.appType);
|
|
65
|
+
if (appType) {
|
|
66
|
+
// Format the public key for JSON output (remove line breaks)
|
|
67
|
+
const publicKeyForJson = publicKey.replace(/\n/g, '');
|
|
68
|
+
consola.log('');
|
|
69
|
+
printSigningKeyConfig(appType, publicKeyForJson);
|
|
70
|
+
}
|
|
57
71
|
consola.log('');
|
|
58
|
-
consola.log('To configure code signing in the Capacitor Live Update plugin, add the following to your Capacitor Configuration file:');
|
|
59
|
-
consola.log('');
|
|
60
|
-
// Format the public key for JSON output (remove line breaks)
|
|
61
|
-
const publicKeyForJson = publicKey.replace(/\n/g, '');
|
|
62
|
-
// Print the JSON configuration
|
|
63
|
-
const config = {
|
|
64
|
-
plugins: {
|
|
65
|
-
LiveUpdate: {
|
|
66
|
-
publicKey: publicKeyForJson,
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
consola.log(JSON.stringify(config, null, 2));
|
|
71
|
-
console.log('');
|
|
72
72
|
consola.success('Code signing key pair generated successfully!');
|
|
73
73
|
}
|
|
74
74
|
catch (error) {
|
|
@@ -77,3 +77,48 @@ export default defineCommand({
|
|
|
77
77
|
}
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
|
+
const resolveAppType = async (appType) => {
|
|
81
|
+
if (appType) {
|
|
82
|
+
return appType;
|
|
83
|
+
}
|
|
84
|
+
if (!isInteractive()) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
88
|
+
return prompt('Which app type do you want to configure code signing for?', {
|
|
89
|
+
type: 'select',
|
|
90
|
+
options: [
|
|
91
|
+
{ label: 'Capacitor', value: 'capacitor' },
|
|
92
|
+
{ label: 'Cordova', value: 'cordova' },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const printSigningKeyConfig = (appType, publicKey) => {
|
|
97
|
+
if (appType === 'cordova') {
|
|
98
|
+
const config = {
|
|
99
|
+
cordova: {
|
|
100
|
+
plugins: {
|
|
101
|
+
'@capawesome/cordova-live-update': {
|
|
102
|
+
PUBLIC_KEY: publicKey,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
consola.log('To configure code signing in the Cordova Live Update plugin, add the following to your `package.json` file:');
|
|
108
|
+
consola.log('');
|
|
109
|
+
consola.log(JSON.stringify(config, null, 2));
|
|
110
|
+
consola.log('');
|
|
111
|
+
consola.warn('If the plugin has already been added, you must re-add it for the changes to take effect.');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const config = {
|
|
115
|
+
plugins: {
|
|
116
|
+
LiveUpdate: {
|
|
117
|
+
publicKey,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
consola.log('To configure code signing in the Capacitor Live Update plugin, add the following to your Capacitor configuration file:');
|
|
122
|
+
consola.log('');
|
|
123
|
+
consola.log(JSON.stringify(config, null, 2));
|
|
124
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ const config = defineConfig({
|
|
|
30
30
|
'apps:unlink': await import('./commands/apps/unlink.js').then((mod) => mod.default),
|
|
31
31
|
'apps:builds:cancel': await import('./commands/apps/builds/cancel.js').then((mod) => mod.default),
|
|
32
32
|
'apps:builds:create': await import('./commands/apps/builds/create.js').then((mod) => mod.default),
|
|
33
|
+
'apps:builds:failure-summary': await import('./commands/apps/builds/failure-summary.js').then((mod) => mod.default),
|
|
33
34
|
'apps:builds:get': await import('./commands/apps/builds/get.js').then((mod) => mod.default),
|
|
34
35
|
'apps:builds:list': await import('./commands/apps/builds/list.js').then((mod) => mod.default),
|
|
35
36
|
'apps:builds:logs': await import('./commands/apps/builds/logs.js').then((mod) => mod.default),
|
|
@@ -51,6 +52,7 @@ const config = defineConfig({
|
|
|
51
52
|
'apps:channels:update': await import('./commands/apps/channels/update.js').then((mod) => mod.default),
|
|
52
53
|
'apps:deployments:create': await import('./commands/apps/deployments/create.js').then((mod) => mod.default),
|
|
53
54
|
'apps:deployments:cancel': await import('./commands/apps/deployments/cancel.js').then((mod) => mod.default),
|
|
55
|
+
'apps:deployments:failure-summary': await import('./commands/apps/deployments/failure-summary.js').then((mod) => mod.default),
|
|
54
56
|
'apps:deployments:get': await import('./commands/apps/deployments/get.js').then((mod) => mod.default),
|
|
55
57
|
'apps:deployments:list': await import('./commands/apps/deployments/list.js').then((mod) => mod.default),
|
|
56
58
|
'apps:deployments:logs': await import('./commands/apps/deployments/logs.js').then((mod) => mod.default),
|
|
@@ -65,6 +67,7 @@ const config = defineConfig({
|
|
|
65
67
|
'apps:devices:unforcechannel': await import('./commands/apps/devices/unforcechannel.js').then((mod) => mod.default),
|
|
66
68
|
'apps:environments:create': await import('./commands/apps/environments/create.js').then((mod) => mod.default),
|
|
67
69
|
'apps:environments:delete': await import('./commands/apps/environments/delete.js').then((mod) => mod.default),
|
|
70
|
+
'apps:environments:get': await import('./commands/apps/environments/get.js').then((mod) => mod.default),
|
|
68
71
|
'apps:environments:list': await import('./commands/apps/environments/list.js').then((mod) => mod.default),
|
|
69
72
|
'apps:environments:set': await import('./commands/apps/environments/set.js').then((mod) => mod.default),
|
|
70
73
|
'apps:environments:unset': await import('./commands/apps/environments/unset.js').then((mod) => mod.default),
|
|
@@ -34,6 +34,9 @@ class AppEnvironmentsServiceImpl {
|
|
|
34
34
|
}
|
|
35
35
|
async findAll(dto) {
|
|
36
36
|
const queryParams = new URLSearchParams();
|
|
37
|
+
if (dto.name) {
|
|
38
|
+
queryParams.append('name', dto.name);
|
|
39
|
+
}
|
|
37
40
|
if (dto.limit) {
|
|
38
41
|
queryParams.append('limit', dto.limit.toString());
|
|
39
42
|
}
|
|
@@ -51,6 +54,14 @@ class AppEnvironmentsServiceImpl {
|
|
|
51
54
|
});
|
|
52
55
|
return response.data;
|
|
53
56
|
}
|
|
57
|
+
async findOneById(dto) {
|
|
58
|
+
const response = await this.httpClient.get(`/v1/apps/${dto.appId}/environments/${dto.id}`, {
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
return response.data;
|
|
64
|
+
}
|
|
54
65
|
async setVariables(dto) {
|
|
55
66
|
await this.httpClient.post(`/v1/apps/${dto.appId}/environments/${dto.environmentId}/variables/set`, dto.variables, {
|
|
56
67
|
headers: {
|
package/dist/services/jobs.js
CHANGED
|
@@ -18,6 +18,14 @@ class JobsServiceImpl {
|
|
|
18
18
|
});
|
|
19
19
|
return response.data;
|
|
20
20
|
}
|
|
21
|
+
async generateFailureSummary(dto) {
|
|
22
|
+
const response = await this.httpClient.post(`/v1/jobs/${dto.jobId}/failure-summary`, undefined, {
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${authorizationService.getCurrentAuthorizationToken()}`,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return response.data;
|
|
28
|
+
}
|
|
21
29
|
async update(options) {
|
|
22
30
|
const { jobId, dto } = options;
|
|
23
31
|
const response = await this.httpClient.patch(`/v1/jobs/${jobId}`, dto, {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import jobsService from '../services/jobs.js';
|
|
2
|
+
import { isInteractive } from '../utils/environment.js';
|
|
3
|
+
import { prompt } from '../utils/prompt.js';
|
|
4
|
+
import consola from 'consola';
|
|
5
|
+
/**
|
|
6
|
+
* Request an AI-powered failure summary for a failed job and print it.
|
|
7
|
+
*
|
|
8
|
+
* Powered by Capawesome Cloud Assist. The summary is printed as plain text so
|
|
9
|
+
* the terminal can soft-wrap it to any width.
|
|
10
|
+
*/
|
|
11
|
+
export const printJobFailureSummary = async (options) => {
|
|
12
|
+
const { jobId } = options;
|
|
13
|
+
consola.start('Generating failure summary with Capawesome Cloud Assist...');
|
|
14
|
+
const { summary } = await jobsService.generateFailureSummary({ jobId });
|
|
15
|
+
consola.success('Failure summary generated by Capawesome Cloud Assist:');
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(summary);
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Best-effort variant of {@link printJobFailureSummary}.
|
|
21
|
+
*
|
|
22
|
+
* Warns instead of throwing so that it never masks the underlying job failure.
|
|
23
|
+
*/
|
|
24
|
+
const tryPrintJobFailureSummary = async (options) => {
|
|
25
|
+
try {
|
|
26
|
+
await printJobFailureSummary(options);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
consola.warn('Could not generate a failure summary at this time.');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Offer an AI-powered failure summary after a job has failed.
|
|
34
|
+
*
|
|
35
|
+
* - If the summary was requested up front, it is generated right away.
|
|
36
|
+
* - Otherwise, the user is asked whether they want one (interactive), or
|
|
37
|
+
* pointed to the command that generates it (non-interactive).
|
|
38
|
+
*/
|
|
39
|
+
export const offerJobFailureSummary = async (options) => {
|
|
40
|
+
const { jobId, requested, command } = options;
|
|
41
|
+
if (requested) {
|
|
42
|
+
await tryPrintJobFailureSummary({ jobId });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (isInteractive()) {
|
|
46
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
47
|
+
const wantsSummary = await prompt('Do you want Capawesome Cloud Assist to explain what went wrong?', {
|
|
48
|
+
type: 'confirm',
|
|
49
|
+
initial: false,
|
|
50
|
+
});
|
|
51
|
+
if (wantsSummary) {
|
|
52
|
+
await tryPrintJobFailureSummary({ jobId });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
consola.info(`Need help? Run \`${command}\` for an AI-powered failure summary.`);
|
|
57
|
+
}
|
|
58
|
+
};
|
package/dist/utils/job.js
CHANGED
|
@@ -52,6 +52,14 @@ export const waitForJobCompletion = async (options) => {
|
|
|
52
52
|
}
|
|
53
53
|
else if (jobStatus === 'failed') {
|
|
54
54
|
consola.error(`${capitalize(label)} failed.`);
|
|
55
|
+
if (options.onFailed) {
|
|
56
|
+
try {
|
|
57
|
+
await options.onFailed(job);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Ignore errors from the failure handler -- the job failure is what matters.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
55
63
|
process.exit(1);
|
|
56
64
|
}
|
|
57
65
|
else if (jobStatus === 'canceled') {
|