@capawesome/cli
Version:
The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.
241 lines (240 loc) • 10.6 kB
JavaScript
import appBuildsService from '../../../services/app-builds.js';
import appsService from '../../../services/apps.js';
import authorizationService from '../../../services/authorization-service.js';
import organizationsService from '../../../services/organizations.js';
import { prompt } from '../../../utils/prompt.js';
import { defineCommand, defineOptions } from '@robingenz/zli';
import consola from 'consola';
import fs from 'fs/promises';
import path from 'path';
import { isInteractive } from '../../../utils/environment.js';
import { z } from 'zod';
export default defineCommand({
description: 'Download an app build.',
options: defineOptions(z.object({
appId: z
.uuid({
message: 'App ID must be a UUID.',
})
.optional()
.describe('App ID the build belongs to.'),
buildId: z
.uuid({
message: 'Build ID must be a UUID.',
})
.optional()
.describe('Build ID to download.'),
apk: z
.union([z.boolean(), z.string()])
.optional()
.describe('Download the APK artifact. Optionally provide a file path.'),
aab: z
.union([z.boolean(), z.string()])
.optional()
.describe('Download the AAB artifact. Optionally provide a file path.'),
ipa: z
.union([z.boolean(), z.string()])
.optional()
.describe('Download the IPA artifact. Optionally provide a file path.'),
})),
action: async (options) => {
let { appId, buildId } = options;
// Check if the user is logged in
if (!authorizationService.hasAuthorizationToken()) {
consola.error('You must be logged in to run this command. Please run the `login` command first.');
process.exit(1);
}
// Prompt for app ID if not provided
if (!appId) {
if (!isInteractive()) {
consola.error('You must provide an app ID when running in non-interactive environment.');
process.exit(1);
}
const organizations = await organizationsService.findAll();
if (organizations.length === 0) {
consola.error('You must create an organization before downloading a build.');
process.exit(1);
}
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
const organizationId = await prompt('Select the organization of the app for which you want to download a build:', {
type: 'select',
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
});
if (!organizationId) {
consola.error('You must select the organization of an app for which you want to download a build.');
process.exit(1);
}
const apps = await appsService.findAll({
organizationId,
});
if (apps.length === 0) {
consola.error('You must create an app before downloading a build.');
process.exit(1);
}
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
appId = await prompt('Select the app for which you want to download a build:', {
type: 'select',
options: apps.map((app) => ({ label: app.name, value: app.id })),
});
if (!appId) {
consola.error('You must select an app to download a build for.');
process.exit(1);
}
}
// Prompt for build ID if not provided
if (!buildId) {
if (!isInteractive()) {
consola.error('You must provide a build ID when running in non-interactive environment.');
process.exit(1);
}
const builds = await appBuildsService.findAll({ appId });
if (builds.length === 0) {
consola.error('No builds found for this app.');
process.exit(1);
}
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
buildId = await prompt('Select the build you want to download:', {
type: 'select',
options: builds.map((build) => ({
label: `Build #${build.numberAsString || build.id} (${build.platform} - ${build.type})`,
value: build.id,
})),
});
if (!buildId) {
consola.error('You must select a build to download.');
process.exit(1);
}
}
// Fetch the build details to get the job ID
const build = await appBuildsService.findOne({ appId, appBuildId: buildId, relations: 'appBuildArtifacts,job' });
if (build.job?.status !== 'succeeded') {
consola.error('The build has not succeeded yet. Cannot download artifacts for incomplete builds.');
process.exit(1);
}
// Validate platform-specific artifact flags
if (build.platform === 'android' && options.ipa) {
consola.error('Cannot download IPA artifact for an Android build.');
process.exit(1);
}
if (build.platform === 'ios' && (options.apk || options.aab)) {
consola.error('Cannot download APK or AAB artifacts for an iOS build.');
process.exit(1);
}
// Determine which artifacts to download
let downloadApk = options.apk;
let downloadAab = options.aab;
let downloadIpa = options.ipa;
// Prompt for artifact types if none were provided
if (!downloadApk && !downloadAab && !downloadIpa) {
if (!isInteractive()) {
consola.error('You must specify at least one artifact type (--apk, --aab, or --ipa) when running in non-interactive environment.');
process.exit(1);
}
// Get available artifact types from the build
const availableArtifacts = build.appBuildArtifacts?.filter((artifact) => artifact.status === 'ready').map((artifact) => artifact.type) ||
[];
if (availableArtifacts.length === 0) {
consola.error('No artifacts available for download.');
process.exit(1);
}
// Create options based on available artifacts and platform
const artifactOptions = [];
if (build.platform === 'android') {
if (availableArtifacts.includes('apk')) {
artifactOptions.push({ label: 'APK', value: 'apk' });
}
if (availableArtifacts.includes('aab')) {
artifactOptions.push({ label: 'AAB', value: 'aab' });
}
}
else if (build.platform === 'ios') {
if (availableArtifacts.includes('ipa')) {
artifactOptions.push({ label: 'IPA', value: 'ipa' });
}
}
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
const selectedArtifacts = await prompt('Which artifact type(s) do you want to download:', {
type: 'multiselect',
options: artifactOptions,
});
if (!selectedArtifacts || selectedArtifacts.length === 0) {
consola.error('You must select at least one artifact type to download.');
process.exit(1);
}
// Set flags based on selection
downloadApk = selectedArtifacts.includes('apk');
downloadAab = selectedArtifacts.includes('aab');
downloadIpa = selectedArtifacts.includes('ipa');
}
// Download artifacts if flags are set
if (downloadApk) {
await handleArtifactDownload({
appId,
buildId: buildId,
buildArtifacts: build.appBuildArtifacts,
artifactType: 'apk',
filePath: typeof options.apk === 'string' ? options.apk : undefined,
});
}
if (downloadAab) {
await handleArtifactDownload({
appId,
buildId: buildId,
buildArtifacts: build.appBuildArtifacts,
artifactType: 'aab',
filePath: typeof options.aab === 'string' ? options.aab : undefined,
});
}
if (downloadIpa) {
await handleArtifactDownload({
appId,
buildId: buildId,
buildArtifacts: build.appBuildArtifacts,
artifactType: 'ipa',
filePath: typeof options.ipa === 'string' ? options.ipa : undefined,
});
}
},
});
/**
* Download a build artifact (APK, AAB, or IPA).
*/
const handleArtifactDownload = async (options) => {
const { appId, buildId, buildArtifacts, artifactType, filePath } = options;
try {
const artifactTypeUpper = artifactType.toUpperCase();
consola.start(`Downloading ${artifactTypeUpper}...`);
// Find the artifact
const artifact = buildArtifacts?.find((artifact) => artifact.type === artifactType);
if (!artifact) {
consola.warn(`No ${artifactTypeUpper} artifact found for this build.`);
return;
}
if (artifact.status !== 'ready') {
consola.warn(`${artifactTypeUpper} artifact is not ready (status: ${artifact.status}).`);
return;
}
// Download the artifact
const artifactData = await appBuildsService.downloadArtifact({
appId,
appBuildId: buildId,
artifactId: artifact.id,
});
// Determine the file path
let outputPath;
if (filePath) {
// Use provided path (can be relative or absolute)
outputPath = path.resolve(filePath);
}
else {
// Default to current working directory with build ID as filename
outputPath = path.resolve(`${buildId}.${artifactType}`);
}
// Save the file
await fs.writeFile(outputPath, Buffer.from(artifactData));
consola.success(`${artifactTypeUpper} downloaded successfully: ${outputPath}`);
}
catch (error) {
consola.error(`Failed to download ${artifactType.toUpperCase()}:`, error);
}
};