UNPKG

@capawesome/cli

Version:

The Capawesome Cloud Command Line Interface (CLI) to manage Live Updates and more.

230 lines (229 loc) 9.87 kB
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); } };