@capawesome/cli
Version:
The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.
230 lines (229 loc) • 9.87 kB
JavaScript
import appBuildsService from '../../../services/app-builds.js';
import { withAuth } from '../../../utils/auth.js';
import { prompt, promptAppSelection, promptOrganizationSelection } 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.'),
zip: z
.union([z.boolean(), z.string()])
.optional()
.describe('Download the ZIP artifact. Optionally provide a file path.'),
})),
action: withAuth(async (options) => {
let { appId, buildId } = options;
// 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 organizationId = await promptOrganizationSelection();
appId = await promptAppSelection(organizationId);
}
// 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);
}
if (build.platform === 'web' && (options.apk || options.aab || options.ipa)) {
consola.error('Cannot download APK, AAB, or IPA artifacts for a Web build.');
process.exit(1);
}
// Determine which artifacts to download
let downloadApk = options.apk;
let downloadAab = options.aab;
let downloadIpa = options.ipa;
let downloadZip = options.zip;
// Prompt for artifact types if none were provided
if (!downloadApk && !downloadAab && !downloadIpa && !downloadZip) {
if (!isInteractive()) {
consola.error('You must specify at least one artifact type (--apk, --aab, --ipa, or --zip) 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' });
}
}
else if (build.platform === 'web') {
if (availableArtifacts.includes('zip')) {
artifactOptions.push({ label: 'ZIP', value: 'zip' });
}
}
// @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');
downloadZip = selectedArtifacts.includes('zip');
}
// 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,
});
}
if (downloadZip) {
await handleArtifactDownload({
appId,
buildId: buildId,
buildArtifacts: build.appBuildArtifacts,
artifactType: 'zip',
filePath: typeof options.zip === 'string' ? options.zip : undefined,
});
}
}),
});
/**
* Download a build artifact (APK, AAB, IPA, or ZIP).
*/
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);
}
};