appstore-cli
Version:
A command-line interface (CLI) to interact with the Apple App Store Connect API.
419 lines (381 loc) • 19.6 kB
text/typescript
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);
}
},
};