forked from trent_larson/crowd-funder-for-time-pwa
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:
32
.cursor/rules/asset_configuration.mdc
Normal file
32
.cursor/rules/asset_configuration.mdc
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# Asset Configuration Directive
|
||||
*Scope: Assets Only (icons, splashes, image pipelines) — not overall build orchestration*
|
||||
|
||||
## Intent
|
||||
- Version **asset configuration files** (optionally dev-time generated).
|
||||
- **Do not** version platform asset outputs (Android/iOS/Electron); generate them **at build-time** with standard tools.
|
||||
- Keep existing per-platform build scripts unchanged.
|
||||
|
||||
## Source of Truth
|
||||
- **Preferred (Capacitor default):** `resources/` as the single master source.
|
||||
- **Alternative:** `assets/` is acceptable **only** if `capacitor-assets` is explicitly configured to read from it.
|
||||
- **Never** maintain both `resources/` and `assets/` as parallel sources. Migrate and delete the redundant folder.
|
||||
|
||||
## Config Files
|
||||
- Live under: `config/assets/` (committed).
|
||||
- Examples:
|
||||
- `config/assets/capacitor-assets.config.json` (or the path the tool expects)
|
||||
- `config/assets/android.assets.json`
|
||||
- `config/assets/ios.assets.json`
|
||||
- `config/assets/common.assets.yaml` (optional shared layer)
|
||||
- **Dev-time generation allowed** for these configs; **build-time generation is forbidden**.
|
||||
|
||||
## Build-Time Behavior
|
||||
- Build generates platform assets (not configs) using the standard chain:
|
||||
```bash
|
||||
npm run build:capacitor # web build via Vite (.mts)
|
||||
npx cap sync
|
||||
npx capacitor-assets generate # produces platform assets; not committed
|
||||
# then platform-specific build steps
|
||||
142
.github/workflows/asset-validation.yml
vendored
Normal file
142
.github/workflows/asset-validation.yml
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
name: Asset Validation & CI Safeguards
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'resources/**'
|
||||
- 'config/assets/**'
|
||||
- 'capacitor-assets.config.json'
|
||||
- 'capacitor.config.ts'
|
||||
- 'capacitor.config.json'
|
||||
push:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'resources/**'
|
||||
- 'config/assets/**'
|
||||
- 'capacitor-assets.config.json'
|
||||
- 'capacitor.config.ts'
|
||||
- 'capacitor.config.json'
|
||||
|
||||
jobs:
|
||||
asset-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Validate asset configuration
|
||||
run: npm run assets:validate
|
||||
|
||||
- name: Check for committed platform assets (Android)
|
||||
run: |
|
||||
if git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)' > /dev/null; then
|
||||
echo "❌ Android platform assets found in VCS - these should be generated at build-time"
|
||||
git ls-files -z android/app/src/main/res | grep -E '(AppIcon.*\.png|Splash.*\.png|mipmap-.*/ic_launcher.*\.png)'
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ No Android platform assets committed"
|
||||
|
||||
- name: Check for committed platform assets (iOS)
|
||||
run: |
|
||||
if git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)' > /dev/null; then
|
||||
echo "❌ iOS platform assets found in VCS - these should be generated at build-time"
|
||||
git ls-files -z ios/App/App/Assets.xcassets | grep -E '(AppIcon.*\.png|Splash.*\.png)'
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ No iOS platform assets committed"
|
||||
|
||||
- name: Test asset generation
|
||||
run: |
|
||||
echo "🧪 Testing asset generation workflow..."
|
||||
npm run build:capacitor
|
||||
npx cap sync
|
||||
npx capacitor-assets generate --dry-run || npx capacitor-assets generate
|
||||
echo "✅ Asset generation test completed"
|
||||
|
||||
- name: Verify clean tree after build
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "❌ Dirty tree after build - asset configs were modified"
|
||||
git status
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Build completed with clean tree"
|
||||
|
||||
schema-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Validate schema compliance
|
||||
run: |
|
||||
echo "🔍 Validating schema compliance..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8'));
|
||||
const schema = JSON.parse(fs.readFileSync('config/assets/schema.json', 'utf8'));
|
||||
|
||||
// Basic schema validation
|
||||
if (!config.icon || !config.splash) {
|
||||
throw new Error('Missing required sections: icon and splash');
|
||||
}
|
||||
|
||||
if (!config.icon.source || !config.splash.source) {
|
||||
throw new Error('Missing required source fields');
|
||||
}
|
||||
|
||||
if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) {
|
||||
throw new Error('Icon source must be in resources/ directory');
|
||||
}
|
||||
|
||||
if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) {
|
||||
throw new Error('Splash source must be in resources/ directory');
|
||||
}
|
||||
|
||||
console.log('✅ Schema validation passed');
|
||||
"
|
||||
|
||||
- name: Check source file existence
|
||||
run: |
|
||||
echo "📁 Checking source file existence..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const config = JSON.parse(fs.readFileSync('capacitor-assets.config.json', 'utf8'));
|
||||
|
||||
const requiredFiles = [
|
||||
config.icon.source,
|
||||
config.splash.source
|
||||
];
|
||||
|
||||
if (config.splash.darkSource) {
|
||||
requiredFiles.push(config.splash.darkSource);
|
||||
}
|
||||
|
||||
const missingFiles = requiredFiles.filter(file => !fs.existsSync(file));
|
||||
|
||||
if (missingFiles.length > 0) {
|
||||
console.error('❌ Missing source files:', missingFiles);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ All source files exist');
|
||||
"
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -56,6 +56,10 @@ icons
|
||||
|
||||
*.log
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Generated Android assets and resources (should be generated during build)
|
||||
android/app/src/main/assets/public/
|
||||
|
||||
@@ -64,6 +68,14 @@ android/app/src/main/res/drawable*/
|
||||
android/app/src/main/res/mipmap*/
|
||||
android/app/src/main/res/values/ic_launcher_background.xml
|
||||
|
||||
# Android generated assets (deny-listed in CI)
|
||||
android/app/src/main/res/mipmap-*/ic_launcher*.png
|
||||
android/app/src/main/res/drawable*/splash*.png
|
||||
|
||||
# iOS generated assets (deny-listed in CI)
|
||||
ios/App/App/Assets.xcassets/**/AppIcon*.png
|
||||
ios/App/App/Assets.xcassets/**/Splash*.png
|
||||
|
||||
# Keep these Android configuration files in version control:
|
||||
# - android/app/src/main/assets/capacitor.plugins.json
|
||||
# - android/app/src/main/res/values/strings.xml
|
||||
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
18.19.0
|
||||
45
README.md
45
README.md
@@ -136,11 +136,50 @@ const apiUrl = `${APP_SERVER}/api/claim/123`;
|
||||
|
||||
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
|
||||
|
||||
## Icons
|
||||
## Asset Management
|
||||
|
||||
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
|
||||
TimeSafari uses a standardized asset configuration system for consistent
|
||||
icon and splash screen generation across all platforms.
|
||||
|
||||
To add a Font Awesome icon, add to fontawesome.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
|
||||
### Asset Sources
|
||||
|
||||
- **Single source of truth**: `resources/` directory (Capacitor default)
|
||||
- **Source files**: `icon.png`, `splash.png`, `splash_dark.png`
|
||||
- **Format**: PNG or SVG files for optimal quality
|
||||
|
||||
### Asset Generation
|
||||
|
||||
- **Configuration**: `config/assets/capacitor-assets.config.json`
|
||||
- **Schema validation**: `config/assets/schema.json`
|
||||
- **Build-time generation**: Platform assets generated via `capacitor-assets`
|
||||
- **No VCS commits**: Generated assets are never committed to version control
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
# Generate/update asset configurations
|
||||
npm run assets:config
|
||||
|
||||
# Validate asset configurations
|
||||
npm run assets:validate
|
||||
|
||||
# Clean generated platform assets (local dev only)
|
||||
npm run assets:clean
|
||||
|
||||
# Build with asset generation
|
||||
npm run build:native
|
||||
```
|
||||
|
||||
### Platform Support
|
||||
|
||||
- **Android**: Adaptive icons with foreground/background, monochrome support
|
||||
- **iOS**: LaunchScreen storyboard preferred, splash assets when needed
|
||||
- **Web**: PWA icons generated during build to `dist/` (not committed)
|
||||
|
||||
### Font Awesome Icons
|
||||
|
||||
To add a Font Awesome icon, add to `fontawesome.ts` and reference with
|
||||
`font-awesome` element and `icon` attribute with the hyphenated name.
|
||||
|
||||
## Other
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"splashFullScreen": true,
|
||||
"splashImmersive": true
|
||||
},
|
||||
"CapacitorSQLite": {
|
||||
"CapSQLite": {
|
||||
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||
"iosIsEncryption": false,
|
||||
"iosBiometric": {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
Application icons are here. They are processed for android & ios by the `capacitor-assets` command, as indicated in the BUILDING.md file.
|
||||
@@ -1,36 +1,32 @@
|
||||
{
|
||||
"icon": {
|
||||
"ios": {
|
||||
"source": "resources/ios/icon/icon.png",
|
||||
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset"
|
||||
},
|
||||
"android": {
|
||||
"source": "resources/android/icon/icon.png",
|
||||
"adaptive": {
|
||||
"background": "#121212",
|
||||
"foreground": "resources/icon.png",
|
||||
"monochrome": "resources/icon.png"
|
||||
},
|
||||
"target": "android/app/src/main/res"
|
||||
},
|
||||
"ios": {
|
||||
"padding": 0,
|
||||
"target": "ios/App/App/Assets.xcassets/AppIcon.appiconset"
|
||||
},
|
||||
"source": "resources/icon.png",
|
||||
"web": {
|
||||
"source": "resources/web/icon/icon.png",
|
||||
"target": "public/img/icons"
|
||||
}
|
||||
},
|
||||
"splash": {
|
||||
"ios": {
|
||||
"source": "resources/ios/splash/splash.png",
|
||||
"target": "ios/App/App/Assets.xcassets/Splash.imageset"
|
||||
},
|
||||
"android": {
|
||||
"source": "resources/android/splash/splash.png",
|
||||
"scale": "cover",
|
||||
"target": "android/app/src/main/res"
|
||||
}
|
||||
},
|
||||
"splashDark": {
|
||||
"ios": {
|
||||
"source": "resources/ios/splash/splash_dark.png",
|
||||
"target": "ios/App/App/Assets.xcassets/SplashDark.imageset"
|
||||
},
|
||||
"android": {
|
||||
"source": "resources/android/splash/splash_dark.png",
|
||||
"target": "android/app/src/main/res"
|
||||
}
|
||||
"darkSource": "resources/splash_dark.png",
|
||||
"ios": {
|
||||
"target": "ios/App/App/Assets.xcassets",
|
||||
"useStoryBoard": true
|
||||
},
|
||||
"source": "resources/splash.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
116
capacitor.config.ts
Normal file
116
capacitor.config.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'app.timesafari',
|
||||
appName: 'TimeSafari',
|
||||
webDir: 'dist',
|
||||
server: {
|
||||
cleartext: true
|
||||
},
|
||||
plugins: {
|
||||
App: {
|
||||
appUrlOpen: {
|
||||
handlers: [
|
||||
{
|
||||
url: 'timesafari://*',
|
||||
autoVerify: true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
SplashScreen: {
|
||||
launchShowDuration: 3000,
|
||||
launchAutoHide: true,
|
||||
backgroundColor: '#ffffff',
|
||||
androidSplashResourceName: 'splash',
|
||||
androidScaleType: 'CENTER_CROP',
|
||||
showSpinner: false,
|
||||
androidSpinnerStyle: 'large',
|
||||
iosSpinnerStyle: 'small',
|
||||
spinnerColor: '#999999',
|
||||
splashFullScreen: true,
|
||||
splashImmersive: true
|
||||
},
|
||||
CapSQLite: {
|
||||
iosDatabaseLocation: 'Library/CapacitorDatabase',
|
||||
iosIsEncryption: false,
|
||||
iosBiometric: {
|
||||
biometricAuth: false,
|
||||
biometricTitle: 'Biometric login for TimeSafari'
|
||||
},
|
||||
androidIsEncryption: false,
|
||||
androidBiometric: {
|
||||
biometricAuth: false,
|
||||
biometricTitle: 'Biometric login for TimeSafari'
|
||||
},
|
||||
electronIsEncryption: false
|
||||
}
|
||||
},
|
||||
ios: {
|
||||
contentInset: 'never',
|
||||
allowsLinkPreview: true,
|
||||
scrollEnabled: true,
|
||||
limitsNavigationsToAppBoundDomains: true,
|
||||
backgroundColor: '#ffffff',
|
||||
allowNavigation: [
|
||||
'*.timesafari.app',
|
||||
'*.jsdelivr.net',
|
||||
'api.endorser.ch'
|
||||
]
|
||||
},
|
||||
android: {
|
||||
allowMixedContent: true,
|
||||
captureInput: true,
|
||||
webContentsDebuggingEnabled: false,
|
||||
allowNavigation: [
|
||||
'*.timesafari.app',
|
||||
'*.jsdelivr.net',
|
||||
'api.endorser.ch',
|
||||
'10.0.2.2:3000'
|
||||
]
|
||||
},
|
||||
electron: {
|
||||
deepLinking: {
|
||||
schemes: ['timesafari']
|
||||
},
|
||||
buildOptions: {
|
||||
appId: 'app.timesafari',
|
||||
productName: 'TimeSafari',
|
||||
directories: {
|
||||
output: 'dist-electron-packages'
|
||||
},
|
||||
files: [
|
||||
'dist/**/*',
|
||||
'electron/**/*'
|
||||
],
|
||||
mac: {
|
||||
category: 'public.app-category.productivity',
|
||||
target: [
|
||||
{
|
||||
target: 'dmg',
|
||||
arch: ['x64', 'arm64']
|
||||
}
|
||||
]
|
||||
},
|
||||
win: {
|
||||
target: [
|
||||
{
|
||||
target: 'nsis',
|
||||
arch: ['x64']
|
||||
}
|
||||
]
|
||||
},
|
||||
linux: {
|
||||
target: [
|
||||
{
|
||||
target: 'AppImage',
|
||||
arch: ['x64']
|
||||
}
|
||||
],
|
||||
category: 'Utility'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
32
config/assets/capacitor-assets.config.json
Normal file
32
config/assets/capacitor-assets.config.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
119
config/assets/schema.json
Normal file
119
config/assets/schema.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Capacitor Assets Configuration Schema",
|
||||
"description": "Schema for validating capacitor-assets configuration files",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Source icon file path relative to project root"
|
||||
},
|
||||
"android": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"adaptive": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foreground": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Foreground icon for Android adaptive icons"
|
||||
},
|
||||
"background": {
|
||||
"type": ["string", "object"],
|
||||
"description": "Background color or image for adaptive icons"
|
||||
},
|
||||
"monochrome": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Monochrome icon for Android 13+"
|
||||
}
|
||||
},
|
||||
"required": ["foreground", "background"]
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Android target directory for generated icons"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"padding": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Padding ratio for iOS icons"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "iOS target directory for generated icons"
|
||||
}
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Web target directory for generated icons"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["source"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"splash": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Source splash screen file"
|
||||
},
|
||||
"darkSource": {
|
||||
"type": "string",
|
||||
"pattern": "^resources/.*\\.(png|svg)$",
|
||||
"description": "Dark mode splash screen file"
|
||||
},
|
||||
"android": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scale": {
|
||||
"type": "string",
|
||||
"enum": ["cover", "contain", "fill"],
|
||||
"description": "Android splash screen scaling mode"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Android target directory for splash screens"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"useStoryBoard": {
|
||||
"type": "boolean",
|
||||
"description": "Use LaunchScreen storyboard instead of splash assets"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "iOS target directory for splash screens"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["source"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["icon", "splash"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
214
doc/asset-migration-plan.md
Normal file
214
doc/asset-migration-plan.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# TimeSafari Asset Configuration Migration Plan
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-08-14
|
||||
**Status**: 🎯 **IMPLEMENTATION** - Ready for Execution
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the migration from the current mixed asset management
|
||||
system to a standardized, single-source asset configuration approach using
|
||||
`capacitor-assets` as the standard generator.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Asset Sources (Duplicated)
|
||||
|
||||
- **`assets/` directory**: Contains `icon.png`, `splash.png`, `splash_dark.png`
|
||||
- **`resources/` directory**: Contains identical files in platform-specific subdirectories
|
||||
- **Result**: Duplicate storage, confusion about source of truth
|
||||
|
||||
### Asset Generation (Manual)
|
||||
|
||||
- **Custom scripts**: `generate-icons.sh`, `generate-ios-assets.sh`, `generate-android-icons.sh`
|
||||
- **Bypass capacitor-assets**: Manual ImageMagick-based generation
|
||||
- **Inconsistent outputs**: Different generation methods for each platform
|
||||
|
||||
### Configuration (Scattered)
|
||||
|
||||
- **`capacitor-assets.config.json`**: Basic configuration at root
|
||||
- **Platform-specific configs**: Mixed in various build scripts
|
||||
- **No validation**: No schema or consistency checks
|
||||
|
||||
## Target State
|
||||
|
||||
### Single Source of Truth
|
||||
|
||||
- **`resources/` directory**: Capacitor default location for source assets
|
||||
- **Eliminate duplication**: Remove `assets/` directory after migration
|
||||
- **Standardized paths**: All tools read from `resources/`
|
||||
|
||||
### Standardized Generation
|
||||
|
||||
- **`capacitor-assets`**: Single tool for all platform asset generation
|
||||
- **Build-time generation**: Assets generated during build, not committed
|
||||
- **Deterministic outputs**: Same inputs → same outputs every time
|
||||
|
||||
### Centralized Configuration
|
||||
|
||||
- **`config/assets/`**: All asset-related configuration files
|
||||
- **Schema validation**: JSON schema for configuration validation
|
||||
- **CI safeguards**: Automated validation and compliance checks
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Phase 1: Foundation Setup ✅
|
||||
|
||||
- [x] Create `config/assets/` directory structure
|
||||
- [x] Create asset configuration schema (`schema.json`)
|
||||
- [x] Create enhanced capacitor-assets configuration
|
||||
- [x] Convert `capacitor.config.json` to `capacitor.config.ts`
|
||||
- [x] Pin Node.js version (`.nvmrc`, `.node-version`)
|
||||
- [x] Create dev-time asset configuration generator
|
||||
- [x] Create asset configuration validator
|
||||
- [x] Add npm scripts for asset management
|
||||
- [x] Update `.gitignore` with proper asset exclusions
|
||||
- [x] Create CI workflow for asset validation
|
||||
|
||||
### Phase 2: Validation & Testing
|
||||
|
||||
- [ ] Run `npm run assets:config` to generate new configuration
|
||||
- [ ] Run `npm run assets:validate` to verify configuration
|
||||
- [ ] Test `npm run build:native` workflow
|
||||
- [ ] Verify CI workflow passes all checks
|
||||
- [ ] Confirm no platform assets are committed to VCS
|
||||
|
||||
### Phase 3: Cleanup & Removal
|
||||
|
||||
- [ ] Remove `assets/` directory (after validation)
|
||||
- [ ] Remove manual asset generation scripts
|
||||
- [ ] Remove asset checking scripts
|
||||
- [ ] Update documentation references
|
||||
- [ ] Final validation of clean state
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
resources/ # Image sources ONLY
|
||||
icon.png
|
||||
splash.png
|
||||
splash_dark.png
|
||||
|
||||
config/assets/ # Versioned config & schema
|
||||
capacitor-assets.config.json
|
||||
schema.json
|
||||
|
||||
scripts/
|
||||
assets-config.js # Dev-time config generator
|
||||
assets-validator.js # Schema validator
|
||||
```
|
||||
|
||||
### Configuration Schema
|
||||
|
||||
The schema enforces:
|
||||
- Source files must be in `resources/` directory
|
||||
- Required fields for icon and splash sections
|
||||
- Android adaptive icon support (foreground/background/monochrome)
|
||||
- iOS LaunchScreen preferences
|
||||
- Target directory validation
|
||||
|
||||
### CI Safeguards
|
||||
|
||||
- **Schema validation**: Configuration must comply with schema
|
||||
- **Source file validation**: All referenced files must exist
|
||||
- **Platform asset denial**: Reject commits with generated assets
|
||||
- **Clean tree enforcement**: Build must not modify committed configs
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Local Validation
|
||||
|
||||
```bash
|
||||
# Generate configuration
|
||||
npm run assets:config
|
||||
|
||||
# Validate configuration
|
||||
npm run assets:validate
|
||||
|
||||
# Test build workflow
|
||||
npm run build:native
|
||||
|
||||
# Clean generated assets
|
||||
npm run assets:clean
|
||||
```
|
||||
|
||||
### CI Validation
|
||||
|
||||
- **Asset validation workflow**: Runs on asset-related changes
|
||||
- **Schema compliance**: Ensures configuration follows schema
|
||||
- **Source file existence**: Verifies all referenced files exist
|
||||
- **Platform asset detection**: Prevents committed generated assets
|
||||
- **Build tree verification**: Ensures clean tree after build
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Data Loss Prevention
|
||||
|
||||
- **Backup branch**: Create backup before removing `assets/`
|
||||
- **Validation checks**: Multiple validation steps before removal
|
||||
- **Gradual migration**: Phase-by-phase approach with rollback capability
|
||||
|
||||
### Build Continuity
|
||||
|
||||
- **Per-platform scripts unchanged**: All existing build orchestration preserved
|
||||
- **Standard toolchain**: Uses capacitor-assets, not custom scripts
|
||||
- **Fallback support**: Manual scripts remain until migration complete
|
||||
|
||||
### Configuration Consistency
|
||||
|
||||
- **Schema enforcement**: JSON schema prevents invalid configurations
|
||||
- **CI validation**: Automated checks catch configuration issues
|
||||
- **Documentation updates**: Clear guidance for future changes
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Technical Requirements
|
||||
|
||||
- [ ] Single source of truth in `resources/` directory
|
||||
- [ ] All platform assets generated via `capacitor-assets`
|
||||
- [ ] No manual asset generation scripts
|
||||
- [ ] Configuration validation passes all checks
|
||||
- [ ] CI workflow enforces asset policies
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
- [ ] Zero duplicate asset sources
|
||||
- [ ] 100% configuration schema compliance
|
||||
- [ ] No platform assets committed to VCS
|
||||
- [ ] Clean build tree after asset generation
|
||||
- [ ] Deterministic asset outputs
|
||||
|
||||
### User Experience
|
||||
|
||||
- [ ] Clear asset management documentation
|
||||
- [ ] Simple development commands
|
||||
- [ ] Consistent asset generation across platforms
|
||||
- [ ] Reduced confusion about asset sources
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Execute Phase 2**: Run validation and testing steps
|
||||
2. **Verify CI workflow**: Ensure all checks pass
|
||||
3. **Execute Phase 3**: Remove duplicate assets and scripts
|
||||
4. **Update documentation**: Finalize README and BUILDING.md
|
||||
5. **Team training**: Ensure all developers understand new workflow
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise during migration:
|
||||
|
||||
1. **Restore backup branch**: `git checkout backup-before-asset-migration`
|
||||
2. **Revert configuration changes**: Remove new config files
|
||||
3. **Restore manual scripts**: Re-enable previous asset generation
|
||||
4. **Investigate issues**: Identify and resolve root causes
|
||||
5. **Plan revised migration**: Adjust approach based on lessons learned
|
||||
|
||||
---
|
||||
|
||||
**Status**: Ready for Phase 2 execution
|
||||
**Priority**: High
|
||||
**Estimated Effort**: 2-3 hours
|
||||
**Dependencies**: CI workflow validation
|
||||
**Stakeholders**: Development team
|
||||
@@ -25,6 +25,10 @@
|
||||
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
|
||||
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||
"build:capacitor:sync": "npm run build:capacitor && npx cap sync",
|
||||
"build:native": "vite build && npx cap sync && npx capacitor-assets generate",
|
||||
"assets:config": "node scripts/assets-config.js",
|
||||
"assets:validate": "node scripts/assets-validator.js",
|
||||
"assets:clean": "rimraf android/app/src/main/res/mipmap-* ios/App/App/Assets.xcassets/**/AppIcon*.png ios/App/App/Assets.xcassets/**/Splash*.png || true",
|
||||
"build:ios": "./scripts/build-ios.sh",
|
||||
"build:ios:dev": "./scripts/build-ios.sh --dev",
|
||||
"build:ios:test": "./scripts/build-ios.sh --test",
|
||||
|
||||
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
174
scripts/assets-config.js
Normal file
174
scripts/assets-config.js
Normal 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
218
scripts/assets-validator.js
Normal 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 };
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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!"
|
||||
@@ -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!"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user