fix(android): resolve icon generation and build script errors #165

Open
anomalist wants to merge 7 commits from imagemagick-anrdoid into master
  1. 32
      .cursor/rules/asset_configuration.mdc
  2. 142
      .github/workflows/asset-validation.yml
  3. 12
      .gitignore
  4. 1
      .node-version
  5. 1
      .nvmrc
  6. 45
      README.md
  7. 2
      android/app/src/main/assets/capacitor.config.json
  8. 7
      android/app/src/main/res/values/colors.xml
  9. 2
      assets/README.md
  10. 36
      capacitor-assets.config.json
  11. 116
      capacitor.config.ts
  12. 32
      config/assets/capacitor-assets.config.json
  13. 119
      config/assets/schema.json
  14. 214
      doc/asset-migration-plan.md
  15. 23
      ios/App/App/Assets.xcassets/SplashDark.imageset/Contents.json
  16. BIN
      ios/App/App/Assets.xcassets/SplashDark.imageset/splash@1x.png
  17. BIN
      ios/App/App/Assets.xcassets/SplashDark.imageset/splash@2x.png
  18. BIN
      ios/App/App/Assets.xcassets/SplashDark.imageset/splash@3x.png
  19. 528
      package-lock.json
  20. 5
      package.json
  21. 0
      resources/icon.png
  22. 0
      resources/splash.png
  23. 0
      resources/splash_dark.png
  24. 224
      scripts/assets-config.ts
  25. 239
      scripts/assets-validator.ts
  26. 6
      scripts/build-android.sh
  27. 142
      scripts/check-android-resources.sh
  28. 96
      scripts/generate-android-icons.sh
  29. 22
      scripts/generate-icons.sh
  30. 67
      scripts/purge-generated-assets.sh

32
.cursor/rules/asset_configuration.mdc

@ -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

@ -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

@ -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

@ -0,0 +1 @@
18.19.0

1
.nvmrc

@ -0,0 +1 @@
18.19.0

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

2
android/app/src/main/assets/capacitor.config.json

@ -29,7 +29,7 @@
"splashFullScreen": true,
"splashImmersive": true
},
"CapacitorSQLite": {
"CapSQLite": {
"iosDatabaseLocation": "Library/CapacitorDatabase",
"iosIsEncryption": false,
"iosBiometric": {

7
android/app/src/main/res/values/colors.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

2
assets/README.md

@ -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.

36
capacitor-assets.config.json

@ -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": {
},
"darkSource": "resources/splash_dark.png",
"ios": {
"source": "resources/ios/splash/splash_dark.png",
"target": "ios/App/App/Assets.xcassets/SplashDark.imageset"
"target": "ios/App/App/Assets.xcassets",
"useStoryBoard": true
},
"android": {
"source": "resources/android/splash/splash_dark.png",
"target": "android/app/src/main/res"
}
"source": "resources/splash.png"
}
}

116
capacitor.config.ts

@ -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

@ -0,0 +1,32 @@
{
"icon": {
"android": {
"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": {
"target": "public/img/icons"
}
},
"splash": {
"android": {
"scale": "cover",
"target": "android/app/src/main/res"
},
"darkSource": "resources/splash_dark.png",
"ios": {
"target": "ios/App/App/Assets.xcassets",
"useStoryBoard": true
},
"source": "resources/splash.png"
}
}

119
config/assets/schema.json

@ -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

@ -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

23
ios/App/App/Assets.xcassets/SplashDark.imageset/Contents.json

@ -0,0 +1,23 @@
{
"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
}
}

BIN
ios/App/App/Assets.xcassets/SplashDark.imageset/splash@1x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
ios/App/App/Assets.xcassets/SplashDark.imageset/splash@2x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
ios/App/App/Assets.xcassets/SplashDark.imageset/splash@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

528
package-lock.json

@ -133,6 +133,7 @@
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.4.0",
"tsx": "^4.20.4",
"typescript": "~5.2.2",
"vite": "^5.2.0"
}
@ -3900,6 +3901,23 @@
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
"integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
@ -3917,6 +3935,23 @@
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
"integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
@ -3934,6 +3969,23 @@
"node": ">=12"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
"integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
@ -17406,6 +17458,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-tsconfig": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/getenv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
@ -26798,6 +26863,16 @@
"node": ">=4"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/resolve-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.0.tgz",
@ -29272,6 +29347,459 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.4",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.4.tgz",
"integrity": "sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
"integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
"integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
"integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
"integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
"integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
"integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
"integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
"integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
"integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
"integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
"integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
"integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
"integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
"integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
"integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
"integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
"integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
"integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
"integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
"integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
"integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
"integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
"integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
"integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.9",
"@esbuild/android-arm": "0.25.9",
"@esbuild/android-arm64": "0.25.9",
"@esbuild/android-x64": "0.25.9",
"@esbuild/darwin-arm64": "0.25.9",
"@esbuild/darwin-x64": "0.25.9",
"@esbuild/freebsd-arm64": "0.25.9",
"@esbuild/freebsd-x64": "0.25.9",
"@esbuild/linux-arm": "0.25.9",
"@esbuild/linux-arm64": "0.25.9",
"@esbuild/linux-ia32": "0.25.9",
"@esbuild/linux-loong64": "0.25.9",
"@esbuild/linux-mips64el": "0.25.9",
"@esbuild/linux-ppc64": "0.25.9",
"@esbuild/linux-riscv64": "0.25.9",
"@esbuild/linux-s390x": "0.25.9",
"@esbuild/linux-x64": "0.25.9",
"@esbuild/netbsd-arm64": "0.25.9",
"@esbuild/netbsd-x64": "0.25.9",
"@esbuild/openbsd-arm64": "0.25.9",
"@esbuild/openbsd-x64": "0.25.9",
"@esbuild/openharmony-arm64": "0.25.9",
"@esbuild/sunos-x64": "0.25.9",
"@esbuild/win32-arm64": "0.25.9",
"@esbuild/win32-ia32": "0.25.9",
"@esbuild/win32-x64": "0.25.9"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

5
package.json

@ -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": "tsx scripts/assets-config.ts",
"assets:validate": "tsx scripts/assets-validator.ts",
"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",
@ -241,6 +245,7 @@
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.4.0",
"tsx": "^4.20.4",
"typescript": "~5.2.2",
"vite": "^5.2.0"
}

0
assets/icon.png → resources/icon.png

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

0
assets/splash.png → resources/splash.png

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

0
assets/splash_dark.png → resources/splash_dark.png

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

224
scripts/assets-config.ts

@ -0,0 +1,224 @@
#!/usr/bin/env tsx
/**
* TimeSafari Asset Configuration Generator
* Generates capacitor-assets configuration files with deterministic outputs
* Author: Matthew Raymer
*
* Usage: tsx scripts/assets-config.ts
*/
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);
// TypeScript interfaces for asset configuration
interface AdaptiveIconConfig {
foreground: string;
background: string;
monochrome: string;
}
interface AndroidIconConfig {
adaptive: AdaptiveIconConfig;
target: string;
}
interface IOSIconConfig {
padding: number;
target: string;
}
interface WebIconConfig {
target: string;
}
interface IconConfig {
source: string;
android: AndroidIconConfig;
ios: IOSIconConfig;
web: WebIconConfig;
}
interface AndroidSplashConfig {
scale: string;
target: string;
}
interface IOSSplashConfig {
useStoryBoard: boolean;
target: string;
}
interface SplashConfig {
source: string;
darkSource: string;
android: AndroidSplashConfig;
ios: IOSSplashConfig;
}
interface AssetConfig {
icon: IconConfig;
splash: SplashConfig;
}
/**
* Generate deterministic capacitor-assets configuration
* @returns Sorted, stable configuration object
*/
function generateAssetConfig(): AssetConfig {
const config: AssetConfig = {
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 obj - Object to sort
* @returns Object with sorted keys
*/
function sortObjectKeys(obj: any): any {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(sortObjectKeys);
}
const sorted: any = {};
Object.keys(obj)
.sort()
.forEach(key => {
sorted[key] = sortObjectKeys(obj[key]);
});
return sorted;
}
/**
* Validate that required source files exist
*/
function validateSourceFiles(): void {
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 config - Configuration object
* @param outputPath - Output file path
*/
function writeConfig(config: AssetConfig, outputPath: string): void {
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(): void {
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 instanceof Error ? error.message : String(error));
process.exit(1);
}
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
export { generateAssetConfig, sortObjectKeys, validateSourceFiles, AssetConfig };

239
scripts/assets-validator.ts

@ -0,0 +1,239 @@
#!/usr/bin/env tsx
/**
* TimeSafari Asset Configuration Validator
* Validates capacitor-assets configuration against schema and source files
* Author: Matthew Raymer
*
* Usage: tsx scripts/assets-validator.ts [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);
// TypeScript interfaces for validation
interface ValidationError {
message: string;
}
interface AssetConfig {
icon?: {
source?: string;
android?: {
adaptive?: {
foreground?: string;
background?: string;
monochrome?: string;
};
};
};
splash?: {
source?: string;
darkSource?: string;
};
}
/**
* Load and parse JSON file
* @param filePath - Path to JSON file
* @returns Parsed JSON object
*/
function loadJsonFile(filePath: string): AssetConfig {
try {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Validate configuration against schema
* @param config - Configuration object to validate
* @returns Array of validation errors
*/
function validateAgainstSchema(config: AssetConfig): ValidationError[] {
const errors: ValidationError[] = [];
// Basic structure validation
if (!config.icon || !config.splash) {
errors.push({ message: 'Configuration must contain both "icon" and "splash" sections' });
}
// Icon validation
if (config.icon) {
if (!config.icon.source) {
errors.push({ message: 'Icon section must contain "source" field' });
} else if (!/^resources\/.*\.(png|svg)$/.test(config.icon.source)) {
errors.push({ message: '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({ message: 'Android adaptive icon must have both foreground and background' });
}
if (adaptive.foreground && !/^resources\/.*\.(png|svg)$/.test(adaptive.foreground)) {
errors.push({ message: 'Android adaptive foreground must be a PNG or SVG file in resources/ directory' });
}
}
}
// Splash validation
if (config.splash) {
if (!config.splash.source) {
errors.push({ message: 'Splash section must contain "source" field' });
} else if (!/^resources\/.*\.(png|svg)$/.test(config.splash.source)) {
errors.push({ message: '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({ message: 'Dark splash source must be a PNG or SVG file in resources/ directory' });
}
}
return errors;
}
/**
* Validate that source files exist
* @param config - Configuration object
* @returns Array of missing file errors
*/
function validateSourceFiles(config: AssetConfig): ValidationError[] {
const errors: ValidationError[] = [];
const requiredFiles = new Set<string>();
// 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({ message: `Source file not found: ${file}` });
}
});
return errors;
}
/**
* Validate target directories are writable
* @param config - Configuration object
* @returns Array of directory validation errors
*/
function validateTargetDirectories(config: AssetConfig): ValidationError[] {
const errors: ValidationError[] = [];
const targetDirs = new Set<string>();
// 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({ message: `Parent directory does not exist: ${parentDir}` });
} else if (!fs.statSync(parentDir).isDirectory()) {
errors.push({ message: `Parent path is not a directory: ${parentDir}` });
}
});
return errors;
}
/**
* Main validation function
* @param configPath - Path to configuration file
* @returns True if validation passes
*/
function validateConfiguration(configPath: string): boolean {
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);
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.message}`);
});
return false;
}
} catch (error) {
console.error('❌ Validation failed:', error instanceof Error ? error.message : String(error));
return false;
}
}
/**
* Main execution function
*/
function main(): void {
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: tsx scripts/assets-validator.ts 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, AssetConfig };

6
scripts/build-android.sh

@ -254,9 +254,9 @@ if [ "$DEPLOY_APP" = true ]; then
exit 0
fi
# Step 1: Check and fix Android resources
safe_execute "Checking Android resources" "$(dirname "$0")/check-android-resources.sh" || {
log_warning "Resource check found issues, but continuing with build..."
# Step 1: Validate asset configuration
safe_execute "Validating asset configuration" "npm run assets:validate" || {
log_warn "Asset validation found issues, but continuing with build..."
}
# Step 2: Clean Android app

142
scripts/check-android-resources.sh

@ -1,142 +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..."
# 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..."
# 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

96
scripts/generate-android-icons.sh

@ -1,96 +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
if ! command -v convert &> /dev/null; then
echo "[ERROR] ImageMagick (convert) 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"
echo "[INFO] Generating launcher icons..."
# Generate launcher icons for different densities
# Android launcher icon sizes: mdpi=48, hdpi=72, xhdpi=96, xxhdpi=144, xxxhdpi=192
convert "$ASSETS_DIR/icon.png" -resize 48x48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher.png"
convert "$ASSETS_DIR/icon.png" -resize 72x72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher.png"
convert "$ASSETS_DIR/icon.png" -resize 96x96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher.png"
convert "$ASSETS_DIR/icon.png" -resize 144x144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher.png"
convert "$ASSETS_DIR/icon.png" -resize 192x192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher.png"
# Generate round launcher icons
convert "$ASSETS_DIR/icon.png" -resize 48x48 "$ANDROID_RES_DIR/mipmap-mdpi/ic_launcher_round.png"
convert "$ASSETS_DIR/icon.png" -resize 72x72 "$ANDROID_RES_DIR/mipmap-hdpi/ic_launcher_round.png"
convert "$ASSETS_DIR/icon.png" -resize 96x96 "$ANDROID_RES_DIR/mipmap-xhdpi/ic_launcher_round.png"
convert "$ASSETS_DIR/icon.png" -resize 144x144 "$ANDROID_RES_DIR/mipmap-xxhdpi/ic_launcher_round.png"
convert "$ASSETS_DIR/icon.png" -resize 192x192 "$ANDROID_RES_DIR/mipmap-xxxhdpi/ic_launcher_round.png"
# Create background and foreground mipmap files
# These reference the existing drawable files
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_background.xml" << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
</adaptive-icon>
EOF
cat > "$ANDROID_RES_DIR/mipmap-anydpi-v26/ic_launcher_foreground.xml" << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
EOF
# Update the existing launcher XML files to reference the correct resources
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="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/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="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
EOF
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 " - Updated mipmap-anydpi-v26 XML files"
echo "[SUCCESS] Android icon generation completed successfully!"

22
scripts/generate-icons.sh

@ -1,22 +0,0 @@
#!/bin/bash
# Create directories if they don't exist
mkdir -p android/app/src/main/res/mipmap-mdpi
mkdir -p android/app/src/main/res/mipmap-hdpi
mkdir -p android/app/src/main/res/mipmap-xhdpi
mkdir -p android/app/src/main/res/mipmap-xxhdpi
mkdir -p android/app/src/main/res/mipmap-xxxhdpi
# Generate placeholder icons using ImageMagick
convert -size 48x48 xc:blue -gravity center -pointsize 20 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-mdpi/ic_launcher.png
convert -size 72x72 xc:blue -gravity center -pointsize 30 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-hdpi/ic_launcher.png
convert -size 96x96 xc:blue -gravity center -pointsize 40 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
convert -size 144x144 xc:blue -gravity center -pointsize 60 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
convert -size 192x192 xc:blue -gravity center -pointsize 80 -fill white -annotate 0 "TS" android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
# Copy to round versions
cp android/app/src/main/res/mipmap-mdpi/ic_launcher.png android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-hdpi/ic_launcher.png android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-xhdpi/ic_launcher.png android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
cp android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

67
scripts/purge-generated-assets.sh

@ -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"
Loading…
Cancel
Save