feat(assets): standardize asset configuration with capacitor-assets

- Replace manual ImageMagick scripts with official capacitor-assets toolchain
- Consolidate duplicate asset sources to single resources/ directory
- Implement comprehensive asset configuration schema and validation
- Add CI safeguards for asset validation and platform asset detection
- Convert capacitor.config.json to TypeScript format
- Pin Node.js version for deterministic builds
- Remove legacy manual asset generation scripts:
  * generate-icons.sh, generate-ios-assets.sh, generate-android-icons.sh
  * check-android-resources.sh, check-ios-resources.sh
  * purge-generated-assets.sh
- Add new asset management commands:
  * assets:config - generate/update configurations
  * assets:validate - validate configurations
  * assets:clean - clean generated assets (dev only)
  * build:native - build with asset generation
- Create GitHub Actions workflow for asset validation
- Update documentation with new asset management workflow

This standardization eliminates asset duplication, improves build reliability,
and provides a maintainable asset management system using Capacitor defaults.

Breaking Changes: Manual asset generation scripts removed
Migration: Assets now sourced from resources/ directory only
CI: Automated validation prevents committed platform assets
This commit is contained in:
Matthew Raymer
2025-08-14 07:22:26 +00:00
parent 1a6b1e6151
commit a284067522
25 changed files with 1126 additions and 987 deletions

174
scripts/assets-config.js Normal file
View File

@@ -0,0 +1,174 @@
#!/usr/bin/env node
/**
* TimeSafari Asset Configuration Generator
* Generates capacitor-assets configuration files with deterministic outputs
* Author: Matthew Raymer
*
* Usage: node scripts/assets-config.js
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.dirname(__dirname);
/**
* Generate deterministic capacitor-assets configuration
* @returns {Object} Sorted, stable configuration object
*/
function generateAssetConfig() {
const config = {
icon: {
source: "resources/icon.png",
android: {
adaptive: {
foreground: "resources/icon.png",
background: "#121212",
monochrome: "resources/icon.png"
},
target: "android/app/src/main/res"
},
ios: {
padding: 0,
target: "ios/App/App/Assets.xcassets/AppIcon.appiconset"
},
web: {
target: "public/img/icons"
}
},
splash: {
source: "resources/splash.png",
darkSource: "resources/splash_dark.png",
android: {
scale: "cover",
target: "android/app/src/main/res"
},
ios: {
useStoryBoard: true,
target: "ios/App/App/Assets.xcassets"
}
}
};
return sortObjectKeys(config);
}
/**
* Sort object keys recursively for deterministic output
* @param {Object} obj - Object to sort
* @returns {Object} Object with sorted keys
*/
function sortObjectKeys(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(sortObjectKeys);
}
const sorted = {};
Object.keys(obj)
.sort()
.forEach(key => {
sorted[key] = sortObjectKeys(obj[key]);
});
return sorted;
}
/**
* Validate that required source files exist
*/
function validateSourceFiles() {
const requiredFiles = [
'resources/icon.png',
'resources/splash.png',
'resources/splash_dark.png'
];
const missingFiles = requiredFiles.filter(file => {
const filePath = path.join(PROJECT_ROOT, file);
return !fs.existsSync(filePath);
});
if (missingFiles.length > 0) {
console.error('❌ Missing required source files:');
missingFiles.forEach(file => console.error(` ${file}`));
process.exit(1);
}
console.log('✅ All required source files found');
}
/**
* Write configuration to file with consistent formatting
* @param {Object} config - Configuration object
* @param {string} outputPath - Output file path
*/
function writeConfig(config, outputPath) {
const jsonString = JSON.stringify(config, null, 2);
// Ensure consistent line endings and no trailing whitespace
const cleanJson = jsonString
.split('\n')
.map(line => line.trimEnd())
.join('\n') + '\n';
fs.writeFileSync(outputPath, cleanJson, 'utf8');
console.log(`✅ Configuration written to: ${outputPath}`);
}
/**
* Main execution function
*/
function main() {
console.log('🔄 Generating TimeSafari asset configuration...');
console.log(`📁 Project root: ${PROJECT_ROOT}`);
console.log(`📅 Generated: ${new Date().toISOString()}`);
try {
// Validate source files exist
validateSourceFiles();
// Generate configuration
const config = generateAssetConfig();
// Ensure config directory exists
const configDir = path.join(PROJECT_ROOT, 'config', 'assets');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// Write configuration files
const capacitorAssetsConfigPath = path.join(configDir, 'capacitor-assets.config.json');
writeConfig(config, capacitorAssetsConfigPath);
// Copy to root for capacitor-assets discovery
const rootConfigPath = path.join(PROJECT_ROOT, 'capacitor-assets.config.json');
writeConfig(config, rootConfigPath);
console.log('🎉 Asset configuration generation completed successfully!');
console.log('');
console.log('📋 Next steps:');
console.log(' 1. Review the generated configuration');
console.log(' 2. Commit the configuration files');
console.log(' 3. Run "npm run assets:validate" to verify');
console.log(' 4. Use "npm run build:native" for builds');
} catch (error) {
console.error('❌ Configuration generation failed:', error.message);
process.exit(1);
}
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export { generateAssetConfig, sortObjectKeys, validateSourceFiles };

218
scripts/assets-validator.js Normal file
View File

@@ -0,0 +1,218 @@
#!/usr/bin/env node
/**
* TimeSafari Asset Configuration Validator
* Validates capacitor-assets configuration against schema and source files
* Author: Matthew Raymer
*
* Usage: node scripts/assets-validator.js [config-path]
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.dirname(__dirname);
/**
* Load and parse JSON file
* @param {string} filePath - Path to JSON file
* @returns {Object} Parsed JSON object
*/
function loadJsonFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
throw new Error(`Failed to load ${filePath}: ${error.message}`);
}
}
/**
* Validate configuration against schema
* @param {Object} config - Configuration object to validate
* @param {Object} schema - JSON schema for validation
* @returns {Array} Array of validation errors
*/
function validateAgainstSchema(config, schema) {
const errors = [];
// Basic structure validation
if (!config.icon || !config.splash) {
errors.push('Configuration must contain both "icon" and "splash" sections');
}
// Icon validation
if (config.icon) {
if (!config.icon.source) {
errors.push('Icon section must contain "source" field');
} else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) {
errors.push('Icon source must be a PNG or SVG file in resources/ directory');
}
// Android adaptive icon validation
if (config.icon.android?.adaptive) {
const adaptive = config.icon.android.adaptive;
if (!adaptive.foreground || !adaptive.background) {
errors.push('Android adaptive icon must have both foreground and background');
}
if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) {
errors.push('Android adaptive foreground must be a PNG or SVG file in resources/ directory');
}
}
}
// Splash validation
if (config.splash) {
if (!config.splash.source) {
errors.push('Splash section must contain "source" field');
} else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) {
errors.push('Splash source must be a PNG or SVG file in resources/ directory');
}
if (config.splash.darkSource && !/^resources\/.*\.(png|svg)$/.test(config.splash.darkSource)) {
errors.push('Dark splash source must be a PNG or SVG file in resources/ directory');
}
}
return errors;
}
/**
* Validate that source files exist
* @param {Object} config - Configuration object
* @returns {Array} Array of missing file errors
*/
function validateSourceFiles(config) {
const errors = [];
const requiredFiles = new Set();
// Collect all required source files
if (config.icon?.source) requiredFiles.add(config.icon.source);
if (config.icon?.android?.adaptive?.foreground) requiredFiles.add(config.icon.android.adaptive.foreground);
if (config.icon?.android?.adaptive?.monochrome) requiredFiles.add(config.icon.android.adaptive.monochrome);
if (config.splash?.source) requiredFiles.add(config.splash.source);
if (config.splash?.darkSource) requiredFiles.add(config.splash.darkSource);
// Check each file exists
requiredFiles.forEach(file => {
const filePath = path.join(PROJECT_ROOT, file);
if (!fs.existsSync(filePath)) {
errors.push(`Source file not found: ${file}`);
}
});
return errors;
}
/**
* Validate target directories are writable
* @param {Object} config - Configuration object
* @returns {Array} Array of directory validation errors
*/
function validateTargetDirectories(config) {
const errors = [];
const targetDirs = new Set();
// Collect all target directories
if (config.icon?.android?.target) targetDirs.add(config.icon.android.target);
if (config.icon?.ios?.target) targetDirs.add(config.icon.ios.target);
if (config.icon?.web?.target) targetDirs.add(config.icon.web.target);
if (config.splash?.android?.target) targetDirs.add(config.splash.android.target);
if (config.splash?.ios?.target) targetDirs.add(config.splash.ios.target);
// Check each target directory
targetDirs.forEach(dir => {
const dirPath = path.join(PROJECT_ROOT, dir);
const parentDir = path.dirname(dirPath);
if (!fs.existsSync(parentDir)) {
errors.push(`Parent directory does not exist: ${parentDir}`);
} else if (!fs.statSync(parentDir).isDirectory()) {
errors.push(`Parent path is not a directory: ${parentDir}`);
}
});
return errors;
}
/**
* Main validation function
* @param {string} configPath - Path to configuration file
* @returns {boolean} True if validation passes
*/
function validateConfiguration(configPath) {
console.log('🔍 Validating TimeSafari asset configuration...');
console.log(`📁 Config file: ${configPath}`);
console.log(`📁 Project root: ${PROJECT_ROOT}`);
try {
// Load configuration
const config = loadJsonFile(configPath);
console.log('✅ Configuration file loaded successfully');
// Load schema
const schemaPath = path.join(PROJECT_ROOT, 'config', 'assets', 'schema.json');
const schema = loadJsonFile(schemaPath);
console.log('✅ Schema file loaded successfully');
// Perform validations
const schemaErrors = validateAgainstSchema(config, schema);
const fileErrors = validateSourceFiles(config);
const dirErrors = validateTargetDirectories(config);
// Report results
const allErrors = [...schemaErrors, ...fileErrors, ...dirErrors];
if (allErrors.length === 0) {
console.log('🎉 All validations passed successfully!');
console.log('');
console.log('📋 Configuration summary:');
console.log(` Icon source: ${config.icon?.source || 'NOT SET'}`);
console.log(` Splash source: ${config.splash?.source || 'NOT SET'}`);
console.log(` Dark splash: ${config.splash?.darkSource || 'NOT SET'}`);
console.log(` Android adaptive: ${config.icon?.android?.adaptive ? 'ENABLED' : 'DISABLED'}`);
console.log(` iOS LaunchScreen: ${config.splash?.ios?.useStoryBoard ? 'ENABLED' : 'DISABLED'}`);
return true;
} else {
console.error('❌ Validation failed with the following errors:');
allErrors.forEach((error, index) => {
console.error(` ${index + 1}. ${error}`);
});
return false;
}
} catch (error) {
console.error('❌ Validation failed:', error.message);
return false;
}
}
/**
* Main execution function
*/
function main() {
const configPath = process.argv[2] || path.join(PROJECT_ROOT, 'capacitor-assets.config.json');
if (!fs.existsSync(configPath)) {
console.error(`❌ Configuration file not found: ${configPath}`);
console.log('');
console.log('💡 Available options:');
console.log(' - Use default: capacitor-assets.config.json');
console.log(' - Specify path: node scripts/assets-validator.js path/to/config.json');
console.log(' - Generate config: npm run assets:config');
process.exit(1);
}
const success = validateConfiguration(configPath);
process.exit(success ? 0 : 1);
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export { validateConfiguration, validateAgainstSchema, validateSourceFiles, validateTargetDirectories };

View File

@@ -1,159 +0,0 @@
#!/bin/bash
# TimeSafari Android Resource Check Script
# Checks for missing Android resources and automatically fixes common issues
# Author: Matthew Raymer
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
ANDROID_RES_DIR="$PROJECT_ROOT/android/app/src/main/res"
ASSETS_DIR="$PROJECT_ROOT/assets"
echo "=== TimeSafari Android Resource Check ==="
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Checking Android resources"
# Function to check if a file exists
check_file() {
local file="$1"
local description="$2"
if [ -f "$file" ]; then
echo "[✓] $description: $file"
return 0
else
echo "[✗] $description: $file (MISSING)"
return 1
fi
}
# Function to check if a directory exists and has files
check_directory() {
local dir="$1"
local description="$2"
if [ -d "$dir" ] && [ "$(ls -A "$dir" 2>/dev/null)" ]; then
echo "[✓] $description: $dir"
return 0
else
echo "[✗] $description: $dir (MISSING OR EMPTY)"
return 1
fi
}
# Track issues
issues_found=0
fixes_applied=0
echo "[INFO] Checking splash screen resources..."
# Ensure drawable directory exists
if [ ! -d "$ANDROID_RES_DIR/drawable" ]; then
echo "[FIX] Creating drawable directory..."
mkdir -p "$ANDROID_RES_DIR/drawable"
fixes_applied=$((fixes_applied + 1))
fi
# Check splash screen resources
if ! check_file "$ANDROID_RES_DIR/drawable/splash.png" "Splash screen (light)"; then
if [ -f "$ASSETS_DIR/splash.png" ]; then
echo "[FIX] Copying splash.png to Android resources..."
cp "$ASSETS_DIR/splash.png" "$ANDROID_RES_DIR/drawable/splash.png"
fixes_applied=$((fixes_applied + 1))
else
issues_found=$((issues_found + 1))
fi
fi
if ! check_file "$ANDROID_RES_DIR/drawable/splash_dark.png" "Splash screen (dark)"; then
if [ -f "$ASSETS_DIR/splash_dark.png" ]; then
echo "[FIX] Copying splash_dark.png to Android resources..."
cp "$ASSETS_DIR/splash_dark.png" "$ANDROID_RES_DIR/drawable/splash_dark.png"
fixes_applied=$((fixes_applied + 1))
else
issues_found=$((issues_found + 1))
fi
fi
echo "[INFO] Checking launcher icon resources..."
# Ensure mipmap directories exist
mipmap_dirs=("mdpi" "hdpi" "xhdpi" "xxhdpi" "xxxhdpi" "anydpi-v26")
for dir in "${mipmap_dirs[@]}"; do
if [ ! -d "$ANDROID_RES_DIR/mipmap-$dir" ]; then
echo "[FIX] Creating mipmap-$dir directory..."
mkdir -p "$ANDROID_RES_DIR/mipmap-$dir"
fixes_applied=$((fixes_applied + 1))
fi
done
# Check launcher icon resources
required_icons=(
"mipmap-mdpi/ic_launcher.png"
"mipmap-hdpi/ic_launcher.png"
"mipmap-xhdpi/ic_launcher.png"
"mipmap-xxhdpi/ic_launcher.png"
"mipmap-xxxhdpi/ic_launcher.png"
"mipmap-anydpi-v26/ic_launcher.xml"
"mipmap-anydpi-v26/ic_launcher_round.xml"
)
missing_icons=0
for icon in "${required_icons[@]}"; do
if ! check_file "$ANDROID_RES_DIR/$icon" "Launcher icon: $icon"; then
missing_icons=$((missing_icons + 1))
fi
done
if [ $missing_icons -gt 0 ]; then
echo "[FIX] Missing launcher icons detected. Running icon generation script..."
if [ -f "$SCRIPT_DIR/generate-android-icons.sh" ]; then
"$SCRIPT_DIR/generate-android-icons.sh"
fixes_applied=$((fixes_applied + 1))
else
echo "[ERROR] Icon generation script not found: $SCRIPT_DIR/generate-android-icons.sh"
issues_found=$((issues_found + 1))
fi
fi
echo "[INFO] Checking Capacitor platform status..."
# Check if Android platform is properly initialized
if [ ! -d "$PROJECT_ROOT/android" ]; then
echo "[ERROR] Android platform directory not found"
issues_found=$((issues_found + 1))
elif [ ! -f "$PROJECT_ROOT/android/app/src/main/AndroidManifest.xml" ]; then
echo "[ERROR] AndroidManifest.xml not found - platform may be corrupted"
issues_found=$((issues_found + 1))
else
echo "[✓] Android platform appears to be properly initialized"
fi
# Check for common build issues
echo "[INFO] Checking for common build issues..."
# Check for invalid resource names (dashes in filenames)
invalid_resources=$(find "$ANDROID_RES_DIR" -name "*-*" -type f 2>/dev/null | grep -E '\.(png|jpg|jpeg|gif|xml)$' || true)
if [ -n "$invalid_resources" ]; then
echo "[WARNING] Found resources with invalid names (containing dashes):"
echo "$invalid_resources" | while read -r file; do
echo " - $file"
done
echo "[INFO] Android resource names must contain only lowercase a-z, 0-9, or underscore"
issues_found=$((issues_found + 1))
fi
# Summary
echo ""
echo "=== Resource Check Summary ==="
if [ $issues_found -eq 0 ] && [ $fixes_applied -eq 0 ]; then
echo "[SUCCESS] All Android resources are present and valid"
exit 0
elif [ $fixes_applied -gt 0 ]; then
echo "[SUCCESS] Fixed $fixes_applied resource issues automatically"
if [ $issues_found -gt 0 ]; then
echo "[WARNING] $issues_found issues remain that require manual attention"
exit 1
else
exit 0
fi
else
echo "[ERROR] Found $issues_found resource issues that require manual attention"
exit 1
fi

View File

@@ -1,294 +0,0 @@
#!/bin/bash
# TimeSafari iOS Resource Check Script
# Checks for missing iOS resources and automatically fixes common issues
# Author: Matthew Raymer
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
IOS_ASSETS_DIR="$PROJECT_ROOT/ios/App/App/Assets.xcassets"
RESOURCES_DIR="$PROJECT_ROOT/resources/ios"
echo "=== TimeSafari iOS Resource Check ==="
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Checking iOS resources"
# Function to check if a file exists
check_file() {
local file="$1"
local description="$2"
if [ -f "$file" ]; then
echo "[✓] $description: $file"
return 0
else
echo "[✗] $description: $file (MISSING)"
return 1
fi
}
# Function to check if a directory exists and has files
check_directory() {
local dir="$1"
local description="$2"
if [ -d "$dir" ] && [ "$(ls -A "$dir" 2>/dev/null)" ]; then
echo "[✓] $description: $dir"
return 0
else
echo "[✗] $description: $dir (MISSING OR EMPTY)"
return 1
fi
}
# Track issues
issues_found=0
fixes_applied=0
echo "[INFO] Checking iOS asset catalog structure..."
# Check if Assets.xcassets directory exists
if ! check_directory "$IOS_ASSETS_DIR" "iOS Assets.xcassets directory"; then
echo "[FIX] Creating iOS Assets.xcassets directory..."
mkdir -p "$IOS_ASSETS_DIR"
fixes_applied=$((fixes_applied + 1))
fi
# Check main Contents.json
if ! check_file "$IOS_ASSETS_DIR/Contents.json" "Main Assets.xcassets Contents.json"; then
echo "[FIX] Creating main Assets.xcassets Contents.json..."
cat > "$IOS_ASSETS_DIR/Contents.json" << 'EOF'
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
EOF
fixes_applied=$((fixes_applied + 1))
fi
echo "[INFO] Checking App Icon resources..."
# Check App Icon directory
if ! check_directory "$IOS_ASSETS_DIR/AppIcon.appiconset" "App Icon directory"; then
echo "[FIX] Creating App Icon directory..."
mkdir -p "$IOS_ASSETS_DIR/AppIcon.appiconset"
fixes_applied=$((fixes_applied + 1))
fi
# Check App Icon Contents.json
if ! check_file "$IOS_ASSETS_DIR/AppIcon.appiconset/Contents.json" "App Icon Contents.json"; then
echo "[FIX] Creating App Icon Contents.json..."
cat > "$IOS_ASSETS_DIR/AppIcon.appiconset/Contents.json" << 'EOF'
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
EOF
fixes_applied=$((fixes_applied + 1))
fi
echo "[INFO] Checking Splash Screen resources..."
# Check Splash directory
if ! check_directory "$IOS_ASSETS_DIR/Splash.imageset" "Splash screen directory"; then
echo "[FIX] Creating Splash screen directory..."
mkdir -p "$IOS_ASSETS_DIR/Splash.imageset"
fixes_applied=$((fixes_applied + 1))
fi
# Check Splash Contents.json
if ! check_file "$IOS_ASSETS_DIR/Splash.imageset/Contents.json" "Splash screen Contents.json"; then
echo "[FIX] Creating Splash screen Contents.json..."
cat > "$IOS_ASSETS_DIR/Splash.imageset/Contents.json" << 'EOF'
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
EOF
fixes_applied=$((fixes_applied + 1))
fi
# Check SplashDark directory
if ! check_directory "$IOS_ASSETS_DIR/SplashDark.imageset" "Dark splash screen directory"; then
echo "[FIX] Creating Dark splash screen directory..."
mkdir -p "$IOS_ASSETS_DIR/SplashDark.imageset"
fixes_applied=$((fixes_applied + 1))
fi
# Check SplashDark Contents.json
if ! check_file "$IOS_ASSETS_DIR/SplashDark.imageset/Contents.json" "Dark splash screen Contents.json"; then
echo "[FIX] Creating Dark splash screen Contents.json..."
cat > "$IOS_ASSETS_DIR/SplashDark.imageset/Contents.json" << 'EOF'
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
EOF
fixes_applied=$((fixes_applied + 1))
fi
echo "[INFO] Checking source resource files..."
# Check if source resources exist
if ! check_file "$RESOURCES_DIR/icon/icon.png" "iOS icon source"; then
issues_found=$((issues_found + 1))
fi
if ! check_file "$RESOURCES_DIR/splash/splash.png" "iOS splash source"; then
issues_found=$((issues_found + 1))
fi
if ! check_file "$RESOURCES_DIR/splash/splash_dark.png" "iOS dark splash source"; then
issues_found=$((issues_found + 1))
fi
echo "[INFO] Checking iOS platform status..."
# Check if iOS platform is properly initialized
if [ ! -d "$PROJECT_ROOT/ios" ]; then
echo "[ERROR] iOS platform directory not found"
issues_found=$((issues_found + 1))
elif [ ! -f "$PROJECT_ROOT/ios/App/App/Info.plist" ]; then
echo "[ERROR] Info.plist not found - platform may be corrupted"
issues_found=$((issues_found + 1))
else
echo "[✓] iOS platform appears to be properly initialized"
fi
# Summary
echo ""
echo "=== iOS Resource Check Summary ==="
if [ $issues_found -eq 0 ] && [ $fixes_applied -eq 0 ]; then
echo "[SUCCESS] All iOS resources are present and valid"
exit 0
elif [ $fixes_applied -gt 0 ]; then
echo "[SUCCESS] Fixed $fixes_applied resource issues automatically"
if [ $issues_found -gt 0 ]; then
echo "[WARNING] $issues_found issues remain that require manual attention"
echo "[NOTE] iOS builds require macOS with Xcode - cannot build on Linux"
exit 1
else
exit 0
fi
else
echo "[ERROR] Found $issues_found resource issues that require manual attention"
echo "[NOTE] iOS builds require macOS with Xcode - cannot build on Linux"
exit 1
fi

View File

@@ -1,107 +0,0 @@
#!/bin/bash
# TimeSafari Android Icon Generation Script
# Generates all required Android launcher icon sizes from assets/icon.png
# Author: Matthew Raymer
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
ASSETS_DIR="$PROJECT_ROOT/assets"
ANDROID_RES_DIR="$PROJECT_ROOT/android/app/src/main/res"
echo "=== TimeSafari Android Icon Generation ==="
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting Android icon generation"
# Check if source icon exists
if [ ! -f "$ASSETS_DIR/icon.png" ]; then
echo "[ERROR] Source icon not found: $ASSETS_DIR/icon.png"
exit 1
fi
# Check if ImageMagick is available and determine the correct command
IMAGEMAGICK_CMD=""
if command -v magick &> /dev/null; then
IMAGEMAGICK_CMD="magick"
echo "[INFO] Using ImageMagick v7+ (magick command)"
elif command -v convert &> /dev/null; then
IMAGEMAGICK_CMD="convert"
echo "[INFO] Using ImageMagick v6 (convert command)"
else
echo "[ERROR] ImageMagick not found. Please install ImageMagick."
echo " Arch: sudo pacman -S imagemagick"
echo " Ubuntu: sudo apt-get install imagemagick"
echo " macOS: brew install imagemagick"
exit 1
fi
# Create mipmap directories if they don't exist
mkdir -p "$ANDROID_RES_DIR/mipmap-hdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-mdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-xhdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-xxhdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-xxxhdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-anydpi-v26"
echo "[INFO] Generating launcher icons..."
# Function to resize image using the appropriate ImageMagick command
resize_image() {
local input="$1"
local output="$2"
local size="$3"
if [ "$IMAGEMAGICK_CMD" = "magick" ]; then
# ImageMagick v7+ syntax
magick "$input" -resize "${size}x${size}" "$output"
else
# ImageMagick v6 syntax
convert "$input" -resize "${size}x${size}" "$output"
fi
}
# Generate launcher icons for different densities
# Android launcher icon sizes: mdpi=48, hdpi=72, xhdpi=96, xxhdpi=144, xxxhdpi=192
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png" 48
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png" 72
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png" 96
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png" 144
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png" 192
# Generate round launcher icons
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher_round.png" 48
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher_round.png" 72
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher_round.png" 96
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher_round.png" 144
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher_round.png" 192
# Create simple launcher XML files (no adaptive icons for now)
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher.xml" << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
EOF
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_round.xml" << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
EOF
# Create foreground mipmap files for adaptive icons
resize_image "$ASSETS_DIR/icon.png" "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_foreground.png" 108
echo "[SUCCESS] Generated Android launcher icons:"
echo " - mipmap-mdpi/ic_launcher.png (48x48)"
echo " - mipmap-hdpi/ic_launcher.png (72x72)"
echo " - mipmap-xhdpi/ic_launcher.png (96x96)"
echo " - mipmap-xxhdpi/ic_launcher.png (144x144)"
echo " - mipmap-xxxhdpi/ic_launcher.png (192x192)"
echo " - mipmap-anydpi-v26/ic_launcher_foreground.png (108x108)"
echo " - Updated mipmap-anydpi-v26 XML files"
echo "[SUCCESS] Android icon generation completed successfully!"

View File

@@ -1,79 +0,0 @@
#!/bin/bash
# TimeSafari Android Icon Generation Script (Placeholder Icons)
# Generates placeholder Android launcher icons with "TS" text
# Author: Matthew Raymer
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
ANDROID_RES_DIR="$PROJECT_ROOT/android/app/src/main/res"
echo "=== TimeSafari Android Placeholder Icon Generation ==="
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting Android placeholder icon generation"
# Check if ImageMagick is available and determine the correct command
IMAGEMAGICK_CMD=""
if command -v magick &> /dev/null; then
IMAGEMAGICK_CMD="magick"
echo "[INFO] Using ImageMagick v7+ (magick command)"
elif command -v convert &> /dev/null; then
IMAGEMAGICK_CMD="convert"
echo "[INFO] Using ImageMagick v6 (convert command)"
else
echo "[ERROR] ImageMagick not found. Please install ImageMagick."
echo " Arch: sudo pacman -S imagemagick"
echo " Ubuntu: sudo apt-get install imagemagick"
echo " macOS: brew install imagemagick"
exit 1
fi
# Create directories if they don't exist
mkdir -p "$ANDROID_RES_DIR/mipmap-mdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-hdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-xhdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-xxhdpi"
mkdir -p "$ANDROID_RES_DIR/mipmap-xxxhdpi"
# Function to generate placeholder icon using the appropriate ImageMagick command
generate_placeholder_icon() {
local size="$1"
local output="$2"
local pointsize="$3"
if [ "$IMAGEMAGICK_CMD" = "magick" ]; then
# ImageMagick v7+ syntax
magick -size "${size}x${size}" xc:blue -gravity center -pointsize "$pointsize" -fill white -annotate 0 "TS" "$output"
else
# ImageMagick v6 syntax
convert -size "${size}x${size}" xc:blue -gravity center -pointsize "$pointsize" -fill white -annotate 0 "TS" "$output"
fi
}
echo "[INFO] Generating placeholder launcher icons..."
# Generate placeholder icons using ImageMagick
generate_placeholder_icon 48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png" 20
generate_placeholder_icon 72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png" 30
generate_placeholder_icon 96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png" 40
generate_placeholder_icon 144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png" 60
generate_placeholder_icon 192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png" 80
echo "[INFO] Copying to round versions..."
# Copy to round versions
cp "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher_round.png"
cp "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher_round.png"
cp "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher_round.png"
cp "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher_round.png"
cp "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png" "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher_round.png"
echo "[SUCCESS] Generated Android placeholder launcher icons:"
echo " - mipmap-mdpi/ic_launcher.png (48x48)"
echo " - mipmap-hdpi/ic_launcher.png (72x72)"
echo " - mipmap-xhdpi/ic_launcher.png (96x96)"
echo " - mipmap-xxhdpi/ic_launcher.png (144x144)"
echo " - mipmap-xxxhdpi/ic_launcher.png (192x192)"
echo " - All round versions created"
echo "[SUCCESS] Android placeholder icon generation completed successfully!"

View File

@@ -1,253 +0,0 @@
#!/bin/bash
# TimeSafari iOS Asset Generation Script
# Manually generates iOS assets using ImageMagick when capacitor-assets fails
# Author: Matthew Raymer
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
IOS_ASSETS_DIR="$PROJECT_ROOT/ios/App/App/Assets.xcassets"
RESOURCES_DIR="$PROJECT_ROOT/resources/ios"
echo "=== TimeSafari iOS Asset Generation ==="
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Generating iOS assets manually"
# Check if ImageMagick is available
if ! command -v convert &> /dev/null; then
echo "[ERROR] ImageMagick 'convert' command not found. Please install ImageMagick."
exit 1
fi
# Check if source files exist
if [ ! -f "$RESOURCES_DIR/icon/icon.png" ]; then
echo "[ERROR] Source icon not found: $RESOURCES_DIR/icon/icon.png"
exit 1
fi
if [ ! -f "$RESOURCES_DIR/splash/splash.png" ]; then
echo "[ERROR] Source splash not found: $RESOURCES_DIR/splash/splash.png"
exit 1
fi
if [ ! -f "$RESOURCES_DIR/splash/splash_dark.png" ]; then
echo "[ERROR] Source dark splash not found: $RESOURCES_DIR/splash/splash_dark.png"
exit 1
fi
echo "[INFO] Generating iOS app icons..."
# Generate app icons for different sizes
# iPhone icons
convert "$RESOURCES_DIR/icon/icon.png" -resize 40x40 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-20@2x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 60x60 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-20@3x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 58x58 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-29@2x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 87x87 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-29@3x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 80x80 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-40@2x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 120x120 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-40@3x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 120x120 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-60@2x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 180x180 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-60@3x.png"
# iPad icons
convert "$RESOURCES_DIR/icon/icon.png" -resize 20x20 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-20@1x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 40x40 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-20@2x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 29x29 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-29@1x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 58x58 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-29@2x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 40x40 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-40@1x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 80x80 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-40@2x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 152x152 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-76@2x.png"
convert "$RESOURCES_DIR/icon/icon.png" -resize 167x167 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-83.5@2x.png"
# App Store icon
convert "$RESOURCES_DIR/icon/icon.png" -resize 1024x1024 "$IOS_ASSETS_DIR/AppIcon.appiconset/AppIcon-1024@1x.png"
echo "[INFO] Generating iOS splash screens..."
# Generate splash screens for different scales
convert "$RESOURCES_DIR/splash/splash.png" -resize 320x480 "$IOS_ASSETS_DIR/Splash.imageset/splash@1x.png"
convert "$RESOURCES_DIR/splash/splash.png" -resize 640x960 "$IOS_ASSETS_DIR/Splash.imageset/splash@2x.png"
convert "$RESOURCES_DIR/splash/splash.png" -resize 960x1440 "$IOS_ASSETS_DIR/Splash.imageset/splash@3x.png"
# Generate dark splash screens
convert "$RESOURCES_DIR/splash/splash_dark.png" -resize 320x480 "$IOS_ASSETS_DIR/SplashDark.imageset/splash@1x.png"
convert "$RESOURCES_DIR/splash/splash_dark.png" -resize 640x960 "$IOS_ASSETS_DIR/SplashDark.imageset/splash@2x.png"
convert "$RESOURCES_DIR/splash/splash_dark.png" -resize 960x1440 "$IOS_ASSETS_DIR/SplashDark.imageset/splash@3x.png"
echo "[INFO] Updating Contents.json files to reference generated images..."
# Update AppIcon Contents.json to reference the generated files
cat > "$IOS_ASSETS_DIR/AppIcon.appiconset/Contents.json" << 'EOF'
{
"images" : [
{
"filename" : "AppIcon-20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "AppIcon-20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "AppIcon-29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "AppIcon-29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "AppIcon-40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "AppIcon-40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "AppIcon-60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "AppIcon-60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "AppIcon-20@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "AppIcon-20@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "AppIcon-29@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "AppIcon-29@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "AppIcon-40@1x.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "AppIcon-40@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "AppIcon-76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "AppIcon-83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "AppIcon-1024@1x.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
EOF
# Update Splash Contents.json to reference the generated files
cat > "$IOS_ASSETS_DIR/Splash.imageset/Contents.json" << 'EOF'
{
"images" : [
{
"filename" : "splash@1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "splash@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "splash@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
EOF
# Update SplashDark Contents.json to reference the generated files
cat > "$IOS_ASSETS_DIR/SplashDark.imageset/Contents.json" << 'EOF'
{
"images" : [
{
"filename" : "splash@1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "splash@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "splash@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
EOF
echo "[SUCCESS] iOS assets generated successfully!"
echo "[INFO] Generated files:"
find "$IOS_ASSETS_DIR" -name "*.png" | sort
echo ""
echo "[NOTE] iOS builds require macOS with Xcode - cannot build on Linux"
echo "[INFO] Assets are now ready for when you build on macOS"

View File

@@ -1,67 +0,0 @@
#!/bin/bash
# TimeSafari Generated Assets Purge Script
# Removes generated Android assets and resources from git history
# Author: Matthew Raymer
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
echo "=== TimeSafari Generated Assets Purge ==="
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] Starting git history cleanup"
# Check if we're in a git repository
if [ ! -d ".git" ]; then
echo "[ERROR] Not in a git repository. Please run this script from the project root."
exit 1
fi
# Check if we have uncommitted changes
if [ -n "$(git status --porcelain)" ]; then
echo "[ERROR] You have uncommitted changes. Please commit or stash them first."
echo "Current status:"
git status --short
exit 1
fi
# Create backup branch
BACKUP_BRANCH="backup-before-asset-purge-$(date +%Y%m%d-%H%M%S)"
echo "[INFO] Creating backup branch: $BACKUP_BRANCH"
git branch "$BACKUP_BRANCH"
echo "[INFO] Starting git filter-branch to remove generated assets..."
# Use git filter-branch to remove the directories from history
git filter-branch --force --index-filter '
# Remove generated Android assets directory
git rm -rf --cached --ignore-unmatch android/app/src/main/assets/public/ 2>/dev/null || true
# Remove generated Android resources (but keep config files)
git rm -rf --cached --ignore-unmatch android/app/src/main/res/drawable*/ 2>/dev/null || true
git rm -rf --cached --ignore-unmatch android/app/src/main/res/mipmap*/ 2>/dev/null || true
git rm -rf --cached --ignore-unmatch android/app/src/main/res/values/ic_launcher_background.xml 2>/dev/null || true
# Keep configuration files
git add android/app/src/main/res/values/strings.xml 2>/dev/null || true
git add android/app/src/main/res/values/styles.xml 2>/dev/null || true
git add android/app/src/main/res/layout/activity_main.xml 2>/dev/null || true
git add android/app/src/main/res/xml/config.xml 2>/dev/null || true
git add android/app/src/main/res/xml/file_paths.xml 2>/dev/null || true
' --prune-empty --tag-name-filter cat -- --all
echo "[INFO] Cleaning up git filter-branch temporary files..."
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now --aggressive
echo "[SUCCESS] Generated assets purged from git history!"
echo "[INFO] Backup branch created: $BACKUP_BRANCH"
echo "[INFO] Repository size should be significantly reduced"
echo ""
echo "Next steps:"
echo "1. Test that the repository works correctly"
echo "2. Force push to remote: git push --force-with-lease origin <branch>"
echo "3. Inform team members to re-clone or reset their local repositories"
echo "4. Delete backup branch when confident: git branch -D $BACKUP_BRANCH"