diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8aa33c1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: iOS CI + +on: + pull_request: + push: + branches: [main, develop] + +env: + DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer + +jobs: + ci: + name: Build, Test & Quality Checks + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "16.4" + + - name: Auto-configure if unconfigured + run: | + # Detect if this is an unconfigured template + if grep -q "{{PROJECT_NAME}}" project.yml 2>/dev/null; then + echo "๐Ÿ”ง Unconfigured template detected - generating minimal test project" + ./scripts/setup.sh \ + --project-name "CITestApp" \ + --bundle-id-root "com.ci.test" \ + --deployment-target 18.0 \ + --swift-version 6.2 \ + --test-framework "swift-testing" \ + --structure mvvm \ + --generate-minimal \ + --no-git-hooks \ + --no-commit \ + --skip-brew \ + --force + echo "โœ… Template auto-configured for CI testing" + else + echo "โœ… Project already configured (derived repository)" + fi + + - name: Cache Homebrew packages + uses: actions/cache@v4 + with: + path: | + ~/Library/Caches/Homebrew + /usr/local/Homebrew + key: ${{ runner.os }}-homebrew-${{ hashFiles('**/Brewfile') }} + restore-keys: | + ${{ runner.os }}-homebrew- + + - name: Install dependencies + run: | + echo "๐Ÿ“ฆ Installing Homebrew dependencies..." + brew bundle install --file=./Brewfile + + echo "โœ… Installed versions:" + swiftlint --version + swiftformat --version + xcodegen --version + yq --version + + - name: Generate Xcode project + run: | + echo "๐Ÿ—๏ธ Generating Xcode project from project.yml..." + xcodegen generate + + PROJECT_NAME=$(yq eval '.name' project.yml) + if [ -d "${PROJECT_NAME}.xcodeproj" ]; then + echo "โœ… Xcode project generated: ${PROJECT_NAME}.xcodeproj" + else + echo "โŒ Failed to generate Xcode project" + exit 1 + fi + + - name: Run preflight checks + run: | + echo "๐Ÿš€ Running preflight checks (format, lint, build, test)..." + ./scripts/preflight.sh + + - name: CI Summary + if: success() + run: | + echo "" + echo "๐ŸŽ‰ CI Complete!" + echo "" + echo "โœ… Code formatting validated" + echo "โœ… Code quality checks passed" + echo "โœ… Build completed successfully" + echo "โœ… Tests executed" + echo "" + echo "Ready for review! ๐Ÿš€" diff --git a/.github/workflows/template-validation.yml b/.github/workflows/template-validation.yml deleted file mode 100644 index af728a4..0000000 --- a/.github/workflows/template-validation.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: Template Validation - -on: - pull_request: - branches: [main, develop] - push: - branches: [main, develop] - -env: - DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer - -jobs: - validate-template: - name: Template End-to-End Validation - runs-on: macos-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Check if this is the template repository - id: repo_check - run: | - echo "Repository: $GITHUB_REPOSITORY" - if [[ "$GITHUB_REPOSITORY" == *"/SwiftProjectTemplate" ]]; then - echo "โœ… This is the SwiftProjectTemplate repository - proceeding with validation" - echo "is_template_repo=true" >> $GITHUB_OUTPUT - else - echo "โ„น๏ธ This is a generated project repository - skipping template validation" - echo "๐Ÿ’ก Template validation only runs on the SwiftProjectTemplate repository" - echo "is_template_repo=false" >> $GITHUB_OUTPUT - fi - - - name: Set up Xcode - if: steps.repo_check.outputs.is_template_repo == 'true' - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "16.0" - - - name: Cache Homebrew packages - if: steps.repo_check.outputs.is_template_repo == 'true' - uses: actions/cache@v4 - with: - path: | - ~/Library/Caches/Homebrew - /usr/local/Homebrew - key: ${{ runner.os }}-homebrew-${{ hashFiles('**/Brewfile') }} - restore-keys: | - ${{ runner.os }}-homebrew- - - - name: Install dependencies - if: steps.repo_check.outputs.is_template_repo == 'true' - run: | - # Install Homebrew dependencies - brew bundle install --file=./Brewfile - - # Verify installations - echo "Installed versions:" - swiftlint --version - swiftformat --version - xcodegen --version - yq --version - - - name: Generate test project - if: steps.repo_check.outputs.is_template_repo == 'true' - run: | - echo "๐Ÿ—๏ธ Generating test project for E2E validation..." - echo "Working directory: $(pwd)" - echo "GITHUB_WORKSPACE: $GITHUB_WORKSPACE" - - # Create test directory but run setup from template root - mkdir -p ci-test-project - ./scripts/dev-sync.sh to ci-test-project - - echo "Running setup script from template root directory" - echo "Target directory: $(pwd)/ci-test-project" - - cd ci-test-project - - echo "๐Ÿš€ Running setup script to generate test project..." - ./scripts/setup.sh \ - --project-name "CITestApp" \ - --bundle-id-root "com.ci.test" \ - --deployment-target 18.0 \ - --swift-version 5.10 \ - --test-framework "swift-testing" \ - --source-language en \ - --no-git-hooks \ - --no-commit \ - --public \ - --skip-brew \ - --force || { - echo "โŒ Setup script failed with exit code $?" - echo "๐Ÿ’ก For detailed debugging, enable ACTIONS_STEP_DEBUG in repository settings" - exit 1 - } - - echo "โœ“ Test project generated successfully" - echo "Generated files:" - ls -la - - - name: Run E2E validation on generated project - if: steps.repo_check.outputs.is_template_repo == 'true' - run: | - echo "๐Ÿ” Running end-to-end validation on generated project..." - cd ci-test-project - - # Run the generated project's preflight check - echo "Running preflight check..." - ./scripts/preflight.sh - - echo "โœ“ E2E validation completed successfully" - - - name: Cleanup test project - if: always() && steps.repo_check.outputs.is_template_repo == 'true' - run: | - echo "๐Ÿงน Cleaning up test project..." - rm -rf ci-test-project - echo "โœ“ Cleanup completed" - - - name: Template validation summary - if: steps.repo_check.outputs.is_template_repo == 'true' - run: | - echo "๐ŸŽ‰ Template Validation Complete!" - echo "" - echo "โœ… Template generates project successfully" - echo "โœ… Generated project passes all quality checks" - echo "โœ… E2E workflow validated" - echo "" - echo "Template is ready for use! ๐Ÿš€" - - - name: Generated repository info - if: steps.repo_check.outputs.is_template_repo == 'false' - run: | - echo "๐ŸŽ‰ Welcome to your new iOS project!" - echo "" - echo "This repository was created from SwiftProjectTemplate." - echo "Template validation is skipped for generated projects." - echo "" - echo "๐Ÿ“– Next steps:" - echo " 1. Run './scripts/setup.sh' to configure your project" - echo " 2. Follow the README.md for development guidance" - echo " 3. Start building your iOS app!" - echo "" - echo "โœจ Happy coding! ๐Ÿš€" diff --git a/templates/.swiftformat.template b/.swiftformat similarity index 98% rename from templates/.swiftformat.template rename to .swiftformat index 6ea1acd..089877e 100644 --- a/templates/.swiftformat.template +++ b/.swiftformat @@ -67,4 +67,4 @@ --exclude .build --exclude DerivedData --exclude build ---exclude **/.git \ No newline at end of file +--exclude **/.git diff --git a/templates/.swiftlint.yml.template b/.swiftlint.yml similarity index 99% rename from templates/.swiftlint.yml.template rename to .swiftlint.yml index b40d321..462c212 100644 --- a/templates/.swiftlint.yml.template +++ b/.swiftlint.yml @@ -128,4 +128,4 @@ custom_rules: message: "Prefer @objc on individual members over @objcMembers" severity: warning -reporter: "xcode" \ No newline at end of file +reporter: "xcode" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a584149 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,165 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Essential Commands +- `./scripts/setup.sh` - One-time project setup: installs dependencies, generates project, configures simulators +- `./scripts/build.sh` - Build the app (add `--device` for device builds, `--release` for Release config) +- `./scripts/test.sh` - Run unit tests (add `--ui` for UI tests, `--all` for both, `--release` for Release config) +- `./scripts/lint.sh` - Check code style with SwiftLint (add `--fix` for auto-fix, `--strict` for warnings as errors) +- `./scripts/format.sh` - Check code formatting with SwiftFormat (add `--fix` for auto-fix) +- `./scripts/preflight.sh` - Complete local CI check: fixes formatting, runs linting, builds, and tests +- `xcodegen` - Regenerate Xcode project from project.yml (required after adding/removing files or changing project structure) + +### Simulator Management +- `./scripts/simulator.sh list` - Show available simulators +- `./scripts/simulator.sh config-tests "device-name"` - Configure simulator for unit tests +- `./scripts/simulator.sh config-ui-tests "device-name"` - Configure simulator for UI tests +- `./scripts/simulator.sh show-config` - Display current simulator configuration + +## Architecture Overview + +### Project Structure Pattern +This is a **Swift iOS project template** that uses: +- **XcodeGen**: Project files generated from `project.yml` configuration, not manually managed +- **In-Place Configuration**: Files contain `{{PLACEHOLDERS}}` that get replaced during setup +- **Dual Mode Setup**: Adopt existing Xcode projects OR generate minimal projects +- **Script Automation**: Comprehensive bash script ecosystem for all development tasks +- **Quality-First**: Built-in SwiftLint, SwiftFormat, and pre-commit hooks +- **Unified CI**: Same workflow runs on template repo and generated projects + +### Core Components +- **`scripts/` Directory**: Contains all development automation scripts with helper functions in `_helpers.sh` +- **`project.yml`**: XcodeGen configuration with placeholders like `{{PROJECT_NAME}}` +- **`simulator.yml`**: Auto-generated simulator configuration for tests +- **`.github/workflows/ci.yml`**: Unified CI workflow that auto-configures if needed +- **Brewfile**: Manages all development tool dependencies (yq, jq, xcodegen, swiftlint, etc.) + +### Setup Modes + +**ADOPT Mode (Primary Workflow - Xcode-First)** +1. Create project in Xcode with your preferred template +2. Clone template repository into project directory +3. Run `./scripts/setup.sh --project-name YourApp` +4. Script detects existing .xcodeproj and configures tooling around it +5. Optional: Create directory structure with `--structure mvvm` + +**GENERATE Mode (Secondary Workflow - Quick Start)** +1. Clone template repository +2. Run `./scripts/setup.sh --project-name YourApp --generate-minimal` +3. Script creates minimal Swift files and directory structure +4. Runs `xcodegen` to create `.xcodeproj` from configured `project.yml` +5. Configures simulators and installs pre-commit hooks + +## Project Generation and Management + +### Using This Template + +**Primary Workflow (Xcode-First - Recommended)** +1. Create new project in Xcode using any template (SwiftUI App, UIKit, etc.) +2. Clone this template repository into the same directory +3. Run `./scripts/setup.sh --project-name YourApp` +4. Script adopts your Xcode project and adds tooling/automation +5. Optional: Use `--structure mvvm` to create directory structure + +**Secondary Workflow (Quick Start)** +1. Clone this template repository +2. Run `./scripts/setup.sh --project-name YourApp --generate-minimal` +3. Script generates minimal project and configures everything +4. Result: Configured project with `YourApp/`, `YourAppTests/`, `YourAppUITests/` + +### After Setup +- **Always run `xcodegen`** after modifying `project.yml` or adding/removing files +- Use scripts for all development tasks rather than Xcode's built-in build/test +- Directory structure is optional: use `--structure mvvm`, `--structure clean`, or `--structure none` +- In ADOPT mode, script works with your existing Xcode project structure +- In GENERATE mode, creates minimal UIKit project with configurable structure + +## Configuration Files + +### Critical Files (Pre-configured with Placeholders) +- **`project.yml`** - XcodeGen project definition with `{{PROJECT_NAME}}` placeholders +- **`simulator.yml`** - Auto-generated during setup with sensible defaults +- **`.swiftlint.yml`** - SwiftLint rules with `{{PROJECT_NAME}}` in paths +- **`.swiftformat`** - SwiftFormat configuration ready to use + +### Development Dependencies +All tools installed via Brewfile: yq, jq, xcodegen, swiftlint, swiftformat, xcbeautify, gh + +## Testing Strategy + +### Test Target Structure +- **Unit Tests**: `{ProjectName}Tests/` - mirrors main app structure +- **UI Tests**: `{ProjectName}UITests/` - user interaction flows +- **Test Configuration**: Managed via `simulator.yml` for consistent device/OS selection +- **Coverage**: Enabled by default, viewable in Xcode Report Navigator + +### Running Tests +- Unit tests use `simulators.tests` configuration from `simulator.yml` +- UI tests use `simulators.ui-tests` configuration from `simulator.yml` +- Scripts auto-detect available simulators and suggest alternatives if configured simulator not found + +## Code Quality and Style + +### Formatting and Linting +- **SwiftFormat**: 2-space indentation, 120 character line width, self insertion required +- **SwiftLint**: iOS development best practices, configurable via `.swiftlint.yml` +- **Pre-commit Hooks**: Automatically installed, run formatting and linting before commits + +### Quality Workflow +1. `./scripts/format.sh --fix` - Fix formatting issues +2. `./scripts/lint.sh --fix` - Fix linting issues +3. `./scripts/build.sh` - Verify build +4. `./scripts/test.sh --all` - Run all tests +5. `./scripts/preflight.sh` - Complete quality check + +## Common Development Tasks + +### Adding New Features (in Generated Project) +1. Create files in appropriate directories (Models/, Views/, ViewModels/, Services/) +2. Add corresponding tests in `{ProjectName}Tests/` +3. Update `project.yml` if new groups/files need explicit configuration +4. Run `xcodegen` to regenerate project file +5. Run `./scripts/preflight.sh` before committing + +### Template Development (this repository) +- Edit placeholder files directly (project.yml, .swiftlint.yml, etc.) +- Test with `./scripts/setup.sh --project-name TestApp --force` +- CI automatically tests template by running setup then preflight +- Same CI workflow works for both template and generated projects + +### Before Every Commit +- Run `./scripts/preflight.sh` (does formatting, linting, building, testing) +- Review git diff for auto-fix changes +- Ensure all tests pass + +### Debugging Issues +- **Build failures**: Clean build folder, run `xcodegen`, check `project.yml` syntax +- **Missing tools**: Run `brew bundle install` to install dependencies +- **Simulator issues**: Use `./scripts/simulator.sh list` to find available devices +- **Setup issues**: Check for `{{PLACEHOLDERS}}` that weren't replaced + +## GitHub Integration + +### CI/CD Strategy +- **Unified Workflow**: `.github/workflows/ci.yml` works for both template and generated projects +- **Auto-Configuration**: CI detects unconfigured state and runs setup automatically +- **Local Parity**: CI runs `./scripts/preflight.sh` - same as local development +- **No Duplication**: CI uses scripts instead of duplicating logic + +### CI Workflow +1. Checkout code +2. Detect if project is configured (check for `{{PROJECT_NAME}}` in project.yml) +3. If unconfigured: run `./scripts/setup.sh --generate-minimal` with test parameters +4. Install dependencies via Brewfile +5. Generate Xcode project with xcodegen (or use existing in adopt mode) +6. Run `./scripts/preflight.sh` (format, lint, build, test) + +### Template Testing +The template repository's CI automatically: +1. Detects it's unconfigured (sees `{{PLACEHOLDERS}}`) +2. Runs `setup.sh --generate-minimal` to create test project +3. Runs full preflight validation +4. Ensures template works before anyone uses it \ No newline at end of file diff --git a/templates/project.yml.template b/project.yml similarity index 99% rename from templates/project.yml.template rename to project.yml index 2629adb..4a90fe3 100644 --- a/templates/project.yml.template +++ b/project.yml @@ -111,4 +111,4 @@ schemes: analyze: config: Debug archive: - config: Release \ No newline at end of file + config: Release diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index 29d1bc5..ed0b1f3 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -4,11 +4,16 @@ set -euo pipefail # Pre-commit hook for SwiftProjectTemplate projects # Runs formatting, linting, and basic validation before allowing commits -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# Find the repository root (works from both scripts/ and .git/hooks/) +if [[ -d ".git" ]]; then + ROOT_DIR="$(pwd)" +else + ROOT_DIR="$(git rev-parse --show-toplevel)" +fi cd "$ROOT_DIR" -# Source helper functions -source "$(dirname "${BASH_SOURCE[0]}")/_helpers.sh" +# Source helper functions from scripts directory +source "$ROOT_DIR/scripts/_helpers.sh" # Configuration AUTO_FIX=true diff --git a/scripts/setup.sh b/scripts/setup.sh index d769dd6..f6de16b 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -# Enhanced setup script for SwiftProjectTemplate -# Supports both interactive and CLI modes with intelligent prompting +# Simplified setup script for SwiftProjectTemplate +# Does in-place replacement of placeholders instead of template processing ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" @@ -12,93 +12,81 @@ source "$(dirname "${BASH_SOURCE[0]}")/_helpers.sh" # Default values PROJECT_NAME="" -DEPLOYMENT_TARGET="26.0" +DEPLOYMENT_TARGET="18.0" SWIFT_VERSION="6.2" -PROJECT_TYPE="private" # private or public +PROJECT_TYPE="private" BUNDLE_ID_ROOT="com.yourcompany" -TEST_FRAMEWORK="swift-testing" # swift-testing or xctest +TEST_FRAMEWORK="swift-testing" SOURCE_LANGUAGE="en" USE_GIT_HOOKS=true CREATE_INITIAL_COMMIT=true -GIT_REPOSITORY_AVAILABLE=false FORCE_OVERWRITE=false SKIP_BREW=false - -# Track which values were set via CLI -CLI_PROJECT_NAME_SET=false -CLI_DEPLOYMENT_TARGET_SET=false -CLI_SWIFT_VERSION_SET=false -CLI_VISIBILITY_SET=false -CLI_BUNDLE_ID_ROOT_SET=false -CLI_TEST_FRAMEWORK_SET=false -CLI_SOURCE_LANGUAGE_SET=false -CLI_GIT_HOOKS_SET=false -CLI_COMMIT_SET=false -CLI_GIT_INIT_SET=false +GENERATE_MINIMAL=false +STRUCTURE="mvvm" # mvvm, clean, or none +PROJECT_MODE="" # Will be set to "adopt" or "generate" show_help() { cat < [OPTIONS] + +PRIMARY OPTIONS: + --project-name Project name (required) + --generate-minimal Create minimal project (default: adopt existing .xcodeproj) + --structure Directory structure: mvvm, clean, or none (default: mvvm) -OPTIONS: - --project-name Project name (e.g., "FooApp") - Must be a valid Swift identifier +CONFIGURATION OPTIONS: --bundle-id-root Bundle identifier root (default: $BUNDLE_ID_ROOT) - Format: com.yourname or com.company --deployment-target iOS deployment target (default: $DEPLOYMENT_TARGET) - Format: X.Y (e.g., 17.0, 18.0, 26.0) --swift-version Swift version (default: $SWIFT_VERSION) - Format: X.Y (e.g., 5.9, 5.10, 6.0) - --test-framework Test framework (default: $TEST_FRAMEWORK) - Options: swift-testing, xctest - --source-language Source language for localization (default: $SOURCE_LANGUAGE) - Format: ISO 639-1 code (e.g., en, es, fr, de) - --git-hooks Enable git pre-commit hooks (default) - --no-git-hooks Disable git pre-commit hooks - --commit Create initial git commit after setup (default) - --no-commit Skip initial git commit after setup - --public Make this a public project (includes LICENSE in README) - --private Make this a private project (default) - --force Overwrite existing files without prompting - --skip-brew Skip Homebrew dependency installation - --help Show this help message + --test-framework Test framework: swift-testing or xctest (default: $TEST_FRAMEWORK) + --source-language Source language (default: $SOURCE_LANGUAGE) + +PROJECT OPTIONS: + --public Make this a public project + --private Make this a private project (default) + +GIT OPTIONS: + --git-hooks Enable git pre-commit hooks (default) + --no-git-hooks Disable git pre-commit hooks + --commit Create initial git commit (default) + --no-commit Skip initial git commit + +OTHER OPTIONS: + --force Overwrite existing files without prompting + --skip-brew Skip Homebrew dependency installation + --help Show this help message EXAMPLES: - $0 # Interactive mode - $0 --project-name "MyApp" # CLI + interactive for missing info - $0 --project-name "MyApp" --public # Mostly CLI, minimal prompts - $0 --project-name "MyApp" \\ - --deployment-target "26.0" \\ - --swift-version "6.2" \\ - --source-language "en" \\ - --public --force # Full CLI mode - $0 --project-name "MyApp" --no-commit # Skip initial git commit - -VALIDATION: - - Project name must be a valid Swift identifier (alphanumeric, starts with letter) - - Deployment target and Swift version must be in X.Y format - - Conflicting options (--public and --private) will cause an error - -WORKFLOW: - 1. Parse CLI arguments and validate for conflicts - 2. Prompt interactively for missing required information - 3. Install Homebrew dependencies (unless --skip-brew) - 4. Generate all configuration files from templates - 5. Create MVVM folder structure - 6. Create Resources with localization and asset catalogs - 7. Generate Xcode project with XcodeGen - 8. Configure simulators with intelligent defaults - 9. Verify or create git repository (offers to run 'git init' if not in a repository) - 10. Set up git pre-commit hooks (if git repository available) - 11. Create initial git commit with project details (unless --no-commit) - 12. Display next steps + # Adopt existing Xcode project (primary workflow) + $0 --project-name "MyApp" + + # Generate minimal project for quick start + $0 --project-name "MyApp" --generate-minimal + + # Generate with clean structure (no MVVM directories) + $0 --project-name "MyApp" --generate-minimal --structure clean + + # Public project with custom config + $0 --project-name "MyApp" --public --deployment-target 17.0 + +WORKFLOWS: + 1. Xcode-first (recommended): + - Create project in Xcode + - Clone template into project directory + - Run: ./scripts/setup.sh --project-name YourApp + + 2. Template-first (quick start): + - Clone template + - Run: ./scripts/setup.sh --project-name YourApp --generate-minimal + - Open generated .xcodeproj in Xcode EOF } @@ -107,100 +95,51 @@ parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in --project-name) - if [[ -z "${2:-}" ]]; then - log_error "Option --project-name requires a value" - exit 1 - fi PROJECT_NAME="$2" - CLI_PROJECT_NAME_SET=true shift 2 ;; --bundle-id-root) - if [[ -z "${2:-}" ]]; then - log_error "Option --bundle-id-root requires a value" - exit 1 - fi BUNDLE_ID_ROOT="$2" - CLI_BUNDLE_ID_ROOT_SET=true shift 2 ;; --deployment-target) - if [[ -z "${2:-}" ]]; then - log_error "Option --deployment-target requires a value" - exit 1 - fi DEPLOYMENT_TARGET="$2" - CLI_DEPLOYMENT_TARGET_SET=true shift 2 ;; --swift-version) - if [[ -z "${2:-}" ]]; then - log_error "Option --swift-version requires a value" - exit 1 - fi SWIFT_VERSION="$2" - CLI_SWIFT_VERSION_SET=true shift 2 ;; --test-framework) - if [[ -z "${2:-}" ]]; then - log_error "Option --test-framework requires a value" - exit 1 - fi - if [[ "$2" != "swift-testing" && "$2" != "xctest" ]]; then - log_error "Invalid test framework: $2. Must be 'swift-testing' or 'xctest'" - exit 1 - fi TEST_FRAMEWORK="$2" - CLI_TEST_FRAMEWORK_SET=true shift 2 ;; --source-language) - if [[ -z "${2:-}" ]]; then - log_error "Option --source-language requires a value" - exit 1 - fi SOURCE_LANGUAGE="$2" - CLI_SOURCE_LANGUAGE_SET=true shift 2 ;; --git-hooks) USE_GIT_HOOKS=true - CLI_GIT_HOOKS_SET=true shift ;; --no-git-hooks) USE_GIT_HOOKS=false - CLI_GIT_HOOKS_SET=true shift ;; --commit) CREATE_INITIAL_COMMIT=true - CLI_COMMIT_SET=true shift ;; --no-commit) CREATE_INITIAL_COMMIT=false - CLI_COMMIT_SET=true shift ;; --public) - if [[ "$PROJECT_TYPE" == "private" ]]; then - PROJECT_TYPE="public" - else - log_error "Conflicting options: --public specified but PROJECT_TYPE is already '$PROJECT_TYPE'" - exit 1 - fi - CLI_VISIBILITY_SET=true + PROJECT_TYPE="public" shift ;; --private) - if [[ "$PROJECT_TYPE" == "public" ]]; then - log_error "Conflicting options: --private specified but --public was already set" - exit 1 - fi PROJECT_TYPE="private" - CLI_VISIBILITY_SET=true shift ;; --force) @@ -211,1035 +150,493 @@ parse_arguments() { SKIP_BREW=true shift ;; + --generate-minimal) + GENERATE_MINIMAL=true + shift + ;; + --structure) + STRUCTURE="$2" + shift 2 + ;; --help|-h) show_help exit 0 ;; - -*) - log_error "Unknown option: $1" - echo "Use '$0 --help' for usage information" - exit 1 - ;; *) - log_error "Unexpected argument: $1" + log_error "Unknown option: $1" echo "Use '$0 --help' for usage information" exit 1 ;; esac done -} - -validate_inputs() { - local has_errors=false - - # Validate project name - if [[ -n "$PROJECT_NAME" ]]; then - if ! validate_project_name "$PROJECT_NAME"; then - log_error "Invalid project name: '$PROJECT_NAME'" - log_info "Project name must:" - log_info " - Start with a letter (A-Z, a-z)" - log_info " - Contain only alphanumeric characters" - log_info " - Be a valid Swift identifier" - log_info "Examples: MyApp, FooApp, TimeTracker" - has_errors=true - fi - fi - # Validate bundle ID root - if [[ -n "$BUNDLE_ID_ROOT" ]]; then - if ! validate_bundle_id_root "$BUNDLE_ID_ROOT"; then - log_error "Invalid bundle ID root: '$BUNDLE_ID_ROOT'" - log_info "Bundle ID root must be in reverse domain format" - log_info "Examples: com.yourname, com.company, io.github.username" - has_errors=true - fi - fi - - # Validate deployment target - if ! validate_ios_version "$DEPLOYMENT_TARGET"; then - log_error "Invalid deployment target: '$DEPLOYMENT_TARGET'" - log_info "Deployment target must be in format X.Y (e.g., 17.0, 18.0, 26.0)" - has_errors=true - fi - - # Validate Swift version - if ! validate_swift_version "$SWIFT_VERSION"; then - log_error "Invalid Swift version: '$SWIFT_VERSION'" - log_info "Swift version must be in format X.Y (e.g., 5.9, 5.10, 6.0)" - has_errors=true - fi - - if $has_errors; then + # Validate structure option + if [[ "$STRUCTURE" != "mvvm" && "$STRUCTURE" != "clean" && "$STRUCTURE" != "none" ]]; then + log_error "Invalid structure: $STRUCTURE. Must be 'mvvm', 'clean', or 'none'" exit 1 fi } -prompt_for_missing_info() { - # Only prompt for project name if not provided via CLI - if [[ "$CLI_PROJECT_NAME_SET" == false ]]; then - while true; do - echo - read -p "Enter project name (e.g., MyApp): " PROJECT_NAME - if validate_project_name "$PROJECT_NAME"; then - break - else - log_error "Invalid project name. Must start with a letter and contain only alphanumeric characters." - fi - done - fi - - # Only prompt for bundle ID root if not provided via CLI - if [[ "$CLI_BUNDLE_ID_ROOT_SET" == false ]]; then - while true; do - echo - read -p "Bundle ID root (default: $BUNDLE_ID_ROOT): " input_bundle_id_root - if [[ -n "$input_bundle_id_root" ]]; then - if validate_bundle_id_root "$input_bundle_id_root"; then - BUNDLE_ID_ROOT="$input_bundle_id_root" - break - else - log_error "Invalid bundle ID root. Use reverse domain format (e.g., com.yourname)" - fi - else - break # Use default - fi - done - fi - - # Only prompt for deployment target if not provided via CLI - if [[ "$CLI_DEPLOYMENT_TARGET_SET" == false ]]; then - echo - read -p "iOS deployment target (default: $DEPLOYMENT_TARGET): " input_deployment_target - if [[ -n "$input_deployment_target" ]]; then - DEPLOYMENT_TARGET="$input_deployment_target" - if ! validate_ios_version "$DEPLOYMENT_TARGET"; then - log_error "Invalid deployment target format. Using default: $DEPLOYMENT_TARGET" - DEPLOYMENT_TARGET="26.0" - fi - fi - fi - - # Only prompt for Swift version if not provided via CLI - if [[ "$CLI_SWIFT_VERSION_SET" == false ]]; then - echo - read -p "Swift version (default: $SWIFT_VERSION): " input_swift_version - if [[ -n "$input_swift_version" ]]; then - SWIFT_VERSION="$input_swift_version" - if ! validate_swift_version "$SWIFT_VERSION"; then - log_error "Invalid Swift version format. Using default: $SWIFT_VERSION" - SWIFT_VERSION="6.2" - fi - fi - fi - - # Only prompt for test framework if not provided via CLI - if [[ "$CLI_TEST_FRAMEWORK_SET" == false ]]; then - echo - while true; do - read -p "Test framework (swift-testing/xctest, default: $TEST_FRAMEWORK): " input_test_framework - if [[ -z "$input_test_framework" ]]; then - break # Use default - elif [[ "$input_test_framework" == "swift-testing" || "$input_test_framework" == "xctest" ]]; then - TEST_FRAMEWORK="$input_test_framework" - break - else - log_error "Invalid test framework. Choose 'swift-testing' or 'xctest'" - fi - done - fi - - # Only prompt for source language if not provided via CLI - if [[ "$CLI_SOURCE_LANGUAGE_SET" == false ]]; then - echo - read -p "Source language for localization (default: $SOURCE_LANGUAGE): " input_source_language - if [[ -n "$input_source_language" ]]; then - SOURCE_LANGUAGE="$input_source_language" - fi - fi - - # Only prompt for git hooks if not provided via CLI - if [[ "$CLI_GIT_HOOKS_SET" == false ]]; then - echo - while true; do - read -p "Enable git pre-commit hooks? (Y/n): " yn - case $yn in - [Yy]*|"") USE_GIT_HOOKS=true; break;; - [Nn]*) USE_GIT_HOOKS=false; break;; - *) log_error "Please answer y or n";; - esac - done - fi - - # Ask about project visibility if not specified via CLI - if [[ "$CLI_VISIBILITY_SET" == false ]]; then - echo - while true; do - read -p "Is this a public project? (y/N): " yn - case $yn in - [Yy]* ) PROJECT_TYPE="public"; break;; - [Nn]* | "" ) PROJECT_TYPE="private"; break;; - * ) echo "Please answer yes or no.";; - esac - done +validate_project_name() { + if [[ -z "$PROJECT_NAME" ]]; then + log_error "Project name is required" + echo "Use: $0 --project-name YourProjectName" + exit 1 fi - # Handle git repository initialization if not already in one and git features are needed - if ! is_git_repo && ($USE_GIT_HOOKS || $CREATE_INITIAL_COMMIT) && [[ "$CLI_GIT_INIT_SET" == false ]]; then - echo - log_warning "Not in a git repository" - while true; do - read -p "Would you like to initialize a git repository? (Y/n): " yn - case $yn in - [Yy]*|"") - log_info "Will initialize git repository during setup" - GIT_REPOSITORY_AVAILABLE=true - break - ;; - [Nn]*) - log_info "Git repository will not be initialized" - log_info "Git hooks and initial commit will be skipped" - GIT_REPOSITORY_AVAILABLE=false - break - ;; - *) echo "Please answer yes or no.";; - esac - done - elif is_git_repo; then - GIT_REPOSITORY_AVAILABLE=true - else - GIT_REPOSITORY_AVAILABLE=false + # Validate it's a valid Swift identifier + if [[ ! "$PROJECT_NAME" =~ ^[A-Za-z][A-Za-z0-9]*$ ]]; then + log_error "Invalid project name. Must start with a letter and contain only alphanumerics." + exit 1 fi } -detect_architecture() { - # Detect Mac architecture for simulator settings - case "$(uname -m)" in - arm64) echo "arm64" ;; - x86_64) echo "x86_64" ;; - *) echo "arm64" ;; # Default to arm64 for Apple Silicon - esac -} - install_dependencies() { if $SKIP_BREW; then - log_info "Skipping Homebrew dependency installation (--skip-brew specified)" + log_info "Skipping Homebrew dependency installation" return fi log_info "Installing Homebrew dependencies..." if ! command_exists brew; then - log_error "Homebrew is not installed" - log_info "Please install Homebrew first: https://brew.sh" - log_info "Or run this script with --skip-brew to skip dependency installation" + log_error "Homebrew is not installed. Install from https://brew.sh" exit 1 fi - if ! brew bundle install --file="$ROOT_DIR/Brewfile"; then - log_error "Failed to install Homebrew dependencies" - log_info "You can retry with: brew bundle install --file=./Brewfile" - exit 1 - fi - - log_success "Dependencies installed successfully" + brew bundle install --file="$ROOT_DIR/Brewfile" + log_success "Dependencies installed" } -generate_template_files() { - log_info "Generating project files from templates..." +replace_placeholders() { + log_info "Replacing placeholders in project files..." - local templates_dir="$ROOT_DIR/templates" local project_name_lower project_name_lower=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]') - local simulator_arch - simulator_arch=$(detect_architecture) - - # Prepare template variables - local template_vars=( - "PROJECT_NAME=$PROJECT_NAME" - "PROJECT_NAME_LOWER=$project_name_lower" - "BUNDLE_ID_ROOT=$BUNDLE_ID_ROOT" - "DEPLOYMENT_TARGET=$DEPLOYMENT_TARGET" - "SWIFT_VERSION=$SWIFT_VERSION" - "SIMULATOR_ARCH=$simulator_arch" - "PROJECT_DESCRIPTION=A new iOS application built with SwiftUI" - "ARCHITECTURE=SwiftUI + MVVM" - "ARCHITECTURE_DESCRIPTION=Modern SwiftUI app with Model-View-ViewModel architecture" - "PATTERN_DESCRIPTION=ViewModels handle business logic, Views handle UI" - "KEY_COMPONENTS=- App entry point\\n- Core Data stack\\n- Main ViewModels\\n- SwiftUI Views" - "DATA_FLOW_DESCRIPTION=1. Views observe ViewModels\\n2. ViewModels update Models\\n3. Models notify ViewModels of changes\\n4. ViewModels update Views" - "PROJECT_SPECIFIC_FEATURES=- SwiftUI interface\\n- Core Data persistence\\n- MVVM architecture" - ) - - # Add project type specific variables - if [[ "$PROJECT_TYPE" == "public" ]]; then - template_vars+=( - "LICENSE_BADGE=![License](https://img.shields.io/badge/license-MIT-green)" - "LICENSE_SECTION=## License\\n\\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details." - "CONTRIBUTING_SECTION=## Contributing\\n\\nContributions are welcome! Please feel free to submit a Pull Request." - "REPOSITORY_URL=https://github.com/yourusername/$project_name_lower" - ) - else - template_vars+=( - "LICENSE_BADGE=" - "LICENSE_SECTION=" - "CONTRIBUTING_SECTION=" - "REPOSITORY_URL=https://github.com/yourusername/$project_name_lower" - ) - fi - # Generate each template file - local files_to_generate=( - "project.yml.template:project.yml" - ".swiftlint.yml.template:.swiftlint.yml" - ".swiftformat.template:.swiftformat" - "README.md.template:README.md" - "ci.yml.template:.github/workflows/ci.yml" - ) - - for file_mapping in "${files_to_generate[@]}"; do - local template_file="${file_mapping%:*}" - local output_file="${file_mapping#*:}" - local template_path="$templates_dir/$template_file" - local output_path="$ROOT_DIR/$output_file" - - # Check if output file exists and handle overwrite - # Force overwrite for template-specific files that should always be regenerated - local force_overwrite_files=("README.md") - local should_force_overwrite=false - - for force_file in "${force_overwrite_files[@]}"; do - if [[ "$output_file" == "$force_file" ]]; then - should_force_overwrite=true - break - fi - done - - if [[ -f "$output_path" && "$FORCE_OVERWRITE" != true && "$should_force_overwrite" != true ]]; then - log_warning "File $output_file already exists" - while true; do - read -p "Overwrite $output_file? (y/n): " yn - case $yn in - [Yy]* ) break;; - [Nn]* ) - log_info "Skipping $output_file" - continue 2;; - * ) echo "Please answer yes or no.";; - esac - done - elif [[ -f "$output_path" && "$should_force_overwrite" == true ]]; then - log_info "Force overwriting $output_file (template-specific file)" - fi + # Prepare replacement values + local license_badge="" + local license_section="" + local contributing_section="" - # Generate file from template - if [[ -f "$template_path" ]]; then - # Create output directory if needed - local output_dir - output_dir=$(dirname "$output_path") - if [[ ! -d "$output_dir" ]]; then - mkdir -p "$output_dir" - log_info "Created directory: $output_dir" + if [[ "$PROJECT_TYPE" == "public" ]]; then + license_badge="![License](https://img.shields.io/badge/license-MIT-green)" + license_section="## License\\n\\nThis project is licensed under the MIT License." + contributing_section="## Contributing\\n\\nContributions are welcome! Please submit a Pull Request." + fi + + # Files to process (excluding .git, scripts, etc.) + local files_to_process=$(find . -type f \ + -not -path "./.git/*" \ + -not -path "./scripts/*" \ + -not -path "./.claude/*" \ + -not -path "./Brewfile" \ + -not -path "./README.md" \ + -not -name "*.xcodeproj" \ + -not -name ".DS_Store") + + # Perform replacements on each file + for file in $files_to_process; do + if grep -q "{{" "$file" 2>/dev/null; then + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS sed + sed -i '' \ + -e "s|{{PROJECT_NAME}}|$PROJECT_NAME|g" \ + -e "s|{{PROJECT_NAME_LOWER}}|$project_name_lower|g" \ + -e "s|{{BUNDLE_ID_ROOT}}|$BUNDLE_ID_ROOT|g" \ + -e "s|{{DEPLOYMENT_TARGET}}|$DEPLOYMENT_TARGET|g" \ + -e "s|{{SWIFT_VERSION}}|$SWIFT_VERSION|g" \ + -e "s|{{SOURCE_LANGUAGE}}|$SOURCE_LANGUAGE|g" \ + -e "s|{{PROJECT_DESCRIPTION}}|An iOS application built with Swift|g" \ + -e "s|{{REPOSITORY_URL}}|https://github.com/yourusername/$project_name_lower|g" \ + -e "s|{{LICENSE_BADGE}}|$license_badge|g" \ + -e "s|{{LICENSE_SECTION}}|$license_section|g" \ + -e "s|{{CONTRIBUTING_SECTION}}|$contributing_section|g" \ + "$file" + else + # GNU sed + sed -i \ + -e "s|{{PROJECT_NAME}}|$PROJECT_NAME|g" \ + -e "s|{{PROJECT_NAME_LOWER}}|$project_name_lower|g" \ + -e "s|{{BUNDLE_ID_ROOT}}|$BUNDLE_ID_ROOT|g" \ + -e "s|{{DEPLOYMENT_TARGET}}|$DEPLOYMENT_TARGET|g" \ + -e "s|{{SWIFT_VERSION}}|$SWIFT_VERSION|g" \ + -e "s|{{SOURCE_LANGUAGE}}|$SOURCE_LANGUAGE|g" \ + -e "s|{{PROJECT_DESCRIPTION}}|An iOS application built with Swift|g" \ + -e "s|{{REPOSITORY_URL}}|https://github.com/yourusername/$project_name_lower|g" \ + -e "s|{{LICENSE_BADGE}}|$license_badge|g" \ + -e "s|{{LICENSE_SECTION}}|$license_section|g" \ + -e "s|{{CONTRIBUTING_SECTION}}|$contributing_section|g" \ + "$file" fi - - replace_template_vars "$template_path" "$output_path" "${template_vars[@]}" - log_success "Generated $output_file" - else - log_warning "Template $template_file not found, skipping" + log_success "Processed: $file" fi done + log_success "Placeholder replacement complete" } -create_project_structure() { - log_info "Creating MVVM project structure..." - - # Create main app directories - local app_dirs=( - "$PROJECT_NAME/Models" - "$PROJECT_NAME/Views" - "$PROJECT_NAME/ViewModels" - "$PROJECT_NAME/Services" - "$PROJECT_NAME/Extensions" - "$PROJECT_NAME/Helpers" - ) - - # Create test directories - local test_dirs=( - "${PROJECT_NAME}Tests/Models" - "${PROJECT_NAME}Tests/Views" - "${PROJECT_NAME}Tests/ViewModels" - "${PROJECT_NAME}Tests/Services" - "${PROJECT_NAME}UITests" - ) - - # Create all directories - for dir in "${app_dirs[@]}" "${test_dirs[@]}"; do - mkdir -p "$dir" - # Add .gitkeep to empty directories - touch "$dir/.gitkeep" - log_success "Created $dir/" - done +detect_project_mode() { + log_info "Detecting project mode..." - # Create basic app entry point - local app_file="$PROJECT_NAME/${PROJECT_NAME}App.swift" - if [[ ! -f "$app_file" || "$FORCE_OVERWRITE" == true ]]; then - cat > "$app_file" < "$content_view_file" < "$main_viewmodel_file" < "$viewmodel_test_file" < "$PROJECT_NAME/AppDelegate.swift" <<'EOF' +import UIKit -@MainActor -struct MainViewModelTests { - - @Test("Initial state should be correct") - func initialState() { - let viewModel = MainViewModel() - #expect(viewModel.message == "Hello from $PROJECT_NAME!") - #expect(viewModel.isLoading == false) - } - - @Test("Refresh data should update message and loading state") - func refreshData() async { - let viewModel = MainViewModel() - - // Start refresh - viewModel.refreshData() - #expect(viewModel.isLoading == true) +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return true + } - // Wait for completion - try? await Task.sleep(nanoseconds: 1_100_000_000) // 1.1 seconds + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + return UISceneConfiguration( + name: "Default Configuration", + sessionRole: connectingSceneSession.role + ) + } - #expect(viewModel.isLoading == false) - #expect(viewModel.message.contains("Data refreshed")) - } + func application( + _ application: UIApplication, + didDiscardSceneSessions sceneSessions: Set + ) { + } } EOF - else - cat > "$viewmodel_test_file" <! - - override func setUp() { - super.setUp() - viewModel = MainViewModel() - cancellables = Set() - } - - override func tearDown() { - viewModel = nil - cancellables = nil - super.tearDown() - } - - func testInitialState() throws { - XCTAssertEqual(viewModel.message, "Hello from $PROJECT_NAME!") - XCTAssertFalse(viewModel.isLoading) - } - - func testRefreshData() throws { - let expectation = XCTestExpectation(description: "Data refresh completes") - - viewModel.\$message - .dropFirst() // Skip initial value - .sink { message in - XCTAssertTrue(message.contains("Data refreshed")) - expectation.fulfill() - } - .store(in: &cancellables) - - viewModel.refreshData() - XCTAssertTrue(viewModel.isLoading) - - wait(for: [expectation], timeout: 2.0) - XCTAssertFalse(viewModel.isLoading) - } + # Create SceneDelegate.swift + cat > "$PROJECT_NAME/SceneDelegate.swift" <<'EOF' +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + let window = UIWindow(windowScene: windowScene) + let viewController = ViewController() + window.rootViewController = viewController + window.makeKeyAndVisible() + self.window = window + } } EOF - fi - log_success "Created MainViewModelTests.swift ($TEST_FRAMEWORK)" + + # Create ViewController.swift in appropriate location + local view_path="$PROJECT_NAME" + if [[ "$STRUCTURE" == "mvvm" ]]; then + view_path="$PROJECT_NAME/Views" fi - # Create basic UI test - local ui_test_file="${PROJECT_NAME}UITests/${PROJECT_NAME}UITests.swift" - if [[ ! -f "$ui_test_file" || "$FORCE_OVERWRITE" == true ]]; then - cat > "$ui_test_file" < "$view_path/ViewController.swift" < "$info_plist_file" < "$PROJECT_NAME/Info.plist" <<'EOF' - CFBundleDevelopmentRegion - \$(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - \$(EXECUTABLE_NAME) - CFBundleIdentifier - \$(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - \$(PRODUCT_NAME) - CFBundlePackageType - \$(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - \$(PRODUCT_MODULE_NAME).SceneDelegate - - - - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + EOF - log_success "Created Info.plist" - fi -} -create_resources_structure() { - log_info "Creating Resources with localization and asset catalogs..." - - # Create Resources directory if it doesn't exist - mkdir -p "$PROJECT_NAME/Resources" - - # Create Localizable.xcstrings - local localizable_file="$PROJECT_NAME/Resources/Localizable.xcstrings" - if [[ ! -f "$localizable_file" || "$FORCE_OVERWRITE" == true ]]; then - cat > "$localizable_file" < "${PROJECT_NAME}Tests/${PROJECT_NAME}Tests.swift" < "$assets_dir/Contents.json" < "$appicon_dir/Contents.json" < "${PROJECT_NAME}UITests/${PROJECT_NAME}UITests.swift" < "$accentcolor_dir/Contents.json" </dev/null 2>&1; then - default_iphone="$iphone" - break - fi - done - - # Find first available iPad - for ipad in "${preferred_ipads[@]}"; do - if "$ROOT_DIR/scripts/simulator.sh" optimal-os "$ipad" >/dev/null 2>&1; then - default_ipad="$ipad" - break - fi - done + log_success "Existing project verified" + log_info "Configuring tooling for existing project structure" - # Configure unit tests simulator - if [[ -n "$default_iphone" ]]; then - log_info "Configuring unit tests with: $default_iphone" - if "$ROOT_DIR/scripts/simulator.sh" config-tests "$default_iphone" --yes; then - log_success "Unit tests simulator configured" - else - log_warning "Failed to configure unit tests simulator" - fi - else - log_warning "No suitable iPhone simulator found for unit tests" - log_info "You can configure manually with: ./scripts/simulator.sh config-tests \"\"" + # Create directory structure if requested + if [[ "$STRUCTURE" != "none" ]]; then + create_directory_structure "$PROJECT_NAME" fi +} - # Configure UI tests simulator (prefer iPad if available, fallback to iPhone) - local ui_device="$default_ipad" - if [[ -z "$ui_device" ]]; then - ui_device="$default_iphone" +generate_xcode_project() { + log_info "Generating Xcode project..." + + if ! command_exists xcodegen; then + log_error "xcodegen not found. Run without --skip-brew first." + exit 1 fi - if [[ -n "$ui_device" ]]; then - log_info "Configuring UI tests with: $ui_device" - if "$ROOT_DIR/scripts/simulator.sh" config-ui-tests "$ui_device" --yes; then - log_success "UI tests simulator configured" - else - log_warning "Failed to configure UI tests simulator" - fi - else - log_warning "No suitable simulator found for UI tests" - log_info "You can configure manually with: ./scripts/simulator.sh config-ui-tests \"\"" + if ! xcodegen generate; then + log_error "Failed to generate Xcode project" + exit 1 fi - echo - log_info "Simulator configuration complete. To view current settings:" - echo " ./scripts/simulator.sh show-config" + log_success "Xcode project generated: ${PROJECT_NAME}.xcodeproj" } -verify_or_create_git_repository() { - # If already in a git repository, nothing to do - if is_git_repo; then - return - fi - - # If git repository should be created (determined during prompting) - if $GIT_REPOSITORY_AVAILABLE; then - log_info "Initializing git repository with 'main' as default branch..." - - # Try modern git init with --initial-branch, fallback for older git versions - if git init --initial-branch=main 2>/dev/null; then - log_success "Git repository initialized with 'main' branch" - elif git init 2>/dev/null; then - # Older git version - rename default branch to main - git checkout -b main 2>/dev/null || true - log_success "Git repository initialized and switched to 'main' branch" - else - log_error "Failed to initialize git repository" - GIT_REPOSITORY_AVAILABLE=false - fi +configure_simulators() { + log_info "Configuring simulators..." + + # Create simulator.yml if it doesn't exist + if [[ ! -f "simulator.yml" ]]; then + cat > simulator.yml < "$pre_commit_hook" </dev/null + log_info "Creating initial commit..." - # Check if there are files to commit - if git diff --cached --quiet; then - log_warning "No changes to commit" - return - fi - - # Create initial commit with descriptive message - local commit_message="Initial project setup - -Generated iOS project using SwiftProjectTemplate with: -- Project: $PROJECT_NAME -- Bundle ID: ${BUNDLE_ID_ROOT}.${PROJECT_NAME} -- iOS Deployment Target: $DEPLOYMENT_TARGET -- Swift Version: $SWIFT_VERSION -- Test Framework: $TEST_FRAMEWORK -- Source Language: $SOURCE_LANGUAGE" - - if git commit -m "$commit_message" 2>/dev/null; then - log_success "Initial commit created successfully" - log_info "You can view the commit with: git log --oneline -1" - else - log_error "Failed to create initial commit" - log_info "You may need to configure git user settings:" - echo " git config --global user.name \"Your Name\"" - echo " git config --global user.email \"your.email@example.com\"" - fi -} + # Add all files + git add . -cleanup_template_files() { - log_info "Cleaning up template-specific files..." + # Create commit + git commit -m "Initial commit: $PROJECT_NAME - # Remove template-validation.yml workflow since this is now a generated project - local template_validation_workflow=".github/workflows/template-validation.yml" - if [[ -f "$template_validation_workflow" ]]; then - rm "$template_validation_workflow" - log_success "Removed template-validation.yml (not needed for generated projects)" - fi +Generated from SwiftProjectTemplate +- iOS $DEPLOYMENT_TARGET+ +- Swift $SWIFT_VERSION +- Test framework: $TEST_FRAMEWORK - # Remove empty .github/workflows directory if it has no other workflows - local workflows_dir=".github/workflows" - if [[ -d "$workflows_dir" && -z "$(ls -A "$workflows_dir" 2>/dev/null)" ]]; then - rmdir "$workflows_dir" - log_info "Removed empty workflows directory" - fi +๐Ÿค– Generated with SwiftProjectTemplate" || log_warning "Commit may have failed or no changes to commit" - # Remove empty .github directory if it has no other content - local github_dir=".github" - if [[ -d "$github_dir" && -z "$(ls -A "$github_dir" 2>/dev/null)" ]]; then - rmdir "$github_dir" - log_info "Removed empty .github directory" - fi + log_success "Initial commit created" } -display_next_steps() { - log_success "๐ŸŽ‰ Project setup complete!" +show_next_steps() { + echo + echo "================================================" + log_success "๐ŸŽ‰ Project Setup Complete!" echo - echo "Project: $PROJECT_NAME" - echo "iOS Deployment Target: $DEPLOYMENT_TARGET" - echo "Swift Version: $SWIFT_VERSION" - echo "Source Language: $SOURCE_LANGUAGE" - echo "Project Type: $PROJECT_TYPE" + log_info "Project: $PROJECT_NAME" + log_info "Bundle ID: ${BUNDLE_ID_ROOT}.${PROJECT_NAME}" + log_info "Deployment Target: iOS $DEPLOYMENT_TARGET+" + log_info "Swift Version: $SWIFT_VERSION" echo log_info "Next steps:" - echo " 1. [optional] Reconfigure simulators for building and testing:" - echo " ./scripts/simulator.sh list" - echo " ./scripts/simulator.sh config-tests \"iPhone 16 Pro\"" - echo " 2. [optional] run the preflight script to verify everything builds and tests with 0 errors!" - echo " ./scripts/preflight.sh" - echo " 3. Open $PROJECT_NAME.xcodeproj in Xcode" - echo " 4. Start building your app!" + echo " 1. Open ${PROJECT_NAME}.xcodeproj in Xcode" + echo " 2. Build and run: ./scripts/build.sh" + echo " 3. Run tests: ./scripts/test.sh" + echo " 4. Before committing: ./scripts/preflight.sh" echo - log_info "Available commands:" - echo " ./scripts/build.sh # Build for simulator" - echo " ./scripts/test.sh # Run tests" - echo " ./scripts/lint.sh --fix # Fix linting issues" - echo " ./scripts/format.sh --fix # Fix formatting" - echo " ./scripts/preflight.sh # Full CI check" - echo " ./scripts/simulator.sh list # List available simulators" + log_info "Development scripts available in scripts/ directory" + echo " Run any script with --help for options" echo - log_info "Need help? Check README.md for guidance." } # Main execution main() { - log_info "SwiftProjectTemplate Setup" + echo "๐Ÿš€ SwiftProjectTemplate Setup" + echo "==============================" echo parse_arguments "$@" - validate_inputs - prompt_for_missing_info - validate_inputs # Validate again after interactive input - - log_info "Setting up project with:" - log_info " Project Name: $PROJECT_NAME" - log_info " Bundle Identifier: ${BUNDLE_ID_ROOT}.$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]')" - log_info " Deployment Target: iOS $DEPLOYMENT_TARGET" - log_info " Swift Version: $SWIFT_VERSION" - log_info " Test Framework: $TEST_FRAMEWORK" - log_info " Source Language: $SOURCE_LANGUAGE" - log_info " Git Hooks: $(if $USE_GIT_HOOKS; then echo "enabled"; else echo "disabled"; fi)" - log_info " Project Type: $PROJECT_TYPE" - echo + validate_project_name + detect_project_mode install_dependencies - generate_template_files - create_project_structure - create_resources_structure + replace_placeholders + + # Mode-specific operations + if [[ "$PROJECT_MODE" == "generate" ]]; then + generate_minimal_project + elif [[ "$PROJECT_MODE" == "adopt" ]]; then + adopt_existing_project + fi + + configure_simulators generate_xcode_project - setup_default_simulators - verify_or_create_git_repository setup_git_hooks create_initial_commit - cleanup_template_files - - display_next_steps + show_next_steps } -# Run main function with all arguments -main "$@" \ No newline at end of file +main "$@" diff --git a/templates/README.md.template b/templates/README.md.template deleted file mode 100644 index fa40442..0000000 --- a/templates/README.md.template +++ /dev/null @@ -1,134 +0,0 @@ -# {{PROJECT_NAME}} - -{{PROJECT_DESCRIPTION}} - -## Badges - -![iOS](https://img.shields.io/badge/iOS-{{DEPLOYMENT_TARGET}}%2B-blue) -![Swift](https://img.shields.io/badge/Swift-{{SWIFT_VERSION}}-orange) -![Xcode](https://img.shields.io/badge/Xcode-15.0%2B-blue) -{{LICENSE_BADGE}} - -## Requirements - -- iOS {{DEPLOYMENT_TARGET}}+ -- Xcode 15.0+ -- Swift {{SWIFT_VERSION}} - -## Quick Start - -### Prerequisites - -Make sure you have [Homebrew](https://brew.sh) installed, then run: - -```bash -./scripts/setup.sh -``` - -This will: -- Install all required dependencies (SwiftLint, SwiftFormat, XcodeGen, etc.) -- Generate the Xcode project from `project.yml` -- Set up git pre-commit hooks - -### Building - -```bash -# Build for simulator -./scripts/build.sh - -# Build for device -./scripts/build.sh --device -``` - -### Testing - -```bash -# Run unit tests -./scripts/test.sh - -# Run UI tests -./scripts/test.sh --ui - -# Run all tests -./scripts/test.sh --all -``` - -### Code Quality - -```bash -# Check and fix formatting -./scripts/format.sh --fix - -# Check and fix linting issues -./scripts/lint.sh --fix - -# Run full preflight check (format, lint, test) -./scripts/preflight.sh -``` - -## Project Structure - -``` -{{PROJECT_NAME}}/ -โ”œโ”€โ”€ Models/ # Data models and Core Data entities -โ”œโ”€โ”€ Views/ # SwiftUI views and UIKit view controllers -โ”œโ”€โ”€ ViewModels/ # Business logic and view state management -โ”œโ”€โ”€ Services/ # Network, persistence, and business services -โ”œโ”€โ”€ Extensions/ # Swift extensions and utilities -โ””โ”€โ”€ Helpers/ # Helper functions and utilities -``` - -## Development Scripts - -This project includes several helper scripts in the `scripts/` directory: - -| Script | Purpose | -|--------|---------| -| `setup.sh` | One-time project setup and dependency installation | -| `build.sh` | Build the app for simulator or device | -| `test.sh` | Run unit tests, UI tests, or both | -| `lint.sh` | Run SwiftLint and optionally fix issues | -| `format.sh` | Run SwiftFormat and optionally fix issues | -| `simulator.sh` | Manage iOS simulators for testing | -| `preflight.sh` | Complete local CI check before committing | -| `ci.sh` | CI-specific build and test commands | - -Run any script with `--help` to see available options. - -## Configuration - -### XcodeGen - -This project uses [XcodeGen](https://github.com/yonaskolb/XcodeGen) to generate the Xcode project file from `project.yml`. - -To regenerate the project file: -```bash -xcodegen -``` - -### Simulators - -Simulator configuration is managed in `simulator.yml`. To update simulator settings: - -```bash -# Configure test simulators -./scripts/simulator.sh --config-tests "iPhone 16 Pro Max" - -# Configure UI test simulators -./scripts/simulator.sh --config-ui-tests "iPad Air 11-inch" -``` - -### Code Style - -- **SwiftLint**: Configuration in `.swiftlint.yml` -- **SwiftFormat**: Configuration in `.swiftformat` -- **Line Length**: 120 characters (warnings), 150 characters (errors) -- **Indentation**: 2 spaces - -{{CONTRIBUTING_SECTION}} - -{{LICENSE_SECTION}} - -## Support - -If you encounter any issues or have questions, please [open an issue]({{REPOSITORY_URL}}/issues) on GitHub. \ No newline at end of file diff --git a/templates/ci.yml.template b/templates/ci.yml.template deleted file mode 100644 index 56a2480..0000000 --- a/templates/ci.yml.template +++ /dev/null @@ -1,217 +0,0 @@ -name: iOS CI - -on: - pull_request: - branches: [main, develop] - push: - branches: [main, develop] - -env: - DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer - -jobs: - ios-ci: - name: Build, Test & Quality Checks - runs-on: macos-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "16.4" - - - name: Cache Homebrew packages - uses: actions/cache@v4 - with: - path: | - ~/Library/Caches/Homebrew - /usr/local/Homebrew - key: ${{ runner.os }}-homebrew-${{ hashFiles('**/Brewfile') }} - restore-keys: | - ${{ runner.os }}-homebrew- - - - name: Install dependencies - run: | - # Install Homebrew dependencies - brew bundle install --file=./Brewfile - - # Verify installations - echo "Installed versions:" - swiftlint --version - swiftformat --version - xcodegen --version - yq --version - - - name: Generate Xcode project - run: | - echo "Generating Xcode project..." - xcodegen generate - - PROJECT_NAME=$(yq eval '.name' project.yml) - if [ -d "${PROJECT_NAME}.xcodeproj" ]; then - echo "โœ“ Xcode project generated successfully" - else - echo "โœ— Failed to generate Xcode project" - exit 1 - fi - - - name: SwiftFormat check - run: | - echo "Checking code formatting..." - PROJECT_NAME=$(yq eval '.name' project.yml) - - if [ -d "$PROJECT_NAME" ]; then - swiftformat --lint $PROJECT_NAME - echo "โœ“ SwiftFormat check passed" - else - echo "โš  Source directory $PROJECT_NAME not found, skipping SwiftFormat" - fi - - - name: SwiftLint - run: | - echo "Running SwiftLint..." - PROJECT_NAME=$(yq eval '.name' project.yml) - - if [ -d "$PROJECT_NAME" ]; then - swiftlint lint --strict - echo "โœ“ SwiftLint passed" - else - echo "โš  Source directory $PROJECT_NAME not found, skipping SwiftLint" - fi - - - name: Build project - run: | - echo "Building project..." - PROJECT_NAME=$(yq eval '.name' project.yml) - DEPLOYMENT_TARGET=$(yq eval '.options.deploymentTarget.iOS' project.yml) - - # Determine simulator based on deployment target - if [ "$DEPLOYMENT_TARGET" != "null" ] && [ -n "$DEPLOYMENT_TARGET" ]; then - MAJOR_VERSION=$(echo $DEPLOYMENT_TARGET | cut -d'.' -f1) - if [ "$MAJOR_VERSION" -ge "17" ]; then - SIMULATOR_NAME="iPhone 16 Pro" - else - SIMULATOR_NAME="iPhone 15 Pro" - fi - else - SIMULATOR_NAME="iPhone 16 Pro" - fi - - echo "Building for $SIMULATOR_NAME..." - - xcodebuild build \ - -project "${PROJECT_NAME}.xcodeproj" \ - -scheme "$PROJECT_NAME" \ - -destination "platform=iOS Simulator,name=$SIMULATOR_NAME" \ - -configuration Debug \ - ONLY_ACTIVE_ARCH=YES \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO - - echo "โœ“ Build completed successfully" - - - name: Run unit tests - run: | - PROJECT_NAME=$(yq eval '.name' project.yml) - - # Check if test target exists - HAS_TESTS=$(yq eval ".targets | has(\"${PROJECT_NAME}Tests\")" project.yml) - - if [ "$HAS_TESTS" = "true" ]; then - echo "Running unit tests..." - - DEPLOYMENT_TARGET=$(yq eval '.options.deploymentTarget.iOS' project.yml) - if [ "$DEPLOYMENT_TARGET" != "null" ] && [ -n "$DEPLOYMENT_TARGET" ]; then - MAJOR_VERSION=$(echo $DEPLOYMENT_TARGET | cut -d'.' -f1) - if [ "$MAJOR_VERSION" -ge "17" ]; then - SIMULATOR_NAME="iPhone 16 Pro" - else - SIMULATOR_NAME="iPhone 15 Pro" - fi - else - SIMULATOR_NAME="iPhone 16 Pro" - fi - - xcodebuild test \ - -project "${PROJECT_NAME}.xcodeproj" \ - -scheme "$PROJECT_NAME" \ - -destination "platform=iOS Simulator,name=$SIMULATOR_NAME" \ - -configuration Debug \ - -enableCodeCoverage YES \ - ONLY_ACTIVE_ARCH=YES \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO - - echo "โœ“ Unit tests completed" - else - echo "โš  No unit test target found, skipping tests" - fi - - - name: Validate scripts - run: | - echo "Validating helper scripts..." - - # Check that all scripts are executable - find scripts -name "*.sh" -type f | while read script; do - if [ -x "$script" ]; then - echo "โœ“ $script is executable" - else - echo "โœ— $script is not executable" - exit 1 - fi - done - - # Check script syntax - find scripts -name "*.sh" -type f | while read script; do - if bash -n "$script"; then - echo "โœ“ $script syntax is valid" - else - echo "โœ— $script has syntax errors" - exit 1 - fi - done - - echo "โœ“ All scripts validated" - - - name: Check configuration files - run: | - echo "Validating configuration files..." - - # Check .swiftlint.yml if it exists - if [ -f ".swiftlint.yml" ]; then - yq eval '.' .swiftlint.yml > /dev/null - echo "โœ“ .swiftlint.yml is valid" - fi - - # Check .swiftformat if it exists - if [ -f ".swiftformat" ]; then - echo "โœ“ .swiftformat found" - fi - - # Check simulator.yml if it exists - if [ -f "simulator.yml" ]; then - yq eval '.' simulator.yml > /dev/null - echo "โœ“ simulator.yml is valid" - fi - - # Check Brewfile - if [ -f "Brewfile" ]; then - echo "โœ“ Brewfile found" - fi - - echo "โœ“ Configuration files validated" - - - name: CI Summary - run: | - echo "๐ŸŽ‰ CI Complete!" - echo "" - echo "โœ… Code formatting validated" - echo "โœ… Code quality checks passed" - echo "โœ… Build completed successfully" - echo "โœ… Tests executed" - echo "โœ… Configuration validated" - echo "" - echo "Ready for review! ๐Ÿš€" \ No newline at end of file