UNPKG

appstore-cli

Version:

A command-line interface (CLI) to interact with the Apple App Store Connect API.

419 lines (381 loc) 19.6 kB
import yargs from 'yargs'; import { IPAExportConfiguration } from '../models/ipa-export-configuration.js'; import { CertificateConfiguration } from '../models/certificate-configuration.js'; import configurationService from '../services/configuration-service.js'; import { safeLogger } from '../security/dataHandler.js'; import * as fs from 'fs'; /** * CLI command for configuring export settings. */ export const exportConfigureCommand: yargs.CommandModule<{}, { method: string; 'team-id'?: string; 'provisioning-profiles'?: string; 'signing-certificate'?: string; 'installer-signing-certificate'?: string; 'compile-bitcode'?: boolean; thinning?: string; 'strip-swift-symbols'?: boolean; 'upload-bitcode'?: boolean; 'upload-symbols'?: boolean; }> = { command: 'configure', describe: 'Configure export settings for IPA creation', builder: (yargs) => { return yargs .option('method', { alias: 'm', describe: 'Export method (app-store, ad-hoc, development, enterprise)', type: 'string', demandOption: true, }) .option('team-id', { alias: 't', describe: 'Apple Developer Team ID (will be automatically fetched if not provided)', type: 'string', demandOption: false, }) .option('provisioning-profiles', { describe: 'Provisioning profiles mapping (comma-separated: bundle-id:uuid)', type: 'string', }) .option('signing-certificate', { describe: 'Specific signing certificate to use', type: 'string', }) .option('installer-signing-certificate', { describe: 'Certificate for signing the installer package', type: 'string', }) .option('compile-bitcode', { describe: 'Whether to compile bitcode (iOS only)', type: 'boolean', }) .option('thinning', { describe: 'App thinning configuration', type: 'string', }) .option('strip-swift-symbols', { describe: 'Whether to strip Swift symbols', type: 'boolean', }) .option('upload-bitcode', { describe: 'Whether to upload bitcode to iTunes Connect', type: 'boolean', }) .option('upload-symbols', { describe: 'Whether to upload symbols to iTunes Connect', type: 'boolean', }) .option('provisioning-profile-path', { describe: 'Path to the provisioning profile file (.mobileprovision)', type: 'string', }) .option('certificate-path', { describe: 'Path to the signing certificate file (.p12 or .pem)', type: 'string', }) .option('certificate-password', { describe: 'Password for the signing certificate (if encrypted)', type: 'string', }) .option('use-automatic-signing', { describe: 'Whether to use automatic code signing', type: 'boolean', }) .option('export-options-plist', { describe: 'Custom export options plist content', type: 'string', }) .check((argv) => { // Custom validation for method const validMethods = ['app-store', 'ad-hoc', 'development', 'enterprise']; if (argv.method && !validMethods.includes(argv.method)) { throw new Error(`Method must be one of: ${validMethods.join(', ')}`); } // Validate provisioning profiles format if provided if (argv.provisioningProfiles && typeof argv.provisioningProfiles === 'string') { const profiles = argv.provisioningProfiles.split(','); for (const profile of profiles) { if (profile.trim() !== '' && !profile.includes(':')) { throw new Error(`Invalid provisioning profile format: ${profile}. Expected format: bundle-id:uuid`); } } } // Validate certificate and provisioning profile paths if (argv['provisioning-profile-path']) { if (!fs.existsSync(argv['provisioning-profile-path'])) { throw new Error(`Provisioning profile file not found: ${argv['provisioning-profile-path']}`); } if (!argv['provisioning-profile-path'].endsWith('.mobileprovision')) { throw new Error('Provisioning profile file must have .mobileprovision extension'); } } if (argv['certificate-path']) { if (!fs.existsSync(argv['certificate-path'])) { throw new Error(`Certificate file not found: ${argv['certificate-path']}`); } if (!argv['certificate-path'].endsWith('.p12') && !argv['certificate-path'].endsWith('.pem')) { throw new Error('Certificate file must have .p12 or .pem extension'); } } // Validate that if certificate path is provided, we should have a password for .p12 files if (argv['certificate-path'] && argv['certificate-path'].endsWith('.p12') && argv['certificate-password'] === undefined) { throw new Error('Certificate password is required for .p12 certificate files'); } // Validate that automatic signing and manual signing options are not both enabled if (argv['use-automatic-signing'] && (argv['provisioning-profile-path'] || argv['certificate-path'])) { throw new Error('Cannot use automatic signing with manual provisioning profile or certificate paths'); } return true; }); }, handler: async (argv) => { try { // If team ID is not provided, try to fetch it from the API let teamId = argv['team-id']; if (!teamId) { console.log('Team ID not provided, attempting to fetch from App Store Connect API...'); console.log('Note: Team IDs are typically 10-character alphanumeric strings found in your Apple Developer account.'); try { const { AppStoreConnectAPI } = await import('../api-client.js'); const api = await AppStoreConnectAPI.create(); // Method 1: Try to get bundle IDs and extract Team ID from seedId field console.log('Attempting to fetch bundle IDs...'); try { const bundleIdsResponse = await api.listBundleIds(); if (bundleIdsResponse.data && bundleIdsResponse.data.length > 0) { // Extract Team ID from the seedId field (this is the most reliable method) for (const bundleId of bundleIdsResponse.data) { if (bundleId.attributes.seedId) { teamId = bundleId.attributes.seedId; console.log(`✅ Auto-detected Team ID from bundle ID seedId: ${teamId}`); break; } } // If we still haven't found a team ID, try pattern matching as fallback if (!teamId) { console.log('Seed ID not found, trying pattern matching...'); const firstBundleId = bundleIdsResponse.data[0].attributes.identifier; const parts = firstBundleId.split('.'); // Look for Team ID pattern (various approaches) // Approach 1: Standard 10-character alphanumeric Team ID for (const part of parts) { if (part.length === 10 && /^[A-Z0-9]+$/.test(part)) { teamId = part; console.log(`Auto-detected Team ID (standard format): ${teamId}`); break; } } // Approach 2: Heuristic - look for likely team identifier if (!teamId && parts.length >= 3) { const potentialTeamId = parts[1]; // Check if it looks like a reasonable team identifier if (potentialTeamId.length >= 2 && potentialTeamId.length <= 20 && !['com', 'apple', 'ios', 'app', 'test', 'demo', 'local'].includes(potentialTeamId.toLowerCase())) { teamId = potentialTeamId; console.log(`Auto-detected Team-like identifier (heuristic): ${teamId}`); console.log('Note: This may not be your actual Team ID. Please verify in your Apple Developer account.'); } } // Approach 3: Look at other parts if (!teamId) { for (let i = 1; i < parts.length; i++) { const part = parts[i]; // Skip common words and look for identifiers if (part.length >= 3 && part.length <= 15 && !['com', 'apple', 'ios', 'app', 'test', 'demo', 'local', 'mobile', 'debug'].includes(part.toLowerCase())) { teamId = part; console.log(`Auto-detected identifier (alternative): ${teamId}`); console.log('Note: This may not be your actual Team ID. Please verify in your Apple Developer account.'); break; } } } } } } catch (error) { console.log('Could not fetch bundle IDs, trying alternative methods...'); } // Method 2: If we still don't have a team ID, try to get it from apps if (!teamId) { console.log('Attempting to fetch apps...'); try { const appsResponse = await api.listApps(); if (appsResponse.data && appsResponse.data.length > 0) { // Try to extract team ID from the bundle ID of the first app const firstApp = appsResponse.data[0]; const bundleId = firstApp.attributes.bundleId; const parts = bundleId.split('.'); // Look for Team ID pattern (various approaches) // Approach 1: Standard 10-character alphanumeric Team ID for (const part of parts) { if (part.length === 10 && /^[A-Z0-9]+$/.test(part)) { teamId = part; console.log(`Auto-detected Team ID from app (standard format): ${teamId}`); break; } } // Approach 2: Heuristic - look for likely team identifier if (!teamId && parts.length >= 3) { const potentialTeamId = parts[1]; // Check if it looks like a reasonable team identifier if (potentialTeamId.length >= 2 && potentialTeamId.length <= 20 && !['com', 'apple', 'ios', 'app', 'test', 'demo', 'local'].includes(potentialTeamId.toLowerCase())) { teamId = potentialTeamId; console.log(`Auto-detected Team-like identifier from app (heuristic): ${teamId}`); console.log('Note: This may not be your actual Team ID. Please verify in your Apple Developer account.'); } } // Approach 3: Look at other parts if (!teamId) { for (let i = 1; i < parts.length; i++) { const part = parts[i]; // Skip common words and look for identifiers if (part.length >= 3 && part.length <= 15 && !['com', 'apple', 'ios', 'app', 'test', 'demo', 'local', 'mobile', 'debug'].includes(part.toLowerCase())) { teamId = part; console.log(`Auto-detected identifier from app (alternative): ${teamId}`); console.log('Note: This may not be your actual Team ID. Please verify in your Apple Developer account.'); break; } } } } } catch (error) { console.log('Could not fetch apps...'); } } // Method 3: Try to get the current user information if (!teamId) { console.log('Attempting to fetch user information...'); try { const userResponse = await api.getCurrentUser(); // Get the user's visible apps if (userResponse.data.relationships.visibleApps.data.length > 0) { // Get details for the first visible app to extract the Team ID const firstAppId = userResponse.data.relationships.visibleApps.data[0].id; const appResponse = await api.getAppDetails(firstAppId); const bundleId = appResponse.data.attributes.bundleId; // Extract Team ID from bundle ID const parts = bundleId.split('.'); // Look for Team ID pattern (various approaches) // Approach 1: Standard 10-character alphanumeric Team ID for (const part of parts) { if (part.length === 10 && /^[A-Z0-9]+$/.test(part)) { teamId = part; console.log(`Auto-detected Team ID from user's app (standard format): ${teamId}`); break; } } // Approach 2: Heuristic - look for likely team identifier if (!teamId && parts.length >= 3) { const potentialTeamId = parts[1]; // Check if it looks like a reasonable team identifier if (potentialTeamId.length >= 2 && potentialTeamId.length <= 20 && !['com', 'apple', 'ios', 'app', 'test', 'demo', 'local'].includes(potentialTeamId.toLowerCase())) { teamId = potentialTeamId; console.log(`Auto-detected Team-like identifier from user's app (heuristic): ${teamId}`); console.log('Note: This may not be your actual Team ID. Please verify in your Apple Developer account.'); } } // Approach 3: Look at other parts if (!teamId) { for (let i = 1; i < parts.length; i++) { const part = parts[i]; // Skip common words and look for identifiers if (part.length >= 3 && part.length <= 15 && !['com', 'apple', 'ios', 'app', 'test', 'demo', 'local', 'mobile', 'debug'].includes(part.toLowerCase())) { teamId = part; console.log(`Auto-detected identifier from user's app (alternative): ${teamId}`); console.log('Note: This may not be your actual Team ID. Please verify in your Apple Developer account.'); break; } } } } } catch (error) { console.log('Could not fetch user information...'); } } // If we still haven't found a team ID, we need to inform the user if (!teamId) { console.log('\n❌ Could not automatically detect Team ID from App Store Connect API.'); console.log('This is normal in some cases where Team IDs are not visible in API responses.'); console.log('\nPlease find your Team ID manually:'); console.log('1. Go to https://developer.apple.com/account'); console.log('2. Click on "Membership" in the sidebar'); console.log('3. Your Team ID will be displayed under the "Team" section'); console.log('\nThen run the command with the --team-id option:'); console.log('appstore-cli build export configure --method app-store --team-id C253N945M7'); process.exit(1); } } catch (error) { console.warn('Failed to automatically fetch Team ID from API:', (error as Error).message); console.warn('Please provide Team ID manually using --team-id option'); process.exit(1); } } // If we still don't have a team ID, exit with error if (!teamId) { console.error('Team ID is required but could not be automatically detected. Please provide it using --team-id option'); console.error('Note: Team IDs are typically 10-character alphanumeric strings found in your Apple Developer account'); process.exit(1); } // Parse provisioning profiles if provided let provisioningProfiles: Record<string, string> | undefined; if (argv['provisioning-profiles']) { provisioningProfiles = {}; const profiles = argv['provisioning-profiles'].split(','); for (const profile of profiles) { const [bundleId, uuid] = profile.split(':'); if (bundleId && uuid) { provisioningProfiles[bundleId.trim()] = uuid.trim(); } } } // Create export configuration from arguments const exportConfig: IPAExportConfiguration = { method: argv.method, teamId: teamId!, // We've already validated that teamId is not null/undefined ...(provisioningProfiles && { provisioningProfiles }), ...(argv['signing-certificate'] && { signingCertificate: argv['signing-certificate'] }), ...(argv['installer-signing-certificate'] && { installerSigningCertificate: argv['installer-signing-certificate'] }), ...(argv['compile-bitcode'] !== undefined && { compileBitcode: argv['compile-bitcode'] }), ...(argv.thinning && { thinning: argv.thinning }), ...(argv['strip-swift-symbols'] !== undefined && { stripSwiftSymbols: argv['strip-swift-symbols'] }), ...(argv['upload-bitcode'] !== undefined && { uploadBitcode: argv['upload-bitcode'] }), ...(argv['upload-symbols'] !== undefined && { uploadSymbols: argv['upload-symbols'] }), }; // Save the configuration configurationService.saveExportConfiguration(exportConfig); // Create and save certificate configuration if provided if (argv['provisioning-profile-path'] || argv['certificate-path'] || argv['use-automatic-signing'] !== undefined || argv['export-options-plist']) { const certConfig: CertificateConfiguration = {}; if (argv['provisioning-profile-path']) { certConfig.provisioningProfilePath = argv['provisioning-profile-path'] as string; } if (argv['certificate-path']) { certConfig.certificatePath = argv['certificate-path'] as string; } if (argv['certificate-password']) { certConfig.certificatePassword = argv['certificate-password'] as string; } if (argv['use-automatic-signing'] !== undefined) { certConfig.useAutomaticSigning = argv['use-automatic-signing'] as boolean; } if (argv['export-options-plist']) { certConfig.exportOptionsPlist = argv['export-options-plist'] as string; } configurationService.saveCertificateConfiguration(certConfig); } // Output success message console.log('Export configuration saved successfully'); } catch (error) { safeLogger.error('Failed to configure export', { error: (error as Error).message }); process.exit(1); } }, };