@capawesome/cli 4.10.0 → 4.12.0-dev.5398bc6.1781010159
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 +21 -0
- package/dist/commands/apps/builds/create.js +26 -1
- package/dist/commands/apps/builds/failure-summary.js +73 -0
- package/dist/commands/apps/certificates/create.js +8 -0
- package/dist/commands/apps/deployments/create.js +13 -1
- package/dist/commands/apps/deployments/failure-summary.js +63 -0
- package/dist/commands/apps/destinations/create.js +8 -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/create.js +8 -5
- package/dist/commands/apps/liveupdates/generate-signing-key.js +59 -14
- package/dist/commands/apps/liveupdates/upload.js +8 -1
- package/dist/commands/apps/liveupdates/upload.test.js +47 -0
- package/dist/index.js +3 -0
- package/dist/services/app-environments.js +11 -0
- package/dist/services/jobs.js +8 -0
- package/dist/utils/hash.js +6 -1
- package/dist/utils/job-failure-summary.js +58 -0
- package/dist/utils/job.js +8 -0
- package/dist/utils/signature.js +4 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
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
|
+
|
|
14
|
+
## [4.11.0](https://github.com/capawesome-team/cli/compare/v4.10.0...v4.11.0) (2026-06-02)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **apps:** derive platform from app type for single-platform apps ([#165](https://github.com/capawesome-team/cli/issues/165)) ([c14efd6](https://github.com/capawesome-team/cli/commit/c14efd607190c7dc8dbc6f2550fde20bd90faeb4))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* hash and sign buffers in chunks to avoid `RangeError: data is too long` ([#164](https://github.com/capawesome-team/cli/issues/164)) ([7c37bcf](https://github.com/capawesome-team/cli/commit/7c37bcfd3bd6d286acdc0674082aea35ec45115b))
|
|
25
|
+
|
|
5
26
|
## [4.10.0](https://github.com/capawesome-team/cli/compare/v4.9.3...v4.10.0) (2026-05-18)
|
|
6
27
|
|
|
7
28
|
|
|
@@ -3,10 +3,12 @@ import appBuildSourcesService from '../../../services/app-build-sources.js';
|
|
|
3
3
|
import appBuildsService from '../../../services/app-builds.js';
|
|
4
4
|
import appCertificatesService from '../../../services/app-certificates.js';
|
|
5
5
|
import appEnvironmentsService from '../../../services/app-environments.js';
|
|
6
|
+
import appsService from '../../../services/apps.js';
|
|
6
7
|
import { parseKeyValuePairs } from '../../../utils/app-environments.js';
|
|
7
8
|
import { withAuth } from '../../../utils/auth.js';
|
|
8
9
|
import { createBufferFromPath } from '../../../utils/buffer.js';
|
|
9
10
|
import { isInteractive } from '../../../utils/environment.js';
|
|
11
|
+
import { offerJobFailureSummary } from '../../../utils/job-failure-summary.js';
|
|
10
12
|
import { isDirectory, isReadable } from '../../../utils/file.js';
|
|
11
13
|
import { waitForJobCompletion } from '../../../utils/job.js';
|
|
12
14
|
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
@@ -43,6 +45,10 @@ export default defineCommand({
|
|
|
43
45
|
.optional()
|
|
44
46
|
.describe('Exit immediately after creating the build without waiting for completion.'),
|
|
45
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.'),
|
|
46
52
|
gitRef: z.string().optional().describe('The Git reference (branch, tag, or commit SHA) to build.'),
|
|
47
53
|
ipa: z
|
|
48
54
|
.union([z.boolean(), z.string()])
|
|
@@ -93,6 +99,11 @@ export default defineCommand({
|
|
|
93
99
|
consola.error('The --detached flag cannot be used with --channel or --destination flags.');
|
|
94
100
|
process.exit(1);
|
|
95
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
|
+
}
|
|
96
107
|
// Validate that channel and destination cannot be used together
|
|
97
108
|
if (options.channel && options.destination) {
|
|
98
109
|
consola.error('The --channel and --destination flags cannot be used together.');
|
|
@@ -147,6 +158,13 @@ export default defineCommand({
|
|
|
147
158
|
const organizationId = await promptOrganizationSelection({ allowCreate: true });
|
|
148
159
|
appId = await promptAppSelection(organizationId, { allowCreate: true });
|
|
149
160
|
}
|
|
161
|
+
// Derive platform from app type for single-platform apps
|
|
162
|
+
if (!platform) {
|
|
163
|
+
const app = await appsService.findOne({ appId });
|
|
164
|
+
if (app.type === 'android' || app.type === 'ios') {
|
|
165
|
+
platform = app.type;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
150
168
|
// Prompt for platform if not provided
|
|
151
169
|
if (!platform) {
|
|
152
170
|
if (!isInteractive()) {
|
|
@@ -320,7 +338,14 @@ export default defineCommand({
|
|
|
320
338
|
// Wait for build job to complete by default, unless --detached flag is set
|
|
321
339
|
const shouldWait = !options.detached;
|
|
322
340
|
if (shouldWait) {
|
|
323
|
-
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
|
+
});
|
|
324
349
|
const appBuild = await appBuildsService.findOne({
|
|
325
350
|
appId,
|
|
326
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
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import appCertificatesService from '../../../services/app-certificates.js';
|
|
2
2
|
import appProvisioningProfilesService from '../../../services/app-provisioning-profiles.js';
|
|
3
|
+
import appsService from '../../../services/apps.js';
|
|
3
4
|
import { withAuth } from '../../../utils/auth.js';
|
|
4
5
|
import { isInteractive } from '../../../utils/environment.js';
|
|
5
6
|
import { isReadable } from '../../../utils/file.js';
|
|
@@ -55,6 +56,13 @@ export default defineCommand({
|
|
|
55
56
|
process.exit(1);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
// Derive platform from app type for single-platform apps
|
|
60
|
+
if (!platform) {
|
|
61
|
+
const app = await appsService.findOne({ appId });
|
|
62
|
+
if (app.type === 'android' || app.type === 'ios') {
|
|
63
|
+
platform = app.type;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
58
66
|
// 3. Select platform
|
|
59
67
|
if (!platform) {
|
|
60
68
|
if (!isInteractive()) {
|
|
@@ -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
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import appAppleApiKeysService from '../../../services/app-apple-api-keys.js';
|
|
2
2
|
import appDestinationsService from '../../../services/app-destinations.js';
|
|
3
3
|
import appGoogleServiceAccountKeysService from '../../../services/app-google-service-account-keys.js';
|
|
4
|
+
import appsService from '../../../services/apps.js';
|
|
4
5
|
import { withAuth } from '../../../utils/auth.js';
|
|
5
6
|
import { isInteractive } from '../../../utils/environment.js';
|
|
6
7
|
import { isReadable } from '../../../utils/file.js';
|
|
@@ -57,6 +58,13 @@ export default defineCommand({
|
|
|
57
58
|
process.exit(1);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
// Derive platform from app type for single-platform apps
|
|
62
|
+
if (!platform) {
|
|
63
|
+
const app = await appsService.findOne({ appId });
|
|
64
|
+
if (app.type === 'android' || app.type === 'ios') {
|
|
65
|
+
platform = app.type;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
60
68
|
// 3. Select platform
|
|
61
69
|
if (!platform) {
|
|
62
70
|
if (!isInteractive()) {
|
|
@@ -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.');
|
|
@@ -273,7 +273,7 @@ export default defineCommand({
|
|
|
273
273
|
}
|
|
274
274
|
// Deploy to channels
|
|
275
275
|
const rolloutPercentage = (options.rolloutPercentage ?? 100) / 100;
|
|
276
|
-
const
|
|
276
|
+
const appDeploymentIds = [];
|
|
277
277
|
for (const channelName of channel) {
|
|
278
278
|
consola.start(`Creating deployment for channel "${channelName}"...`);
|
|
279
279
|
const deployment = await appDeploymentsService.create({
|
|
@@ -282,7 +282,7 @@ export default defineCommand({
|
|
|
282
282
|
appChannelName: channelName,
|
|
283
283
|
rolloutPercentage,
|
|
284
284
|
});
|
|
285
|
-
|
|
285
|
+
appDeploymentIds.push(deployment.id);
|
|
286
286
|
consola.info(`Deployment ID: ${deployment.id}`);
|
|
287
287
|
consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${deployment.id}`);
|
|
288
288
|
consola.success('Deployment created successfully.');
|
|
@@ -290,9 +290,12 @@ export default defineCommand({
|
|
|
290
290
|
// Output JSON if json flag is set
|
|
291
291
|
if (json) {
|
|
292
292
|
console.log(JSON.stringify({
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
appBuildId: response.id,
|
|
294
|
+
appBuildNumberAsString: response.numberAsString,
|
|
295
|
+
appDeploymentIds,
|
|
296
|
+
buildId: response.id, // Deprecated, use appBuildId instead
|
|
297
|
+
buildNumberAsString: response.numberAsString, // Deprecated, use appBuildNumberAsString instead
|
|
298
|
+
deploymentIds: appDeploymentIds, // Deprecated, use appDeploymentIds instead
|
|
296
299
|
}, null, 2));
|
|
297
300
|
}
|
|
298
301
|
}),
|
|
@@ -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
|
+
};
|
|
@@ -92,6 +92,7 @@ export default defineCommand({
|
|
|
92
92
|
.string()
|
|
93
93
|
.optional()
|
|
94
94
|
.describe('The exact iOS bundle version (`CFBundleVersion`) that the bundle does not support.'),
|
|
95
|
+
json: z.boolean().optional().describe('Output in JSON format.'),
|
|
95
96
|
path: z
|
|
96
97
|
.string()
|
|
97
98
|
.optional()
|
|
@@ -116,7 +117,7 @@ export default defineCommand({
|
|
|
116
117
|
yes: z.boolean().optional().describe('Skip confirmation prompt.'),
|
|
117
118
|
}), { y: 'yes' }),
|
|
118
119
|
action: withAuth(async (options, args) => {
|
|
119
|
-
let { androidEq, androidMax, androidMin, appId, artifactType, channel, commitMessage, commitRef, commitSha, customProperty, expiresInDays, gitRef, iosEq, iosMax, iosMin, path, privateKey, rolloutPercentage, } = options;
|
|
120
|
+
let { androidEq, androidMax, androidMin, appId, artifactType, channel, commitMessage, commitRef, commitSha, customProperty, expiresInDays, gitRef, iosEq, iosMax, iosMin, json, path, privateKey, rolloutPercentage, } = options;
|
|
120
121
|
if (expiresInDays) {
|
|
121
122
|
consola.warn('The `--expires-in-days` option is deprecated and will be removed in a future version. Bundle expiration is now managed by the data retention policy of your organization billing plan.');
|
|
122
123
|
}
|
|
@@ -289,6 +290,12 @@ export default defineCommand({
|
|
|
289
290
|
consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${updateBundleResponse.appDeploymentId}`);
|
|
290
291
|
}
|
|
291
292
|
consola.success('Live Update successfully uploaded.');
|
|
293
|
+
if (json) {
|
|
294
|
+
console.log(JSON.stringify({
|
|
295
|
+
appBuildId: createBundleResponse.appBuildId,
|
|
296
|
+
appBuildArtifactId: createBundleResponse.id,
|
|
297
|
+
}, null, 2));
|
|
298
|
+
}
|
|
292
299
|
}),
|
|
293
300
|
});
|
|
294
301
|
const uploadFile = async (options) => {
|
|
@@ -180,6 +180,53 @@ describe('apps-liveupdates-upload', () => {
|
|
|
180
180
|
expect(mockConsola.info).toHaveBeenCalledWith(`Build Artifact ID: ${bundleId}`);
|
|
181
181
|
expect(mockConsola.success).toHaveBeenCalledWith('Live Update successfully uploaded.');
|
|
182
182
|
});
|
|
183
|
+
it('should output JSON when json flag is set', async () => {
|
|
184
|
+
const appId = 'app-123';
|
|
185
|
+
const bundlePath = './dist';
|
|
186
|
+
const bundleId = 'bundle-456';
|
|
187
|
+
const appBuildId = 'build-789';
|
|
188
|
+
const testToken = 'test-token';
|
|
189
|
+
const testBuffer = Buffer.from('test');
|
|
190
|
+
const options = {
|
|
191
|
+
appId,
|
|
192
|
+
path: bundlePath,
|
|
193
|
+
artifactType: 'zip',
|
|
194
|
+
rollout: 1,
|
|
195
|
+
json: true,
|
|
196
|
+
};
|
|
197
|
+
mockIsReadable.mockResolvedValue(true);
|
|
198
|
+
mockIsDirectory.mockResolvedValue(true);
|
|
199
|
+
mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([
|
|
200
|
+
{ href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' },
|
|
201
|
+
]);
|
|
202
|
+
const mockZip = await import('../../../utils/zip.js');
|
|
203
|
+
const mockHash = await import('../../../utils/hash.js');
|
|
204
|
+
vi.mocked(mockZip.default.isZipped).mockReturnValue(false);
|
|
205
|
+
vi.mocked(mockZip.default.zipFolder).mockResolvedValue(testBuffer);
|
|
206
|
+
vi.mocked(mockHash.createHash).mockResolvedValue('test-hash');
|
|
207
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
208
|
+
nock(DEFAULT_API_BASE_URL)
|
|
209
|
+
.get(`/v1/apps/${appId}`)
|
|
210
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
211
|
+
.reply(200, { id: appId, name: 'Test App' });
|
|
212
|
+
nock(DEFAULT_API_BASE_URL)
|
|
213
|
+
.post(`/v1/apps/${appId}/bundles`)
|
|
214
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
215
|
+
.reply(201, { id: bundleId, appBuildId });
|
|
216
|
+
nock(DEFAULT_API_BASE_URL)
|
|
217
|
+
.post(`/v1/apps/${appId}/bundles/${bundleId}/files`)
|
|
218
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
219
|
+
.reply(201, { id: 'file-123' });
|
|
220
|
+
nock(DEFAULT_API_BASE_URL)
|
|
221
|
+
.patch(`/v1/apps/${appId}/bundles/${bundleId}`)
|
|
222
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
223
|
+
.reply(200, { id: bundleId });
|
|
224
|
+
await uploadCommand.action(options, undefined);
|
|
225
|
+
expect(logSpy).toHaveBeenCalledWith(JSON.stringify({
|
|
226
|
+
appBuildId,
|
|
227
|
+
appBuildArtifactId: bundleId,
|
|
228
|
+
}, null, 2));
|
|
229
|
+
});
|
|
183
230
|
it('should handle private key file path', async () => {
|
|
184
231
|
const appId = 'app-123';
|
|
185
232
|
const bundlePath = './dist';
|
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, {
|
package/dist/utils/hash.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
const CHUNK_SIZE = 64 * 1024 * 1024;
|
|
1
2
|
export const createHash = async (data) => {
|
|
2
3
|
const crypto = await import('crypto');
|
|
3
|
-
|
|
4
|
+
const hash = crypto.createHash('sha256');
|
|
5
|
+
for (let offset = 0; offset < data.length; offset += CHUNK_SIZE) {
|
|
6
|
+
hash.update(data.subarray(offset, offset + CHUNK_SIZE));
|
|
7
|
+
}
|
|
8
|
+
return hash.digest('hex');
|
|
4
9
|
};
|
|
@@ -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') {
|
package/dist/utils/signature.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
const CHUNK_SIZE = 64 * 1024 * 1024;
|
|
1
2
|
export const createSignature = async (privateKey, data) => {
|
|
2
3
|
const crypto = await import('crypto');
|
|
3
4
|
const privateKeyObject = crypto.createPrivateKey(privateKey);
|
|
4
5
|
const sign = crypto.createSign('sha256');
|
|
5
|
-
|
|
6
|
+
for (let offset = 0; offset < data.length; offset += CHUNK_SIZE) {
|
|
7
|
+
sign.update(data.subarray(offset, offset + CHUNK_SIZE));
|
|
8
|
+
}
|
|
6
9
|
sign.end();
|
|
7
10
|
return sign.sign(privateKeyObject).toString('base64');
|
|
8
11
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capawesome/cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.0-dev.5398bc6.1781010159",
|
|
4
4
|
"description": "The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -78,14 +78,14 @@
|
|
|
78
78
|
"@types/mime": "3.0.4",
|
|
79
79
|
"@types/node": "24.2.1",
|
|
80
80
|
"@types/semver": "7.5.8",
|
|
81
|
-
"@vitest/ui": "
|
|
81
|
+
"@vitest/ui": "4.1.7",
|
|
82
82
|
"commit-and-tag-version": "12.6.1",
|
|
83
83
|
"nock": "14.0.10",
|
|
84
84
|
"prettier": "3.3.3",
|
|
85
85
|
"rimraf": "6.0.1",
|
|
86
86
|
"tsc-alias": "1.8.16",
|
|
87
87
|
"typescript": "5.6.3",
|
|
88
|
-
"vitest": "
|
|
88
|
+
"vitest": "4.1.7"
|
|
89
89
|
},
|
|
90
90
|
"prettier": "@ionic/prettier-config"
|
|
91
91
|
}
|