From 88ff1bb563696f452b6bda71f8f4f6cb631f3a1d Mon Sep 17 00:00:00 2001 From: crimson-knight Date: Tue, 11 Aug 2026 16:07:28 -0400 Subject: [PATCH 1/6] Ship persistence in the default V2 web app --- .github/workflows/build.yml | 2 +- .github/workflows/platform-compile.yml | 4 +- README.md | 65 +++---- RELEASE_NOTES_V2.0.4.md | 39 +++++ docs/BETA_WEB_APP.md | 55 ++++-- docs/GENERATOR_SUPPORT.md | 11 +- docs/RELEASE_CHECKLIST.md | 17 +- scripts/check_beta_contract.sh | 14 +- scripts/smoke_generated_web.ps1 | 46 ++++- scripts/smoke_generated_web.sh | 81 ++++++++- shard.lock | 2 +- shard.yml | 5 +- spec/commands/generate_command_spec.cr | 37 ++++ spec/commands/new_command_spec.cr | 20 ++- src/amber_cli.cr | 2 +- src/amber_cli/commands/database.cr | 107 +++++------- src/amber_cli/commands/generate.cr | 165 +++++++++++++----- src/amber_cli/commands/new.cr | 111 +++++++++++- src/amber_cli/commands/setup_lsp.cr | 2 +- src/amber_cli/config.cr | 4 +- src/amber_cli/documentation.cr | 7 +- src/amber_cli/templates/app/.amber.yml.ecr | 2 +- src/amber_cli/templates/app/README.md.ecr | 16 +- .../templates/app/config/database.cr.ecr | 19 ++ src/amber_cli/templates/app/db/seeds.cr.ecr | 4 +- src/amber_cli/templates/app/shard.yml.ecr | 19 +- 26 files changed, 629 insertions(+), 227 deletions(-) create mode 100644 RELEASE_NOTES_V2.0.4.md create mode 100644 src/amber_cli/templates/app/config/database.cr.ecr diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e50fdfe..8118b11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,7 +83,7 @@ jobs: ! otool -L amber amber-lsp | grep -F 'openssl@1.1' - name: Smoke test generated Amber V2 web app - run: scripts/smoke_generated_web.sh ./amber + run: scripts/smoke_generated_web.sh ./amber bd8d9428f45cb836cb6f26bbc358721a9f0ca3b3 - name: Upload build artifacts uses: actions/upload-artifact@v7 diff --git a/.github/workflows/platform-compile.yml b/.github/workflows/platform-compile.yml index 7a782d2..f22fab5 100644 --- a/.github/workflows/platform-compile.yml +++ b/.github/workflows/platform-compile.yml @@ -27,7 +27,7 @@ jobs: run: crystal build src/amber_cli.cr --no-debug -o amber - name: Compile generated Amber V2 web app - run: scripts/smoke_generated_web.sh ./amber + run: scripts/smoke_generated_web.sh ./amber bd8d9428f45cb836cb6f26bbc358721a9f0ca3b3 windows-x86-64: name: Windows x86_64 generated web app (merged framework fix) @@ -57,4 +57,4 @@ jobs: - name: Compile generated Amber V2 web app shell: pwsh - run: scripts/smoke_generated_web.ps1 ./amber.exe d044ea05e884b3247fd2af00859bde272aab676a + run: scripts/smoke_generated_web.ps1 ./amber.exe bd8d9428f45cb836cb6f26bbc358721a9f0ca3b3 diff --git a/README.md b/README.md index 89bd95f..7e40a55 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ [![GitHub release](https://img.shields.io/github/release/amberframework/amber_cli.svg)](https://github.com/amberframework/amber_cli/releases) [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://amberframework.github.io/amber_cli/) -Amber CLI is the standalone command-line companion for Amber V2. CLI `2.0.3` -creates the supported Amber `2.0.0-beta.2` ECR web application and includes +Amber CLI is the standalone command-line companion for Amber V2. CLI `2.0.4` +creates the supported Amber `2.0.0-beta.3` ECR web application and includes development, generator, database, and LSP tooling. Amber V2 is a beta. The release-gated path is a web application on Apple Silicon macOS or x86_64 Linux. Linux ARM64 generated-app compilation is checked in CI; its first direct archive will ship with the next CLI release. See [Generator support](docs/GENERATOR_SUPPORT.md) -before relying on persistence, authentication, API-resource, or native output. +before relying on authentication, API-resource, or native output. ## Install @@ -31,13 +31,12 @@ the `amber_cli` formula. ### Direct release archive -CLI `2.0.3` publishes `darwin-arm64` for Apple Silicon macOS and -`linux-x86_64` for x86_64 Linux. The release workflow now also builds -`linux-arm64`; that archive becomes available with the next published CLI -version. +CLI `2.0.4` publishes `darwin-arm64`, `linux-x86_64`, and `linux-arm64` +archives. Windows x86-64 is compiled in CI but does not yet have a release +archive. ```bash -version=v2.0.3 +version=v2.0.4 platform=darwin-arm64 asset="amber_cli-${platform}.tar.gz" @@ -52,27 +51,6 @@ amber --version On Linux, use `sha256sum -c` for the checksum. Prefix only the `install` command with `sudo` if `/usr/local/bin` is not writable. -### Linux ARM64 source install for 2.0.3 - -CLI `2.0.3` does not contain a `linux-arm64` archive. On an ARM64 Linux host, -build that tagged source instead of downloading the x86_64 binary: - -```bash -sudo apt-get update -sudo apt-get install -y libsqlite3-dev -git clone --branch v2.0.3 --depth 1 https://github.com/amberframework/amber_cli.git -cd amber_cli -shards install --production -crystal build src/amber_cli.cr -o amber --release -crystal build src/amber_lsp.cr -o amber-lsp --release -sudo install -m 0755 amber amber-lsp /usr/local/bin/ -amber --version -``` - -Run the clone command from a directory where the temporary `amber_cli/` -checkout can be created. The next CLI release workflow now builds and smoke -tests a native `linux-arm64` archive on a GitHub-hosted ARM64 Linux runner. - ## Create and verify a web app ```bash @@ -89,15 +67,30 @@ workflow needs to run `shards install` later. Open and The web template is deliberately small: -- Amber from `amberframework/amber`, pinned to `2.0.0-beta.2` +- Amber from `amberframework/amber`, pinned to `2.0.0-beta.3` - ECR views (Slang and Kilt are not supported in Amber V2) - typed development, test, and production YAML - branded homepage, controller spec, and static CSS/JavaScript - a browser-native import map with a local JavaScript module entry point -- no ORM or database driver by default +- Grant ORM, Micrate-powered migration commands, and the selected database driver +- SQLite by default, so the first persisted feature needs no database server + +The `-d pg|mysql|sqlite` option selects the generated driver, connection, and +development/test URLs. SQLite is the default; PostgreSQL and MySQL expect their +respective local servers or a `DATABASE_URL`. + +Create the first complete resource and its database table: + +```bash +amber generate scaffold Pet name:string:required species:string:required adopted:bool +amber database migrate +amber watch +``` -The `-d pg|mysql|sqlite` option records metadata and suggested URLs for future -persistence tooling. It does not add an ORM or driver to the core web app. +The generator writes the Grant model to `src/models/pet.cr`, the request schema +to `src/schemas/pet_schema.cr`, the controller to +`src/controllers/pet_controller.cr`, ECR views to `src/views/pet/`, a Micrate +SQL migration to `db/migrations/`, and the resource route to `config/routes.cr`. ## Commands @@ -107,8 +100,8 @@ persistence tooling. It does not add an ORM or driver to the core web app. | `amber watch` | Supported | Rebuild and restart during development | | `amber routes` | Supported | Inspect application routes | | `amber pipelines` | Supported | Inspect configured pipelines | -| `amber generate` | Mixed | Core generators supported; dependency-backed generators preview | -| `amber database` | Preview for new apps | Requires an explicitly configured persistence stack | +| `amber generate` | Mixed | Model, scaffold, migration, and core generators supported; auth and API preview | +| `amber database` | Supported | Apply, roll back, inspect, redo, and seed the generated database | | `amber new APP --type native` | Preview | Not part of the beta platform guarantee | | `amber setup:lsp` | Available | Configure the bundled diagnostics LSP | @@ -143,7 +136,7 @@ The release archive includes `amber-lsp`. From an Amber project: amber setup:lsp ``` -See the [LSP setup guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.2/docs/guides/lsp-setup.md). +See the [LSP setup guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/guides/lsp-setup.md). ## Contributing diff --git a/RELEASE_NOTES_V2.0.4.md b/RELEASE_NOTES_V2.0.4.md new file mode 100644 index 0000000..d0106a6 --- /dev/null +++ b/RELEASE_NOTES_V2.0.4.md @@ -0,0 +1,39 @@ +# Amber CLI 2.0.4 + +Amber CLI 2.0.4 makes persistence part of the supported Amber V2 web-app path. +A new application includes Grant ORM, SQLite, typed database settings, and +Micrate-powered database commands by default. + +## Start a database-backed app + +```bash +amber new pet_tracker --type web +cd pet_tracker +amber generate scaffold Pet name:string:required species:string:required adopted:bool +amber database migrate +amber watch +``` + +Open . The generated resource includes a Grant +model, request schema, HTML CRUD controller, ECR views, specs, resource route, +and reversible SQL migration. SQLite keeps the first run self-contained; pass +`-d pg` or `-d mysql` to `amber new` for a server database. + +## What changed + +- Added Grant and the selected database driver to every generated web app. +- Made SQLite the default database and connected development, test, and + production settings through `config/database.cr`. +- Embedded Micrate in `amber database` for migrations, status, rollback, redo, + reset, and seeding. +- Promoted model, scaffold, and migration generation to the supported beta path. +- Corrected scaffold forms, optional fields, resource routes, and form-body + handling so generated create and update requests persist successfully. +- Added a release smoke test that generates a Pet resource, migrates both test + and development databases, runs its specs, boots the app, creates a Pet, and + updates it through the generated HTML forms. +- Added a native Linux ARM64 release artifact and a Windows x86-64 generated-app + compile gate. + +Amber V2 native applications, generated authentication, and generated APIs +remain preview surfaces in this release. diff --git a/docs/BETA_WEB_APP.md b/docs/BETA_WEB_APP.md index 41d21f9..61efa02 100644 --- a/docs/BETA_WEB_APP.md +++ b/docs/BETA_WEB_APP.md @@ -1,7 +1,8 @@ # Amber V2 Beta Web App -This guide is the consumer smoke test for Amber CLI `2.0.3` and Amber -`2.0.0-beta.2`. It is expected to pass on Apple Silicon macOS and x86_64 Linux. +This guide is the consumer smoke test for Amber CLI `2.0.4` and Amber +`2.0.0-beta.3`. It is expected to pass on Apple Silicon macOS, x86_64 Linux, +and ARM64 Linux. Windows x86-64 compilation is checked in CI. ## 1. Verify the toolchain @@ -11,7 +12,7 @@ shards --version amber --version ``` -Crystal must be at least 1.20 and earlier than 2.0. Amber CLI must be 2.0.3 or +Crystal must be at least 1.20 and earlier than 2.0. Amber CLI must be 2.0.4 or newer. ## 2. Generate the web app @@ -26,12 +27,13 @@ contract before continuing: ```bash grep -A2 'amber:' shard.yml -grep 'template:' .amber.yml +grep -E '^(template|database|model):' .amber.yml ``` -The dependency must be `amberframework/amber` version `2.0.0-beta.2`; the -template must be `ecr`. A newly generated app must not contain a personal fork, -Grant, Gemma, Slang, or all three database drivers. +The dependency must be `amberframework/amber` version `2.0.0-beta.3`; the +template must be `ecr`, the database must be `sqlite`, and the model layer must +be `grant`. `shard.yml` must include Grant and `crystal-sqlite3`, but it must not +include Gemma, Slang, PostgreSQL, or MySQL unless you selected that database. The first page carries a compact version of Amber's V2 visual language: warm paper colors, a CSS faceted-crystal mark, editorial heading scale, status chips, @@ -47,7 +49,8 @@ crystal spec crystal build src/amber_beta_smoke.cr -o bin/amber_beta_smoke ``` -No running database is required for this core application. +No database server is required: the default development and test databases are +local SQLite files under `db/`. ## 4. Start and probe it @@ -71,18 +74,44 @@ curl --fail http://127.0.0.1:3000/ | grep 'Your new idea' curl --fail http://127.0.0.1:3000/css/app.css | grep -- '--amber-accent: #e96918' ``` -## 5. Try core generators +## 5. Generate and migrate a real resource + +```bash +amber generate scaffold Pet name:string:required species:string:required adopted:bool +amber database migrate +AMBER_ENV=test amber database migrate +amber database status +crystal spec +``` + +That one scaffold command creates: + +- `src/models/pet.cr` — the Grant model +- `src/schemas/pet_schema.cr` — typed request validation +- `src/controllers/pet_controller.cr` — HTML CRUD actions +- `src/views/pet/*.ecr` — index, show, new, edit, and shared form views +- `db/migrations/*_create_pets.sql` — Micrate Up/Down SQL +- `spec/models/pet_spec.cr` and `spec/controllers/pet_controller_spec.cr` +- `resources "/pets", PetController` inside `config/routes.cr` + +Restart `amber watch`, open , and create a +record. The create form posts to `/pets`; the edit form sends `_method=PATCH` +to `/pets/:id`. + +## 6. Try the other supported generators ```bash amber generate controller Posts index show amber generate schema Post title:string:required body:text +amber generate model Person name:string:required amber generate job PublishPost --queue=default amber generate mailer Digest --actions=weekly amber generate channel Updates --topics=posts amber generate migration CreatePosts ``` -Review generated files before adding them to an application. Run `crystal tool -format --check` and `crystal spec` after generation. These six core generator -paths run in the release smoke test on both supported platforms. See -[Generator support](GENERATOR_SUPPORT.md) for dependency-backed preview types. +Review generated files before adding them to an application. Run +`amber database migrate`, `crystal tool format --check src spec`, and +`crystal spec` after generation. The release smoke test generates, migrates, +compiles, and exercises a Pet create/update flow. See +[Generator support](GENERATOR_SUPPORT.md) for the remaining preview surfaces. diff --git a/docs/GENERATOR_SUPPORT.md b/docs/GENERATOR_SUPPORT.md index 021bd2f..8c66f27 100644 --- a/docs/GENERATOR_SUPPORT.md +++ b/docs/GENERATOR_SUPPORT.md @@ -10,17 +10,16 @@ without adding an unreleased ecosystem dependency. | `generate job` | Supported | Uses Amber's built-in job system | | `generate mailer` | Supported | Uses Amber's built-in mailer API | | `generate channel` | Supported | Uses Amber WebSockets | -| `generate migration` | Supported output | Produces SQL migration files; applying them requires database tooling | -| `generate model` | Preview | Currently emits Grant-based output; Grant is not in the core app | -| `generate scaffold` | Preview | Includes Grant persistence and resource output | +| `generate migration` | Supported | Produces Micrate Up/Down SQL in `db/migrations/`; apply it with `amber database migrate` | +| `generate model` | Supported | Produces a Grant model, model spec, and migration | +| `generate scaffold` | Supported | Produces a Grant model, request schema, HTML CRUD controller, ECR views, specs, migration, and route | | `generate api` | Preview | Includes a persistence-backed model | | `generate auth` | Preview | Requires a compatible persistence/auth stack | | `new --type native` | Preview | Has a separate multi-platform dependency and validation matrix | Preview does not mean removed. It means the CLI may generate the files, but the -Amber beta release does not promise that a clean web application will compile -them without additional work. The CLI prints a warning before generating a -dependency-backed preview surface. +Amber beta release does not promise production-ready behavior without review. +The CLI prints a warning before generating a preview surface. Amber V2 supports ECR only. The CLI ignores legacy Slang settings for new output; migrate old `.slang` files before using V2 generators. diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index f63746e..e89fcbc 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -6,19 +6,19 @@ Homebrew formula pins CLI archives and checksums. ## 1. Framework prerelease 1. Confirm `shard.yml`, `src/amber/version.cr`, and the changelog all say - `2.0.0-beta.2`. + `2.0.0-beta.3`. 2. Run framework specs and formatting on macOS and Linux. -3. Tag the reviewed `v2-dev` commit as `v2.0.0-beta.2`. +3. Tag the reviewed `v2-dev` commit as `v2.0.0-beta.3`. 4. Publish it as a GitHub prerelease with migration and support-matrix links. ## 2. CLI release -1. Confirm `shard.yml` and `AmberCLI::VERSION` both say `2.0.3`. +1. Confirm `shard.yml` and `AmberCLI::VERSION` both say `2.0.4`. 2. Generate a web app and verify it pins the framework prerelease. 3. On both supported platforms, install shards, run app specs, build the app, start it, and request `/` plus `/css/app.css`. 4. On macOS, reject any binary linked to `openssl@1.1`. -5. Tag `v2.0.3`, publish the release, and wait for both archives and checksum +5. Tag `v2.0.4`, publish the release, and wait for all archives and checksum files to upload. ## 3. Homebrew @@ -33,9 +33,10 @@ Homebrew formula pins CLI archives and checksums. 1. Publish framework, CLI, and website guides with the same command names, versions, platform matrix, and preview labels. 2. Check that no onboarding page uses a hyphenated tap or formula name, a - separate tap step, a personal fork, a moving `v2-dev` dependency, or Slang - for a new V2 app. + separate tap step, a personal Amber fork, a moving `v2-dev` dependency, or + Slang for a new V2 app. 3. Verify all release/download links and commands from a clean shell. -Do not mark the beta complete if either supported platform cannot install the -CLI and build the generated web application. +Do not mark the beta complete if macOS, x86-64 Linux, or ARM64 Linux cannot +install the CLI and build the generated web application, or if the Windows +generated-app compile gate fails. diff --git a/scripts/check_beta_contract.sh b/scripts/check_beta_contract.sh index dab6de9..31a2720 100755 --- a/scripts/check_beta_contract.sh +++ b/scripts/check_beta_contract.sh @@ -3,12 +3,17 @@ set -euo pipefail shard_version="$(awk '/^version:/ { print $2; exit }' shard.yml)" cli_version="$(sed -n 's/.*VERSION = "\([^"]*\)".*/\1/p' src/amber_cli.cr | head -1)" -test "$shard_version" = "2.0.3" +test "$shard_version" = "2.0.4" test "$cli_version" = "$shard_version" grep -F 'github: amberframework/amber' src/amber_cli/commands/new.cr -grep -F 'version: 2.0.0-beta.2' src/amber_cli/commands/new.cr +grep -F 'version: 2.0.0-beta.3' src/amber_cli/commands/new.cr grep -F 'template: ecr' src/amber_cli/commands/new.cr +grep -F 'model: grant' src/amber_cli/commands/new.cr +grep -F 'database: #{database}' src/amber_cli/commands/new.cr +grep -F 'github: crimson-knight/grant' src/amber_cli/commands/new.cr +grep -F 'github: amberframework/micrate' shard.yml +test -s src/amber_cli/templates/app/config/database.cr.ecr grep -F 'Your new idea' src/amber_cli/commands/new.cr grep -F -- '--amber-accent: #e96918' src/amber_cli/commands/new.cr grep -F 'Your new idea' src/amber_cli/templates/app/src/views/home/index.ecr.ecr @@ -19,6 +24,7 @@ grep -F 'brew install amberframework/amber_cli/amber_cli' README.md files=( README.md RELEASE_NOTES_V2.0.3.md + RELEASE_NOTES_V2.0.4.md RELEASE_SETUP.md .github/ISSUE_TEMPLATE/release-checklist.md docs/*.md @@ -29,8 +35,8 @@ if grep -Ein 'amberframework/amber-cli|brew tap amberframework/amber_cli|brew in exit 1 fi -if grep -Eir 'crimson-knight/(amber|grant|gemma)' src/amber_cli/templates/app src/amber_cli/commands/new.cr; then - echo "supported web template contains a personal dependency" >&2 +if grep -Eir 'crimson-knight/(amber|gemma)' src/amber_cli/templates/app src/amber_cli/commands/new.cr; then + echo "supported web template contains a personal Amber or Gemma dependency" >&2 exit 1 fi diff --git a/scripts/smoke_generated_web.ps1 b/scripts/smoke_generated_web.ps1 index 3f3820f..12ce9df 100644 --- a/scripts/smoke_generated_web.ps1 +++ b/scripts/smoke_generated_web.ps1 @@ -32,23 +32,57 @@ New-Item -ItemType Directory -Force $smokeRoot | Out-Null Invoke-Checked -Command $cliPath -Arguments @("new", $appPath, "--type", "web", "--no-deps") +$shardPath = Join-Path $appPath "shard.yml" +$amberConfigPath = Join-Path $appPath ".amber.yml" +$manifest = [System.IO.File]::ReadAllText($shardPath) +$amberConfig = [System.IO.File]::ReadAllText($amberConfigPath) + +if (-not $manifest.Contains("github: crimson-knight/grant")) { + throw "Generated shard.yml does not include Grant" +} +if (-not $manifest.Contains("github: crystal-lang/crystal-sqlite3")) { + throw "Generated shard.yml does not include SQLite" +} +if (-not $amberConfig.Contains("database: sqlite") -or -not $amberConfig.Contains("model: grant")) { + throw "Generated .amber.yml does not select SQLite and Grant" +} + if ($FrameworkCommit) { - $shardPath = Join-Path $appPath "shard.yml" - $manifest = [System.IO.File]::ReadAllText($shardPath) - $releasedFramework = " version: 2.0.0-beta.2" + $releasedFramework = " version: 2.0.0-beta.3" if (-not $manifest.Contains($releasedFramework)) { throw "Generated shard.yml does not contain the expected Amber beta pin" } $manifest = $manifest.Replace( - $releasedFramework, - " commit: $FrameworkCommit" - ) + " github: amberframework/amber", + " github: crimson-knight/amber" + ).Replace( + $releasedFramework, + " commit: $FrameworkCommit" + ) [System.IO.File]::WriteAllText($shardPath, $manifest) } Push-Location $appPath try { Invoke-Checked -Command "shards" -Arguments @("install") + Invoke-Checked -Command "crystal" -Arguments @("spec") + Invoke-Checked -Command $cliPath -Arguments @( + "generate", + "scaffold", + "Pet", + "name:string:required", + "species:string:required", + "adopted:bool" + ) + + $previousAmberEnv = $env:AMBER_ENV + try { + $env:AMBER_ENV = "test" + Invoke-Checked -Command $cliPath -Arguments @("database", "migrate") + } finally { + $env:AMBER_ENV = $previousAmberEnv + } + Invoke-Checked -Command "crystal" -Arguments @("spec") New-Item -ItemType Directory -Force bin | Out-Null Invoke-Checked -Command "crystal" -Arguments @( diff --git a/scripts/smoke_generated_web.sh b/scripts/smoke_generated_web.sh index fa73765..4122ca8 100755 --- a/scripts/smoke_generated_web.sh +++ b/scripts/smoke_generated_web.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash set -euo pipefail -if [[ $# -ne 1 ]]; then - echo "usage: $0 /path/to/amber" >&2 +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "usage: $0 /path/to/amber [framework-commit]" >&2 exit 64 fi cli_path="$1" +framework_commit="${2:-}" if [[ "$cli_path" != /* ]]; then cli_path="$(cd "$(dirname "$cli_path")" && pwd)/$(basename "$cli_path")" fi @@ -24,28 +25,53 @@ cleanup() { } trap cleanup EXIT -"$cli_path" --version | grep -F "Amber CLI v2.0.3" +"$cli_path" --version | grep -E "Amber CLI v2\.0\.[0-9]+" "$cli_path" new "$app_path" --type web --no-deps grep -F "github: amberframework/amber" "$app_path/shard.yml" -grep -F "version: 2.0.0-beta.2" "$app_path/shard.yml" +grep -F "version: 2.0.0-beta.3" "$app_path/shard.yml" +grep -F "github: crimson-knight/grant" "$app_path/shard.yml" +grep -F "github: crystal-lang/crystal-sqlite3" "$app_path/shard.yml" grep -F "template: ecr" "$app_path/.amber.yml" -if grep -Eiq 'crimson-knight|grant:|gemma:|slang' "$app_path/shard.yml" "$app_path/.amber.yml"; then +grep -F "database: sqlite" "$app_path/.amber.yml" +grep -F "model: grant" "$app_path/.amber.yml" +test -s "$app_path/config/database.cr" + +if grep -Eiq 'gemma:|slang' "$app_path/shard.yml" "$app_path/.amber.yml"; then echo "generated app contains an unsupported beta dependency or template" >&2 exit 1 fi +if [[ -n "$framework_commit" ]]; then + sed -i.bak "s/github: amberframework\/amber/github: crimson-knight\/amber/" "$app_path/shard.yml" + sed -i.bak -E "s/ version: 2\.0\.0-beta\.[0-9]+/ commit: ${framework_commit}/" "$app_path/shard.yml" + rm -f "$app_path/shard.yml.bak" +fi + cd "$app_path" -shards install +env GIT_CONFIG_COUNT=1 \ + GIT_CONFIG_KEY_0=core.hooksPath \ + GIT_CONFIG_VALUE_0=/dev/null \ + shards install crystal spec -"$cli_path" generate controller Posts index show -"$cli_path" generate schema Post title:string:required body:text +"$cli_path" generate scaffold Pet name:string:required species:string:required adopted:bool "$cli_path" generate job PublishPost --queue=default "$cli_path" generate mailer Digest --actions=weekly "$cli_path" generate channel Updates --topics=posts "$cli_path" generate migration CreatePosts +grep -F "class Pet < Grant::Base" src/models/pet.cr +grep -F -- "-- +micrate Up" db/migrations/*_create_pets.sql +grep -F -- "-- +micrate Down" db/migrations/*_create_pets.sql +grep -F 'resources "/pets", PetController' config/routes.cr +grep -F 'render(partial: "_form.ecr")' src/views/pet/new.ecr +grep -F 'hidden_field("_method", "PATCH")' src/views/pet/_form.ecr + +AMBER_ENV=test "$cli_path" database migrate +AMBER_ENV=test "$cli_path" database status | grep -F "Applied At" +"$cli_path" database migrate + if find src spec -iname '*slang*' -print | grep -q .; then echo "a core generator emitted a Slang file" >&2 exit 1 @@ -71,6 +97,45 @@ curl --fail --silent http://127.0.0.1:3210/ | grep -F '"app":"/js/app.js"' curl --fail --silent http://127.0.0.1:3210/css/app.css | grep -F "Amber V2 starter styles" curl --fail --silent http://127.0.0.1:3210/css/app.css | grep -F -- "--amber-accent: #e96918" +new_page="$smoke_root/new-pet.html" +edit_page="$smoke_root/edit-pet.html" +cookie_jar="$smoke_root/cookies.txt" +response_headers="$smoke_root/response-headers.txt" + +curl --fail --silent --cookie-jar "$cookie_jar" http://127.0.0.1:3210/pets/new >"$new_page" +test "$(grep -c '' "$new_page")" -eq 1 +grep -F 'form action="/pets" method="POST"' "$new_page" +grep -F 'name="adopted"' "$new_page" | grep -F 'value="true"' +csrf_token="$(sed -n 's/.*name="_csrf" value="\([^"]*\)".*/\1/p' "$new_page" | head -1)" +test -n "$csrf_token" + +curl --fail --silent --output /dev/null --dump-header "$response_headers" \ + --cookie "$cookie_jar" --request POST \ + --data-urlencode "_csrf=$csrf_token" \ + --data-urlencode "name=Ruby" \ + --data-urlencode "species=Dog" \ + http://127.0.0.1:3210/pets +grep -F "Location: /pets/1" "$response_headers" +curl --fail --silent http://127.0.0.1:3210/pets/1 | grep -F "Ruby" + +curl --fail --silent --cookie "$cookie_jar" --cookie-jar "$cookie_jar" \ + http://127.0.0.1:3210/pets/1/edit >"$edit_page" +grep -F 'form action="/pets/1" method="POST"' "$edit_page" +grep -F 'name="_method"' "$edit_page" | grep -F 'value="PATCH"' +csrf_token="$(sed -n 's/.*name="_csrf" value="\([^"]*\)".*/\1/p' "$edit_page" | head -1)" +test -n "$csrf_token" + +curl --fail --silent --output /dev/null --dump-header "$response_headers" \ + --cookie "$cookie_jar" --request POST \ + --data-urlencode "_csrf=$csrf_token" \ + --data-urlencode "_method=PATCH" \ + --data-urlencode "name=Ruby II" \ + --data-urlencode "species=Dog" \ + --data-urlencode "adopted=true" \ + http://127.0.0.1:3210/pets/1 +grep -F "Location: /pets/1" "$response_headers" +curl --fail --silent http://127.0.0.1:3210/pets/1 | grep -F "Ruby II" + kill "$server_pid" wait "$server_pid" || true server_pid="" diff --git a/shard.lock b/shard.lock index b49cf0e..896fb5b 100644 --- a/shard.lock +++ b/shard.lock @@ -26,7 +26,7 @@ shards: micrate: git: https://github.com/amberframework/micrate.git - version: 0.15.1+git.commit.647fac0490a522956fef305c720e8ebf1422a87d + version: 0.16.0-beta.1+git.commit.56afd5607cc591776980f3d3762495c037b98d98 mysql: git: https://github.com/crystal-lang/crystal-mysql.git diff --git a/shard.yml b/shard.yml index ad7c79d..8632944 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: amber_cli -version: 2.0.3 +version: 2.0.4 authors: - crimson-knight @@ -23,8 +23,7 @@ dependencies: micrate: github: amberframework/micrate - # version: ~> 0.15.0 - branch: master + branch: agent/crystal-1.21-runner pg: github: will/crystal-pg diff --git a/spec/commands/generate_command_spec.cr b/spec/commands/generate_command_spec.cr index 58d6f70..3616899 100644 --- a/spec/commands/generate_command_spec.cr +++ b/spec/commands/generate_command_spec.cr @@ -28,4 +28,41 @@ describe AmberCLI::Commands::GenerateCommand do File.exists?("src/views/digest_mailer/weekly.ecr").should be_true end end + + it "generates a database-backed ECR scaffold that is ready to migrate" do + SpecHelper.within_temp_directory do + File.write(".amber.yml", "template: ecr\ndatabase: sqlite\nmodel: grant\n") + Dir.mkdir_p("config") + File.write("config/routes.cr", <<-CRYSTAL) + Amber::Server.configure do |app| + routes :web do + end + end + CRYSTAL + + command = AmberCLI::Commands::GenerateCommand.new("generate") + command.parse_and_execute([ + "scaffold", + "Pet", + "name:string:required", + "species:string:required", + "adopted:bool", + ]) + + File.read("src/models/pet.cr").should contain("class Pet < Grant::Base") + File.read("src/models/pet.cr").should contain("column adopted : Bool?") + + migration = Dir.glob("db/migrations/*_create_pets.sql").first + File.read(migration).should contain("-- +micrate Up") + File.read(migration).should contain("CREATE TABLE IF NOT EXISTS pets") + File.read(migration).should contain("-- +micrate Down") + + File.read("config/routes.cr").should contain(%(resources "/pets", PetController)) + File.read("src/controllers/pet_controller.cr").should contain("schema.adopted") + File.read("src/controllers/pet_controller.cr").should_not contain("schema.adopted.not_nil!") + File.read("src/views/pet/new.ecr").should contain(%(render(partial: "_form.ecr"))) + File.read("src/views/pet/_form.ecr").should contain(%(hidden_field("_method", "PATCH"))) + File.read("src/views/pet/_form.ecr").should contain(%(checkbox("adopted", checked: @pet.adopted? || false, value: "true"))) + end + end end diff --git a/spec/commands/new_command_spec.cr b/spec/commands/new_command_spec.cr index d93fe15..fdeb64b 100644 --- a/spec/commands/new_command_spec.cr +++ b/spec/commands/new_command_spec.cr @@ -86,11 +86,21 @@ describe AmberCLI::Commands::NewCommand do shard = File.read(File.join(destination, "shard.yml")) shard.should contain("github: amberframework/amber") - shard.should contain("version: 2.0.0-beta.2") - shard.should_not contain("crimson-knight") - shard.should_not contain("grant:") + shard.should contain("version: 2.0.0-beta.3") + shard.should contain("grant:") + shard.should contain("github: crimson-knight/grant") + shard.should contain("github: crystal-lang/crystal-sqlite3") shard.should_not contain("slang") + amber_config = YAML.parse(File.read(File.join(destination, ".amber.yml"))) + amber_config["database"].as_s.should eq("sqlite") + amber_config["model"].as_s.should eq("grant") + + database_config = File.read(File.join(destination, "config/database.cr")) + database_config.should contain(%(require "grant/adapter/sqlite")) + database_config.should contain(%(name: "primary")) + database_config.should contain(%(ENV["DATABASE_URL"]? || Amber.settings.database_url)) + config = YAML.parse(File.read(File.join(destination, "config/environments/development.yml"))) config["server"]["port"].as_i.should eq(3000) config["database"]["url"].as_s.should contain("sqlite3:") @@ -123,6 +133,10 @@ describe AmberCLI::Commands::NewCommand do File.exists?(File.join(destination, "src/views/home/index.ecr")).should be_true File.exists?(File.join(destination, "src/views/home/index.slang")).should be_false + + readme = File.read(File.join(destination, "README.md")) + readme.should contain("amber generate scaffold Pet") + readme.should contain("amber database migrate") end end end diff --git a/src/amber_cli.cr b/src/amber_cli.cr index 38ff9a6..3aa549d 100644 --- a/src/amber_cli.cr +++ b/src/amber_cli.cr @@ -38,7 +38,7 @@ end Log.builder.bind "*", :info, backend module AmberCLI - VERSION = "2.0.3" + VERSION = "2.0.4" def self.run(args = ARGV) if args.empty? diff --git a/src/amber_cli/commands/database.cr b/src/amber_cli/commands/database.cr index 4813718..f41b19f 100644 --- a/src/amber_cli/commands/database.cr +++ b/src/amber_cli/commands/database.cr @@ -2,6 +2,7 @@ require "micrate" require "pg" require "mysql" require "sqlite3" +require "yaml" require "../core/base_command" require "../helpers/helpers" require "../helpers/migration" @@ -87,35 +88,34 @@ module AmberCLI::Commands end def execute - connect_to_database if remaining_arguments.empty? process_commands(remaining_arguments) rescue e : DB::ConnectionRefused - exit! "Connection unsuccessful: #{Micrate::DB.connection_url || "unknown"}", error: true + exit! "Connection unsuccessful: #{database_url}", error: true rescue e : Exception exit! e.message || "Unknown error", error: true end private def process_commands(commands) commands.each do |command| - Micrate::DB.connection_url = database_url + url = database_url case command when "drop" - drop_database + Micrate::Cli.drop_database(url) when "create" - create_database + Micrate::Cli.create_database(url) when "seed" Amber::CLI::Helpers.run("crystal db/seeds.cr", wait: true, shell: true) info "Seeded database" when "migrate" - migrate + Micrate::Cli.run_up(url, MIGRATIONS_DIR) when "rollback" - Micrate::Cli.run_down + Micrate::Cli.run_down(url, MIGRATIONS_DIR) when "redo" - Micrate::Cli.run_redo + Micrate::Cli.run_redo(url, MIGRATIONS_DIR) when "status" - Micrate::Cli.run_status + Micrate::Cli.run_status(url, MIGRATIONS_DIR) when "version" - Micrate::Cli.run_dbversion + Micrate::Cli.run_dbversion(url, MIGRATIONS_DIR) when "connect" connect_to_database else @@ -126,54 +126,8 @@ module AmberCLI::Commands end end - private def migrate - Micrate::Cli.run_up - rescue e : IndexError - exit! "No migrations to run in #{MIGRATIONS_DIR}." - end - - private def drop_database - url = Micrate::DB.connection_url.to_s - if url.starts_with? "sqlite3:" - path = url.gsub("sqlite3:", "") - File.delete(path) - info "Deleted file #{path}" - else - name = set_database_to_schema url - Micrate::DB.connect do |db| - db.exec "DROP DATABASE IF EXISTS #{name};" - end - info "Dropped database #{name}" - end - end - - private def create_database - url = Micrate::DB.connection_url.to_s - if url.starts_with? "sqlite3:" - info CREATE_SQLITE_MESSAGE - else - name = set_database_to_schema url - Micrate::DB.connect do |db| - db.exec "CREATE DATABASE #{name};" - end - info "Created database #{name}" - end - end - - private def set_database_to_schema(url) : String - uri = URI.parse(url) - if path = uri.path - Micrate::DB.connection_url = url.gsub(path, "/#{uri.scheme}") - return path.gsub("/", "") - else - error "Could not determine database name" - exit!(error: true) - return "" # This won't be reached but satisfies the compiler - end - end - private def connect_to_database - Process.exec(command_line_tool, {database_url}) if database_url + Process.exec(command_line_tool, {database_cli_argument}) exit! end @@ -215,21 +169,40 @@ module AmberCLI::Commands end end - private def database_url - ENV["DATABASE_URL"]? || default_database_url + private def database_url : String + ENV["DATABASE_URL"]? || ENV["AMBER_DATABASE_URL"]? || environment_database_url || default_database_url end - private def default_database_url - # Try to read from config - config_file = "config/database.cr" - if File.exists?(config_file) - # This is a simplified approach - in reality we'd need to parse the config - # For now, return a default - "postgres://localhost/amber_development" + private def environment_database_url : String? + environment = ENV["AMBER_ENV"]? || "development" + config_file = "config/environments/#{environment}.yml" + return unless File.exists?(config_file) + + document = YAML.parse(File.read(config_file)) + document["database"]?.try(&.["url"]?).try(&.as_s?) + rescue ex : YAML::ParseException + warning "Could not parse #{config_file}: #{ex.message}" + nil + end + + private def default_database_url : String + name = Amber::CLI::Config.get_name + environment = ENV["AMBER_ENV"]? || "development" + + case database_type + when "mysql" + "mysql://localhost:3306/#{name}_#{environment}" + when "sqlite" + "sqlite3:./db/#{name}_#{environment}.db" else - "postgres://localhost/amber_development" + "postgres://localhost:5432/#{name}_#{environment}" end end + + private def database_cli_argument : String + url = database_url + database_type == "sqlite" ? url.sub(/^sqlite3:(?:\/\/)?/, "") : url + end end end diff --git a/src/amber_cli/commands/generate.cr b/src/amber_cli/commands/generate.cr index 1ab0750..dfef668 100644 --- a/src/amber_cli/commands/generate.cr +++ b/src/amber_cli/commands/generate.cr @@ -1,4 +1,5 @@ require "../core/base_command" +require "../config" # The `generate` command creates models, controllers, migrations, scaffolds, # jobs, mailers, schemas, and channels for an Amber V2 application. @@ -35,7 +36,7 @@ require "../core/base_command" module AmberCLI::Commands class GenerateCommand < AmberCLI::Core::BaseCommand VALID_TYPES = %w[model controller scaffold migration mailer job schema channel api auth] - PREVIEW_TYPES = %w[model scaffold api auth] + PREVIEW_TYPES = %w[api auth] FIELD_TYPE_MAP = { "string" => "String", @@ -190,7 +191,7 @@ module AmberCLI::Commands def execute if PREVIEW_TYPES.includes?(generator_type) warning "#{generator_type} generation is a preview surface in the Amber V2 beta." - warning "The generated output requires a compatible persistence stack that is not included in new web apps." + warning "Review the generated authentication or API behavior before production use." end if File.exists?(".amber.yml") && File.read(".amber.yml").includes?("template: slang") @@ -291,7 +292,7 @@ RETRIES # #{class_name}.new.enqueue(delay: 5.minutes) # #{class_name}.new.enqueue(queue: "critical") # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.2/docs/guides/background-jobs.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/guides/background-jobs.md class #{class_name} < Amber::Jobs::Job include JSON::Serializable @@ -389,7 +390,7 @@ METHOD # .subject("Welcome!") # .deliver # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.2/docs/guides/mailer.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/guides/mailer.md class #{class_name}Mailer < Amber::Mailer::Base def initialize(@user_name : String, @user_email : String) end @@ -493,7 +494,7 @@ SPEC # # Handle errors: result.errors # end # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.2/docs/guides/schema-api.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/guides/schema-api.md class #{class_name}Schema < Amber::Schema::Definition #{field_definitions} end @@ -598,7 +599,7 @@ SPEC # Clients subscribe to this channel through a ClientSocket. # Messages sent to this channel are handled by `handle_message`. # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.2/docs/guides/websockets.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/guides/websockets.md class #{class_name}Channel < Amber::WebSockets::Channel # Called when a client subscribes to this channel. # Use this for authorization or sending initial state. @@ -629,7 +630,7 @@ CHANNEL # Configure in config/routes.cr: # websocket "/#{file_name}", #{class_name}Socket # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.2/docs/guides/websockets.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/guides/websockets.md struct #{class_name}Socket < Amber::WebSockets::ClientSocket channel "#{file_name}:*", #{class_name}Channel @@ -678,14 +679,16 @@ SPEC private def model_template field_definitions = fields.map do |field_name, field_type| crystal_type = FIELD_TYPE_MAP[field_type]? || "String" + crystal_type += "?" unless field_required?(field_name) " column #{field_name} : #{crystal_type}" end.join("\n") <<-MODEL -class #{class_name} < Grant::Model - table :#{table_name} +class #{class_name} < Grant::Base + connection primary + table #{table_name} - primary_key id : Int64 + column id : Int64, primary: true #{field_definitions} @@ -704,9 +707,9 @@ MODEL require "../spec_helper" describe #{class_name} do - it "can be created" do + it "uses the #{table_name} table" do #{variable_name} = #{class_name}.new - #{variable_name}.should_not be_nil + #{variable_name}.class.table_name.should eq("#{table_name}") end end SPEC @@ -832,21 +835,23 @@ SPEC generate_scaffold_schema generate_controller_for_scaffold generate_views + add_resource_route success "Scaffold #{class_name} generated successfully!" puts "" - info "Don't forget to add routes to config/routes.cr:" - info " resources \"/#{plural_name}\", #{controller_name}" + info "Added resources \"/#{plural_name}\", #{controller_name} to config/routes.cr" + info "Run 'amber database migrate' before opening /#{plural_name}." end private def generate_scaffold_schema schema_path = "src/schemas/#{file_name}_schema.cr" - field_definitions = fields.map do |field_name, field_type| + field_definitions = schema_fields.map do |field_name, field_type, is_required| schema_info = SCHEMA_TYPE_MAP[field_type]? || {type: "String", options: ""} crystal_type = schema_info[:type] extra_options = schema_info[:options] - " field :#{field_name}, #{crystal_type}, required: true#{extra_options}" + required_option = is_required ? ", required: true" : "" + " field :#{field_name}, #{crystal_type}#{required_option}#{extra_options}" end.join("\n") content = <<-SCHEMA @@ -854,7 +859,7 @@ SPEC # # Used by #{controller_name} for request validation. # -# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.2/docs/guides/schema-api.md +# See: https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/guides/schema-api.md class #{class_name}Schema < Amber::Schema::Definition #{field_definitions} end @@ -875,17 +880,23 @@ SCHEMA template_ext = detect_template_extension schema_field_assignments = fields.map do |field_name, _| - " #{variable_name}.#{field_name} = schema.#{field_name}.not_nil!" + suffix = field_required?(field_name) ? ".not_nil!" : "" + " #{variable_name}.#{field_name} = schema.#{field_name}#{suffix}" end.join("\n") update_field_assignments = fields.map do |field_name, _| - " #{variable_name}.#{field_name} = schema.#{field_name}.not_nil!" + suffix = field_required?(field_name) ? ".not_nil!" : "" + " #{variable_name}.#{field_name} = schema.#{field_name}#{suffix}" end.join("\n") <<-CONTROLLER class #{controller_name} < ApplicationController + @#{plural_variable_name} = [] of #{class_name} + @#{variable_name} = #{class_name}.new + @errors = [] of Amber::Schema::Error + def index - @#{plural_variable_name} = #{class_name}.all + @#{plural_variable_name} = #{class_name}.all.to_a render("index.#{template_ext}") end @@ -1058,7 +1069,11 @@ SPEC -- Migration: #{migration_name} -- Created: #{Time.utc} --- Add your migration SQL here +-- +micrate Up +-- Add SQL to apply the migration here. + +-- +micrate Down +-- Add SQL to roll the migration back here. SQL else @@ -1109,18 +1124,20 @@ SQL private def api_controller_template schema_field_assignments = fields.map do |field_name, _| - " #{variable_name}.#{field_name} = schema.#{field_name}.not_nil!" + suffix = field_required?(field_name) ? ".not_nil!" : "" + " #{variable_name}.#{field_name} = schema.#{field_name}#{suffix}" end.join("\n") update_field_assignments = fields.map do |field_name, _| - " #{variable_name}.#{field_name} = schema.#{field_name}.not_nil!" + suffix = field_required?(field_name) ? ".not_nil!" : "" + " #{variable_name}.#{field_name} = schema.#{field_name}#{suffix}" end.join("\n") <<-CONTROLLER module Api class #{controller_name} < ApplicationController def index - #{plural_variable_name} = #{class_name}.all + #{plural_variable_name} = #{class_name}.all.to_a render json: #{plural_variable_name}.to_json end @@ -1363,7 +1380,8 @@ VIEW private def create_table_migration column_definitions = fields.map do |field_name, field_type| sql_type = case field_type - when "string", "uuid", "email" then "VARCHAR(255)" + when "string", "email" then "VARCHAR(255)" + when "uuid" then database_type == "pg" ? "UUID" : "VARCHAR(36)" when "text" then "TEXT" when "integer", "int", "int32" then "INTEGER" when "int64", "reference" then "BIGINT" @@ -1373,17 +1391,23 @@ VIEW when "time", "timestamp" then "TIMESTAMP" else "VARCHAR(255)" end - " #{field_name} #{sql_type}" + nullability = field_required?(field_name) ? " NOT NULL" : "" + " #{field_name} #{sql_type}#{nullability}" end.join(",\n") + field_sql = column_definitions.empty? ? "" : "#{column_definitions},\n" + <<-SQL +-- +micrate Up -- Create #{table_name} table CREATE TABLE IF NOT EXISTS #{table_name} ( - id BIGSERIAL PRIMARY KEY, -#{column_definitions}, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + #{primary_key_sql}, +#{field_sql} created_at TIMESTAMP, + updated_at TIMESTAMP ); + +-- +micrate Down +DROP TABLE IF EXISTS #{table_name}; SQL end @@ -1474,7 +1498,7 @@ VIEW <<-VIEW h1 New #{class_name} -== render("_form.slang") +== render(partial: "_form.slang") a href="/#{plural_name}" Back VIEW @@ -1482,7 +1506,7 @@ VIEW <<-VIEW

New #{class_name}

-<%= render("_form.ecr") %> +<%= render(partial: "_form.ecr") %> Back VIEW @@ -1494,7 +1518,7 @@ VIEW <<-VIEW h1 Edit #{class_name} -== render("_form.slang") +== render(partial: "_form.slang") a href="/#{plural_name}" Back VIEW @@ -1502,7 +1526,7 @@ VIEW <<-VIEW

Edit #{class_name}

-<%= render("_form.ecr") %> +<%= render(partial: "_form.ecr") %> Back VIEW @@ -1542,7 +1566,10 @@ FIELD end.join("\n") <<-VIEW -== form(action: "/#{plural_name}", method: "post") do +- form_action = @#{variable_name}.persisted? ? "/#{plural_name}/\#{@#{variable_name}.id}" : "/#{plural_name}" +== form(action: form_action, method: "post") do + - if @#{variable_name}.persisted? + input type="hidden" name="_method" value="PATCH" #{form_fields} button type="submit" Save VIEW @@ -1553,13 +1580,13 @@ VIEW <<-FIELD
<%= label("#{field_name}") %> - <%= text_area("#{field_name}", value: @#{variable_name}.try(&.#{field_name})) %> + <%= text_area("#{field_name}", value: @#{variable_name}.#{field_name}?) %>
FIELD when "bool", "boolean" <<-FIELD
- <%= checkbox("#{field_name}", checked: @#{variable_name}.try(&.#{field_name}) || false) %> + <%= checkbox("#{field_name}", checked: @#{variable_name}.#{field_name}? || false, value: "true") %> <%= label("#{field_name}") %>
FIELD @@ -1567,31 +1594,46 @@ FIELD <<-FIELD
<%= label("#{field_name}") %> - <%= email_field("#{field_name}", value: @#{variable_name}.try(&.#{field_name})) %> + <%= email_field("#{field_name}", value: @#{variable_name}.#{field_name}?) %>
FIELD when "integer", "int", "int32", "int64", "float", "float64", "decimal" <<-FIELD
<%= label("#{field_name}") %> - <%= number_field("#{field_name}", value: @#{variable_name}.try(&.#{field_name})) %> + <%= number_field("#{field_name}", value: @#{variable_name}.#{field_name}?) %>
FIELD else <<-FIELD
<%= label("#{field_name}") %> - <%= text_field("#{field_name}", value: @#{variable_name}.try(&.#{field_name})) %> + <%= text_field("#{field_name}", value: @#{variable_name}.#{field_name}?) %>
FIELD end end.join("\n") <<-VIEW -<%= form_for("/#{plural_name}", method: "POST") { %> +<% unless @errors.empty? %> + +<% end %> + +
" method="POST"> + <%= csrf_tag %> + <% if @#{variable_name}.persisted? %> + <%= hidden_field("_method", "PATCH") %> + <% end %> #{form_fields} <%= submit_button("Save") %> -<% } %> +
VIEW end end @@ -1642,6 +1684,45 @@ VIEW %w[index show new create edit update destroy] end + private def field_required?(field_name : String) : Bool + schema_fields.find { |field| field[0] == field_name }.try(&.[2]) || false + end + + private def database_type : String + Amber::CLI.config.database + end + + private def primary_key_sql : String + case database_type + when "pg" + "id BIGSERIAL PRIMARY KEY" + when "mysql" + "id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY" + else + "id INTEGER PRIMARY KEY AUTOINCREMENT" + end + end + + private def add_resource_route + routes_path = "config/routes.cr" + unless File.exists?(routes_path) + warning "Could not add the resource route because #{routes_path} does not exist." + return + end + + route = " resources \"/#{plural_name}\", #{controller_name}\n" + content = File.read(routes_path) + return if content.includes?(route.strip) + + anchor = " routes :web do\n" + unless content.includes?(anchor) + warning "Could not find the web routes block in #{routes_path}." + return + end + + File.write(routes_path, content.sub(anchor, "#{anchor}#{route}")) + end + private def field_assignments fields.map do |field_name, _| " #{variable_name}.#{field_name} = params[:#{field_name}]" diff --git a/src/amber_cli/commands/new.cr b/src/amber_cli/commands/new.cr index e846eca..104066e 100644 --- a/src/amber_cli/commands/new.cr +++ b/src/amber_cli/commands/new.cr @@ -17,10 +17,10 @@ require "../generators/native_app" # # ## Examples # ``` -# # Create a new web app with PostgreSQL and ECR (defaults) +# # Create a new web app with SQLite, Grant, and ECR (defaults) # amber new my_blog # -# # Record MySQL as the database for future persistence tooling +# # Create a web app backed by MySQL instead # amber new my_blog -d mysql # # # Create a native cross-platform app (macOS, iOS, Android) @@ -35,7 +35,7 @@ module AmberCLI::Commands VALID_DATABASES = %w[pg mysql sqlite] VALID_TEMPLATES = %w[ecr] - getter database : String = "pg" + getter database : String = "sqlite" getter template : String = "ecr" getter app_type : String = "web" getter assume_yes : Bool = false @@ -88,7 +88,7 @@ module AmberCLI::Commands option_parser.separator "Usage: amber new [NAME] [options]" option_parser.separator "" option_parser.separator "App types:" - option_parser.separator " web Web application with HTTP server, routes, views (default)" + option_parser.separator " web Web application with HTTP server, Grant ORM, SQLite, routes, and views (default)" option_parser.separator " native Preview: cross-platform native app (macOS, iOS, Android)" option_parser.separator " Uses Asset Pipeline UI, FSDD process managers," option_parser.separator " crystal-audio, and platform build scripts." @@ -188,10 +188,10 @@ module AmberCLI::Commands info " cd #{name}" unless name == "." info " shards install" if no_deps info " crystal spec" + info " amber generate scaffold Pet name:string:required species:string:required" + info " amber database migrate" info " amber watch" - puts "" - info "Persistence, auth, API-resource, and native generators are preview surfaces." - info "The generated web app intentionally has no ORM or database driver." + info " # Choose -d pg or -d mysql when you need a server database." end private def install_dependencies(path : String) @@ -241,6 +241,7 @@ module AmberCLI::Commands # Create all project files create_shard_yml(path, name) create_amber_yml(path, name) + create_readme(path, name) create_gitignore(path) create_main_file(path, name) create_config_files(path, name) @@ -277,7 +278,11 @@ targets: dependencies: amber: github: amberframework/amber - version: 2.0.0-beta.2 + version: 2.0.0-beta.3 + grant: + github: crimson-knight/grant + commit: 2665a978b43ac608c68cde9243821f8f8f053372 +#{database_shard_dependency} SHARD write_text(File.join(path, "shard.yml"), shard_content) @@ -291,13 +296,50 @@ author: Your Name email: your.email@example.com database: #{database} language: crystal -model: none +model: grant template: ecr AMBER write_text(File.join(path, ".amber.yml"), amber_content) end + private def create_readme(path : String, name : String) + readme_content = <<-README +# #{name} + +An ECR web application generated by Amber CLI for Amber `2.0.0-beta.3`. + +## Run it + +```bash +shards install +crystal spec +amber generate scaffold Pet name:string:required species:string:required adopted:bool +amber database migrate +amber watch +``` + +Open for the starter page or + after generating the example scaffold. + +The scaffold writes its Grant model to `src/models/pet.cr`, request schema to +`src/schemas/pet_schema.cr`, controller to `src/controllers/pet_controller.cr`, +ECR views to `src/views/pet/`, SQL migration to `db/migrations/`, specs to +`spec/`, and resource route to `config/routes.cr`. + +This application uses #{database} through `config/database.cr`. SQLite is the +zero-setup default; generate the app with `-d pg` or `-d mysql` to select a +server database. Set `DATABASE_URL` to override the environment YAML URL. + +## Production configuration + +Set at least `AMBER_SERVER_SECRET_KEY_BASE` and `DATABASE_URL`. See the +[Amber V2 beta guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/beta-installation.md). +README + + write_text(File.join(path, "README.md"), readme_content) + end + private def create_gitignore(path : String) gitignore_content = <<-GITIGNORE # Crystal @@ -354,6 +396,19 @@ require "amber" CONFIG write_text(File.join(path, "config/application.cr"), app_config) + + database_config = <<-CONFIG +require "amber" +require "grant" +require "grant/adapter/#{database_adapter_require}" + +Grant::Connections << Grant::Adapter::#{database_adapter_class}.new( + name: "primary", + url: ENV["DATABASE_URL"]? || Amber.settings.database_url +) +CONFIG + + write_text(File.join(path, "config/database.cr"), database_config) end private def create_application_controller(path : String) @@ -569,6 +624,9 @@ SPEC private def create_seeds_file(path : String) seeds_content = <<-SEEDS +require "../config/*" +require "../src/models/**" + # Database seed file # # Use this file to populate your database with initial data. @@ -1022,6 +1080,41 @@ ROBOTS private def write_text(path : String, content : String) File.write(path, content.ends_with?("\n") ? content : "#{content}\n") end + + private def database_shard_dependency : String + case database + when "pg" + <<-YAML + pg: + github: will/crystal-pg + version: 0.30.0 + YAML + when "mysql" + <<-YAML + mysql: + github: crimson-knight/crystal-mysql + commit: c061324dcef89a200a7a3f86a59b2ebf03f83602 + YAML + else + <<-YAML + sqlite3: + github: crystal-lang/crystal-sqlite3 + version: 0.23.0 + YAML + end + end + + private def database_adapter_require : String + database == "sqlite" ? "sqlite" : database + end + + private def database_adapter_class : String + case database + when "pg" then "Pg" + when "mysql" then "Mysql" + else "Sqlite" + end + end end end diff --git a/src/amber_cli/commands/setup_lsp.cr b/src/amber_cli/commands/setup_lsp.cr index ff51492..8560272 100644 --- a/src/amber_cli/commands/setup_lsp.cr +++ b/src/amber_cli/commands/setup_lsp.cr @@ -215,7 +215,7 @@ module AmberCLI::Commands content = <<-YAML # Amber LSP Configuration - # See: https://github.com/amberframework/amber/blob/v2.0.0-beta.2/docs/guides/lsp-setup.md + # See: https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/guides/lsp-setup.md # Override built-in rule settings # rules: diff --git a/src/amber_cli/config.cr b/src/amber_cli/config.cr index 3e018cd..2ba9853 100644 --- a/src/amber_cli/config.cr +++ b/src/amber_cli/config.cr @@ -26,9 +26,9 @@ module Amber::CLI # see defaults below alias WatchOptions = Hash(String, Hash(String, Array(String))) - property database : String = "pg" + property database : String = "sqlite" property language : String = "ecr" - property model : String = "none" + property model : String = "grant" property watch : WatchOptions? def initialize diff --git a/src/amber_cli/documentation.cr b/src/amber_cli/documentation.cr index 10423df..d8b2d9e 100644 --- a/src/amber_cli/documentation.cr +++ b/src/amber_cli/documentation.cr @@ -67,7 +67,7 @@ module AmberCLI::Documentation # amber new my_blog # ``` # - # Record a database choice for future persistence tooling: + # Create an application backed by MySQL: # ```bash # amber new my_api -d mysql -t ecr # ``` @@ -89,8 +89,9 @@ module AmberCLI::Documentation # - **.amber.yml** - Project configuration # - **.gitignore** - Git ignore rules # - # The web template pins Amber `2.0.0-beta.2`, uses ECR, and intentionally - # does not add an ORM or database driver. Model, scaffold, API-resource, + # The web template pins Amber `2.0.0-beta.3`, uses ECR, and includes Grant, + # Micrate-powered database commands, and the selected database driver. Model, + # scaffold, and migration generators are supported. API-resource, # authentication, and native generators are preview surfaces during the beta. class NewCommand end diff --git a/src/amber_cli/templates/app/.amber.yml.ecr b/src/amber_cli/templates/app/.amber.yml.ecr index d61ad80..9cd10aa 100644 --- a/src/amber_cli/templates/app/.amber.yml.ecr +++ b/src/amber_cli/templates/app/.amber.yml.ecr @@ -1,7 +1,7 @@ type: app database: <%= @database %> language: crystal -model: none +model: grant template: ecr # list of tasks to be run by `amber watch` diff --git a/src/amber_cli/templates/app/README.md.ecr b/src/amber_cli/templates/app/README.md.ecr index a2c8949..5ad3751 100644 --- a/src/amber_cli/templates/app/README.md.ecr +++ b/src/amber_cli/templates/app/README.md.ecr @@ -1,28 +1,28 @@ # <%= @name %> -An ECR web application generated by Amber CLI for Amber `2.0.0-beta.2`. +An ECR web application generated by Amber CLI for Amber `2.0.0-beta.3`. ## Run it ```bash shards install crystal spec +amber generate scaffold Pet name:string:required species:string:required +amber database migrate amber watch ``` Open and . -The generated application intentionally includes no ORM or database driver. -Persistence, authentication, API-resource, and native generators are preview -surfaces during this beta. Add those dependencies only by following their own -release documentation. +The generated application includes Grant ORM and the selected database driver. +SQLite is the zero-setup default; choose `-d pg` or `-d mysql` when generating +the app to target a server database. ## Production configuration -Set at least `AMBER_SERVER_SECRET_KEY_BASE`. Set `AMBER_DATABASE_URL` after an -ORM and database driver have been added. See the -[Amber V2 beta guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.2/docs/beta-installation.md). +Set at least `AMBER_SERVER_SECRET_KEY_BASE` and `AMBER_DATABASE_URL`. See the +[Amber V2 beta guide](https://github.com/amberframework/amber/blob/v2.0.0-beta.3/docs/beta-installation.md). ## Tests diff --git a/src/amber_cli/templates/app/config/database.cr.ecr b/src/amber_cli/templates/app/config/database.cr.ecr new file mode 100644 index 0000000..158fa40 --- /dev/null +++ b/src/amber_cli/templates/app/config/database.cr.ecr @@ -0,0 +1,19 @@ +require "amber" +require "grant" +<% case @database +when "pg" -%> +require "grant/adapter/pg" + +Grant::Connections << Grant::Adapter::Pg.new( +<% when "mysql" -%> +require "grant/adapter/mysql" + +Grant::Connections << Grant::Adapter::Mysql.new( +<% else -%> +require "grant/adapter/sqlite" + +Grant::Connections << Grant::Adapter::Sqlite.new( +<% end -%> + name: "primary", + url: ENV["DATABASE_URL"]? || Amber.settings.database_url +) diff --git a/src/amber_cli/templates/app/db/seeds.cr.ecr b/src/amber_cli/templates/app/db/seeds.cr.ecr index cc02231..2ab4627 100644 --- a/src/amber_cli/templates/app/db/seeds.cr.ecr +++ b/src/amber_cli/templates/app/db/seeds.cr.ecr @@ -1,8 +1,10 @@ require "../config/application" +require "../config/database" +require "../src/models/**" # This file is for setting up your seeds. # -# To run seeds execute `amber db seed` +# To run seeds execute `amber database seed` # Example: # User.create(name: "example", email: "ex@mple.com") diff --git a/src/amber_cli/templates/app/shard.yml.ecr b/src/amber_cli/templates/app/shard.yml.ecr index a8ce201..efd58e0 100644 --- a/src/amber_cli/templates/app/shard.yml.ecr +++ b/src/amber_cli/templates/app/shard.yml.ecr @@ -15,4 +15,21 @@ targets: dependencies: amber: github: amberframework/amber - version: 2.0.0-beta.2 + version: 2.0.0-beta.3 + grant: + github: crimson-knight/grant + commit: 2665a978b43ac608c68cde9243821f8f8f053372 +<% case @database +when "pg" -%> + pg: + github: will/crystal-pg + version: 0.30.0 +<% when "mysql" -%> + mysql: + github: crimson-knight/crystal-mysql + commit: c061324dcef89a200a7a3f86a59b2ebf03f83602 +<% else -%> + sqlite3: + github: crystal-lang/crystal-sqlite3 + version: 0.23.0 +<% end -%> From 95062ef8fa615cbddd44bf7a6b1abe192c25759a Mon Sep 17 00:00:00 2001 From: crimson-knight Date: Tue, 11 Aug 2026 16:09:17 -0400 Subject: [PATCH 2/6] Pin CLI release dependencies --- .github/workflows/build.yml | 8 ++++---- .github/workflows/ci.yml | 18 +++++++++--------- .github/workflows/docs.yml | 10 +++++----- .github/workflows/release.yml | 14 +++++++------- shard.lock | 2 +- shard.yml | 4 ++-- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8118b11..7bb05b2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,11 +23,11 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install Crystal (Linux) if: matrix.target == 'linux-x86_64' - uses: crystal-lang/install-crystal@v1 + uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1 with: crystal: latest @@ -36,7 +36,7 @@ jobs: run: brew install crystal openssl@3 - name: Cache shards - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | ~/.cache/shards @@ -86,7 +86,7 @@ jobs: run: scripts/smoke_generated_web.sh ./amber bd8d9428f45cb836cb6f26bbc358721a9f0ca3b3 - name: Upload build artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: github.event_name == 'workflow_dispatch' with: name: amber-cli-${{ matrix.target }}-build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5f4833..247afde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,16 +19,16 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install Crystal - uses: crystal-lang/install-crystal@v1 + uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1 with: crystal: ${{ matrix.crystal }} - name: Cache shards - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | ~/.cache/shards @@ -71,7 +71,7 @@ jobs: crystal build src/amber_lsp.cr --release --no-debug -o amber_lsp - name: Upload binary artifacts (Linux) - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: matrix.os == 'ubuntu-latest' with: name: amber-cli-linux @@ -91,15 +91,15 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install Crystal - uses: crystal-lang/install-crystal@v1 + uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1 with: crystal: latest - name: Restore tested shard cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: | ~/.cache/shards @@ -130,10 +130,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install Crystal - uses: crystal-lang/install-crystal@v1 + uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1 with: crystal: latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d6e9acc..3a8d564 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,10 +26,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install Crystal - uses: crystal-lang/install-crystal@v1 + uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1 with: crystal: latest @@ -49,11 +49,11 @@ jobs: - name: Setup Pages if: github.ref == 'refs/heads/main' - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - name: Upload artifact if: github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: path: ./docs @@ -68,4 +68,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16f6edd..ed65547 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,13 +34,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: ref: ${{ github.event.release.tag_name || github.event.inputs.ref }} - name: Install Crystal (Linux) if: startsWith(matrix.target, 'linux-') - uses: crystal-lang/install-crystal@v1 + uses: crystal-lang/install-crystal@d8ef131ecec0352ce0e39b81b0a6d95def58fe2f # v1 with: crystal: latest @@ -112,7 +112,7 @@ jobs: echo "sha256=$(cat amber_cli-${{ matrix.target }}.tar.gz.sha256 | cut -d' ' -f1)" >> $GITHUB_OUTPUT - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: amber_cli-${{ matrix.target }} path: | @@ -127,7 +127,7 @@ jobs: steps: - name: Download all artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: artifacts @@ -151,7 +151,7 @@ jobs: done - name: Upload release assets - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@c12583777ecdfd3be55c69cf75464299dc01057e # v3 with: files: | artifacts/amber_cli-darwin-arm64/amber_cli-darwin-arm64.tar.gz @@ -173,7 +173,7 @@ jobs: steps: - name: Dispatch to underscore tap (homebrew-amber_cli) - uses: peter-evans/repository-dispatch@v4 + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 with: token: ${{ secrets.HOMEBREW_TAP_TOKEN }} repository: amberframework/homebrew-amber_cli @@ -181,7 +181,7 @@ jobs: client-payload: '{"version": "${{ github.event.release.tag_name }}"}' - name: Dispatch to hyphen tap (homebrew-amber-cli) - uses: peter-evans/repository-dispatch@v4 + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 with: token: ${{ secrets.HOMEBREW_TAP_TOKEN }} repository: amberframework/homebrew-amber-cli diff --git a/shard.lock b/shard.lock index 896fb5b..e93d72f 100644 --- a/shard.lock +++ b/shard.lock @@ -6,7 +6,7 @@ shards: asset_pipeline: git: https://github.com/amberframework/asset_pipeline.git - version: 0.36.0+git.commit.ded6af312d667b93db29e678ac4b8ff8dc26774e + version: 0.36.0+git.commit.5f63d9adb02be566cdade67e4616ca0bb3cbf30b backtracer: git: https://github.com/sija/backtracer.cr.git diff --git a/shard.yml b/shard.yml index 8632944..0f5a25d 100644 --- a/shard.yml +++ b/shard.yml @@ -19,11 +19,11 @@ dependencies: # Temporary branch until the Apple shell export APIs land in a tagged release. asset_pipeline: github: amberframework/asset_pipeline - branch: feature/utility-first-css-asset-pipeline + commit: 5f63d9adb02be566cdade67e4616ca0bb3cbf30b micrate: github: amberframework/micrate - branch: agent/crystal-1.21-runner + commit: 56afd5607cc591776980f3d3762495c037b98d98 pg: github: will/crystal-pg From 9aa1282cd9213fdd836d9c44eb25dd5e1ddb5909 Mon Sep 17 00:00:00 2001 From: crimson-knight Date: Tue, 11 Aug 2026 16:29:33 -0400 Subject: [PATCH 3/6] Support Windows route files in scaffolds --- scripts/smoke_generated_web.ps1 | 6 ++++++ spec/commands/generate_command_spec.cr | 18 ++++++++++++++++++ src/amber_cli/commands/generate.cr | 9 +++++---- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/scripts/smoke_generated_web.ps1 b/scripts/smoke_generated_web.ps1 index 12ce9df..eb43c06 100644 --- a/scripts/smoke_generated_web.ps1 +++ b/scripts/smoke_generated_web.ps1 @@ -75,6 +75,12 @@ try { "adopted:bool" ) + $routesPath = Join-Path $appPath "config/routes.cr" + $routes = [System.IO.File]::ReadAllText($routesPath) + if (-not $routes.Contains('resources "/pets", PetController')) { + throw "Generated scaffold did not add the Pet resource route" + } + $previousAmberEnv = $env:AMBER_ENV try { $env:AMBER_ENV = "test" diff --git a/spec/commands/generate_command_spec.cr b/spec/commands/generate_command_spec.cr index 3616899..25a6ade 100644 --- a/spec/commands/generate_command_spec.cr +++ b/spec/commands/generate_command_spec.cr @@ -65,4 +65,22 @@ describe AmberCLI::Commands::GenerateCommand do File.read("src/views/pet/_form.ecr").should contain(%(checkbox("adopted", checked: @pet.adopted? || false, value: "true"))) end end + + it "adds scaffold routes to Windows-style route files" do + SpecHelper.within_temp_directory do + File.write(".amber.yml", "template: ecr\ndatabase: sqlite\nmodel: grant\n") + Dir.mkdir_p("config") + File.write("config/routes.cr", "Amber::Server.configure do |app|\r\n routes :web do\r\n end\r\nend\r\n") + + command = AmberCLI::Commands::GenerateCommand.new("generate") + command.parse_and_execute([ + "scaffold", + "Pet", + "name:string:required", + ]) + + routes = File.read("config/routes.cr") + routes.should contain(" routes :web do\r\n resources \"/pets\", PetController\r\n") + end + end end diff --git a/src/amber_cli/commands/generate.cr b/src/amber_cli/commands/generate.cr index dfef668..31bd2f4 100644 --- a/src/amber_cli/commands/generate.cr +++ b/src/amber_cli/commands/generate.cr @@ -1710,17 +1710,18 @@ VIEW return end - route = " resources \"/#{plural_name}\", #{controller_name}\n" content = File.read(routes_path) - return if content.includes?(route.strip) + route = " resources \"/#{plural_name}\", #{controller_name}" + return if content.includes?(route) - anchor = " routes :web do\n" + anchor = " routes :web do" unless content.includes?(anchor) warning "Could not find the web routes block in #{routes_path}." return end - File.write(routes_path, content.sub(anchor, "#{anchor}#{route}")) + newline = content.includes?("\r\n") ? "\r\n" : "\n" + File.write(routes_path, content.sub(anchor, "#{anchor}#{newline}#{route}")) end private def field_assignments From 2b75e3631db37755c7f5c006785b0abc5a532b4e Mon Sep 17 00:00:00 2001 From: crimson-knight Date: Tue, 11 Aug 2026 16:41:18 -0400 Subject: [PATCH 4/6] Test CLI against the beta 3 framework candidate --- .github/workflows/build.yml | 2 +- .github/workflows/platform-compile.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7bb05b2..497771c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,7 +83,7 @@ jobs: ! otool -L amber amber-lsp | grep -F 'openssl@1.1' - name: Smoke test generated Amber V2 web app - run: scripts/smoke_generated_web.sh ./amber bd8d9428f45cb836cb6f26bbc358721a9f0ca3b3 + run: scripts/smoke_generated_web.sh ./amber c342aa716dce0700bca71b0dac5d112c374361cd - name: Upload build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.github/workflows/platform-compile.yml b/.github/workflows/platform-compile.yml index f22fab5..4046bb5 100644 --- a/.github/workflows/platform-compile.yml +++ b/.github/workflows/platform-compile.yml @@ -27,7 +27,7 @@ jobs: run: crystal build src/amber_cli.cr --no-debug -o amber - name: Compile generated Amber V2 web app - run: scripts/smoke_generated_web.sh ./amber bd8d9428f45cb836cb6f26bbc358721a9f0ca3b3 + run: scripts/smoke_generated_web.sh ./amber c342aa716dce0700bca71b0dac5d112c374361cd windows-x86-64: name: Windows x86_64 generated web app (merged framework fix) @@ -57,4 +57,4 @@ jobs: - name: Compile generated Amber V2 web app shell: pwsh - run: scripts/smoke_generated_web.ps1 ./amber.exe bd8d9428f45cb836cb6f26bbc358721a9f0ca3b3 + run: scripts/smoke_generated_web.ps1 ./amber.exe c342aa716dce0700bca71b0dac5d112c374361cd From 712220bdc650c01bf90a05b708d9a469494ecc02 Mon Sep 17 00:00:00 2001 From: crimson-knight Date: Tue, 11 Aug 2026 16:48:08 -0400 Subject: [PATCH 5/6] Test generated apps against released Amber beta 3 --- .github/workflows/build.yml | 2 +- .github/workflows/platform-compile.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 497771c..b2e6b49 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,7 +83,7 @@ jobs: ! otool -L amber amber-lsp | grep -F 'openssl@1.1' - name: Smoke test generated Amber V2 web app - run: scripts/smoke_generated_web.sh ./amber c342aa716dce0700bca71b0dac5d112c374361cd + run: scripts/smoke_generated_web.sh ./amber - name: Upload build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.github/workflows/platform-compile.yml b/.github/workflows/platform-compile.yml index 4046bb5..21e7258 100644 --- a/.github/workflows/platform-compile.yml +++ b/.github/workflows/platform-compile.yml @@ -27,10 +27,10 @@ jobs: run: crystal build src/amber_cli.cr --no-debug -o amber - name: Compile generated Amber V2 web app - run: scripts/smoke_generated_web.sh ./amber c342aa716dce0700bca71b0dac5d112c374361cd + run: scripts/smoke_generated_web.sh ./amber windows-x86-64: - name: Windows x86_64 generated web app (merged framework fix) + name: Windows x86_64 generated web app runs-on: windows-latest steps: - name: Checkout code @@ -57,4 +57,4 @@ jobs: - name: Compile generated Amber V2 web app shell: pwsh - run: scripts/smoke_generated_web.ps1 ./amber.exe c342aa716dce0700bca71b0dac5d112c374361cd + run: scripts/smoke_generated_web.ps1 ./amber.exe From e3a06633365da4be8a1a089e9717e667578fadcd Mon Sep 17 00:00:00 2001 From: crimson-knight Date: Tue, 11 Aug 2026 16:59:57 -0400 Subject: [PATCH 6/6] Migrate both databases in Windows web smoke --- scripts/smoke_generated_web.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/smoke_generated_web.ps1 b/scripts/smoke_generated_web.ps1 index eb43c06..15c9985 100644 --- a/scripts/smoke_generated_web.ps1 +++ b/scripts/smoke_generated_web.ps1 @@ -89,6 +89,10 @@ try { $env:AMBER_ENV = $previousAmberEnv } + # Request specs use Amber's default development environment unless the + # caller sets AMBER_ENV. Mirror the Unix smoke test and migrate both stores. + Invoke-Checked -Command $cliPath -Arguments @("database", "migrate") + Invoke-Checked -Command "crystal" -Arguments @("spec") New-Item -ItemType Directory -Force bin | Out-Null Invoke-Checked -Command "crystal" -Arguments @(