trek 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +55 -0
  3. data/CHANGELOG.md +20 -0
  4. data/CLAUDE.md +1 -0
  5. data/Gemfile.lock +3 -3
  6. data/Makefile +1 -0
  7. data/app/controllers/concerns/trek/filters.rb +52 -0
  8. data/app/controllers/concerns/trek/scopes.rb +24 -0
  9. data/app/models/concerns/trek/users/invitable.rb +2 -0
  10. data/config/locales/trek.fr.yml +3 -3
  11. data/docs/reference/controllers.md +72 -0
  12. data/lib/generators/trek/helpers.rb +15 -0
  13. data/lib/generators/trek/install/agents_generator.rb +26 -0
  14. data/lib/generators/trek/install/attachments_generator.rb +3 -1
  15. data/lib/generators/trek/install/authentication_generator.rb +10 -2
  16. data/lib/generators/trek/install/authorization_generator.rb +4 -2
  17. data/lib/generators/trek/install/brakeman_generator.rb +4 -2
  18. data/lib/generators/trek/install/bundler_audit_generator.rb +4 -2
  19. data/lib/generators/trek/install/config_generator.rb +4 -0
  20. data/lib/generators/trek/install/healthcheck_generator.rb +4 -2
  21. data/lib/generators/trek/install/kaminari_generator.rb +4 -2
  22. data/lib/generators/trek/install/lookbook_generator.rb +3 -1
  23. data/lib/generators/trek/install/page_generator.rb +4 -2
  24. data/lib/generators/trek/install/postmark_generator.rb +5 -3
  25. data/lib/generators/trek/install/rubocop_generator.rb +4 -2
  26. data/lib/generators/trek/install/scalingo_generator.rb +2 -1
  27. data/lib/generators/trek/install/slim_lint_generator.rb +4 -2
  28. data/lib/generators/trek/install/specs_generator.rb +3 -2
  29. data/lib/generators/trek/install/strip_attributes_generator.rb +4 -2
  30. data/lib/generators/trek/install/translations_generator.rb +3 -1
  31. data/lib/generators/trek/install/yarn_generator.rb +8 -0
  32. data/lib/generators/trek/install_generator.rb +7 -0
  33. data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
  34. data/lib/generators/trek/templates/AGENTS.md +36 -0
  35. data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +4 -1
  36. data/lib/generators/trek/templates/controllers/admin/users_controller.rb +7 -0
  37. data/lib/generators/trek/templates/mailers/admin/passwords_mailer.rb +1 -1
  38. data/lib/generators/trek/templates/mailers/admin_mailer.rb +4 -1
  39. data/lib/generators/trek/templates/views/admin/dashboards/index.html.slim +2 -1
  40. data/lib/generators/trek/templates/views/admin_mailer/invite.en.html.slim +1 -0
  41. data/lib/generators/trek/templates/views/admin_mailer/invite.en.text.erb +2 -0
  42. data/lib/generators/trek/templates/views/admin_mailer/invite.fr.html.slim +1 -0
  43. data/lib/generators/trek/templates/views/admin_mailer/invite.fr.text.erb +2 -0
  44. data/lib/trek/version.rb +1 -1
  45. data/package.json +1 -1
  46. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac23b0f2968a03be59d307c19c62632c761cde58846a324dddd3f745a2bc5e8d
4
- data.tar.gz: 30b95227f9369edb137c5208e1b1a390e9fd56efaf6e75143020af418007b7b3
3
+ metadata.gz: 71867750da0cef4ae1928a4245e69742d7d32c6614c54da4d107f992da5e0e23
4
+ data.tar.gz: 71cb0d0b2ce4098030705245aaf92b50eefec11b4491bc0180e145eeb2f6a8e3
5
5
  SHA512:
6
- metadata.gz: d5572d3b2d99a149ae840b7b04643e10bece86e6664f6595349a596b116dc33f07cb988f197d6c8ffc0c47aff9511fcacb328224d7ec9b7590d8a7f9490ee9a5
7
- data.tar.gz: 2ac6db32ec1818397cbfd1e321164861d4e1bd0078296a7d37fe71f1a54e92d20793c0c435818eafdbbb489240ec812c4e22dd8e41d201614587a8b08164ac32
6
+ metadata.gz: 0e3a672cb3ebf8abee38d436832d92d3cc5c8bd5502f37344bda47275d4bd090a7326920cd823ed84e30990cd9feb453dddc2e62bfc1d43967de230498fd8287
7
+ data.tar.gz: aa4c5e9ee652804fc0361b8ac9e459863f67ff42441484e26fd8387312d8ba70517809b04d81fb68d9b9cab0ea6e6c7a27b943486c80c94b1544678841806fab
data/AGENTS.md ADDED
@@ -0,0 +1,55 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI coding agents working in this repository. Keep it accurate — update it when commands or conventions change.
4
+
5
+ ## What is Trek
6
+
7
+ Trek is a customizable CMS for Ruby on Rails, shipped as a **Ruby gem** (`trek`) plus a companion **NPM package** (`@etaminstudio/trek`). It's a Rails **engine** (`lib/trek/engine.rb`) that installs a back-office/admin into a host app. Installation and features are driven by Rails generators (`rails g trek:install`, `trek:scaffold`, `trek:admin:user`, …). Authored by Etamin Studio. Private gem hosted on self-hosted GitLab (`git.etaminstud.io`), not public RubyGems.
8
+
9
+ ## Tech stack
10
+
11
+ - **Ruby** `>= 3.1` (RuboCop targets 3.4) · **Rails** `>= 6.0, < 8.2` (lockfile 8.1.3; host apps need 7.0+)
12
+ - **Node** 22.21.1 (`.node-version`) · **Yarn** 3.8.0 (berry) · **PostgreSQL** 12+ · **libvips** for images
13
+ - Backend: `action_policy`, `closure_tree`, `acts_as_list`, `mobility` + `i18n-tasks`, `shrine` (+ blurhash/image_processing/aws-sdk-s3), `view_component` + `slim-rails` + `lookbook`, `kaminari`, `deepl-rb`
14
+ - Frontend: **esbuild** (`esbuild.config.js`) + **PostCSS** (`postcss.config.js`); Stimulus/Turbo, TipTap, Uppy, Tom Select, SortableJS
15
+ - Tests: **RSpec** (`rspec-rails`, capybara, simplecov) · Docs: **VitePress** (`docs/`)
16
+
17
+ ## Commands
18
+
19
+ ```bash
20
+ bin/setup # bundle install + yarn install (dev setup)
21
+ bin/console # IRB with trek loaded
22
+ rake # default: spec + rubocop
23
+ rake spec # run tests (or: bundle exec rspec)
24
+ make release # bump version (version.rb + package.json), stamp CHANGELOG, tag, push gem + npm
25
+ yarn docs:dev # VitePress docs locally
26
+ ```
27
+
28
+ Linters (also run automatically via lefthook on pre-commit):
29
+
30
+ ```bash
31
+ bundle exec rubocop
32
+ bundle exec slim-lint "app/**/*.slim"
33
+ yarn eslint "app/**/*.js"
34
+ yarn stylelint "app/**/*.css"
35
+ yarn prettier --check "{app,config,lib,spec}/**/*.{css,js,json,md,yml}"
36
+ ```
37
+
38
+ Security: `bundle exec brakeman` · `bundle exec bundle-audit check --update`
39
+
40
+ ## Structure
41
+
42
+ - `app/` — engine code, namespaced under `trek/`: `components/` (ViewComponents), `controllers/`, `forms/`, `models/`, `policies/` (ActionPolicy), `uploaders/` (Shrine), `views/`, `javascript/` (Stimulus)
43
+ - `lib/generators/trek/` — the heart of the gem: `install/` (~30 generators), `scaffold_generator.rb`, `admin/`, `templates/`
44
+ - `lib/trek/` — `engine.rb`, `version.rb`, `i18n_keys_resolver.rb`, `typography_formatter.rb`
45
+ - `config/locales/` — `trek.en.yml`, `trek.fr.yml`
46
+ - `spec/` — RSpec suite · `docs/` — VitePress site
47
+
48
+ ## Conventions
49
+
50
+ - **Ruby**: rubocop-rails-omakase + rubocop-rspec (`.rubocop.yml`) — double-quoted strings, line length 120, target 3.4
51
+ - **Slim** linted via slim_lint; **JS/CSS** via ESLint + Stylelint + Prettier 2.8.8
52
+ - **i18n**: keep English + French locales in sync (`config/locales/trek.*.yml`); DeepL auto-translation via `DEEPL_AUTH_KEY`
53
+ - **Versioning**: version lives in both `lib/trek/version.rb` and `package.json` — keep in sync via `make release`
54
+ - **CHANGELOG.md**: Keep-a-Changelog format. **Every MR must add an `[Unreleased]` entry.** When the change requires action in the host app on upgrade, add an "Upgrade note" describing what to do. When the change lets host apps remove or simplify existing patches or custom implementations, mention that too (e.g. "you can now drop your custom …").
55
+ - **CI**: GitLab CI (`.gitlab-ci.yml`), stages build → lint → security → test → deploy; docs deploy to Netlify; deps via Renovate
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.25] - 2026-07-01
4
+
5
+ ### Added
6
+
7
+ - Invitation and password-reset emails now tell the recipient when the link expires. Invitations get their own `:invite` token purpose (1 week) — distinct from the 15-minute `:password_reset` token — and the generated `Admin::UserPasswordResetsController` accepts either purpose, so the shared "set your password" form works from both emails. Each mailer reads its window from the model's token definition (`User.token_definitions[:purpose].expires_in`) rather than a hardcoded constant. **Upgrade note:** add `generates_token_for :invite, expires_in: 1.week do password_salt&.last(10) end` to your `User`; have `AdminMailer#invite` issue an `:invite` token (and `include ActionView::Helpers::DateHelper`); and update `Admin::UserPasswordResetsController#set_user_by_token` to try both purposes, e.g. `User.privileged.find_by_token_for(:invite, @token) || User.privileged.find_by_token_for!(:password_reset, @token)`.
8
+ - `trek:install` now generates an `AGENTS.md` (onboarding guidance for AI coding agents: commands, conventions, and a "read the changelog on every upgrade" checklist) plus a `CLAUDE.md` that imports it via `See @AGENTS.md`, so Claude Code and other agents share one source of truth. **Upgrade note:** existing apps can add both files by running `rails g trek:install:agents`.
9
+ - `Trek::Filters` controller concern: filter the index by `params[:filters]` (attribute equality via `where`). Call `apply_filters!` in your `index`.
10
+ - Pedagogical `NotAppliedError` guard on `Trek::Filters` and `Trek::Scopes`: raises (outside production) when the concern is included but `index` never calls `apply_filters!` / `apply_scopes!`.
11
+ - `trek:install:scalingo` now adds `libjemalloc2` to the generated `Aptfile`, enabling the jemalloc allocator to reduce Ruby memory usage on Scalingo.
12
+
13
+ ### Fixed
14
+
15
+ - The generated dashboard's "search a user" tile now links to the users index (`href: %i[admin users]`) instead of a dead `#`, and gates on `allowed_to: allowed_to?(:index?, User)` so roles that can't reach user management no longer see it (matching the sidebar's Users entry). **Upgrade note:** existing apps can set `href: %i[admin users]` and add `allowed_to: allowed_to?(:index?, User)` on that tile in `app/views/admin/dashboards/index.html.slim`.
16
+ - `trek:install` now points Action Mailer at the same host as the routes' `default_url_options` (driven by `DEFAULT_HOSTNAME`) in the generated `config/initializers/host.rb`, in production only (development and test keep the host from their environment files). Previously the mailer kept Rails' `example.com` default, so invitation and password-reset email links pointed to `https://example.com/...` in production. **Upgrade note:** existing apps can add `ActionMailer::Base.default_url_options = Rails.application.routes.default_url_options if Rails.env.production?` to their `config/initializers/host.rb` (and drop any hardcoded `config.action_mailer.default_url_options` host).
17
+ - The generated `Admin::UsersController` (`trek:install`) seeds a random password in `create_hook`, so users created via the admin form — which never collects one — satisfy `has_secure_password` and the non-null `password_digest` column. Invitees set their real password via the invite email; others via password reset. (Password seeding lives in the controller rather than `Trek::Users::Invitable` so it also covers users created without an invite.) **Upgrade note:** ensure your `Admin::UsersController#create_hook` seeds a password when blank: `@object.password = SecureRandom.alphanumeric(24) if @object.password.blank?`.
18
+ - `trek:install:ci` now sets `POSTGRES_DB` in `.gitlab-ci.yml` from the application name (e.g. `myapp_test`) instead of the hardcoded `project_test`.
19
+ - `trek:install:yarn` now rewrites `bin/setup`'s `yarn install --check-files` to `yarn install`, since `--check-files` is not a valid flag under Yarn berry and was breaking `bin/setup` (and therefore CI).
20
+ - `trek:install` now adds a `!/.env.example` exception to `.gitignore` so the generated `.env.example` template stays tracked (Rails ignores all `/.env*` files by default).
21
+ - `trek:install:postmark` now matches the production.rb SMTP comment with or without the `bin/` prefix, so the Postmark config is injected on Rails 8.0 as well as 8.1 (previously the injection silently did nothing when the anchor did not match).
22
+
3
23
  ## [0.0.1] - 2023-01-24
4
24
 
5
25
  - Initial release
data/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ AGENTS.md
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trek (0.1.24)
4
+ trek (0.1.25)
5
5
  action_policy (~> 0.6)
6
6
  actioncable
7
7
  acts_as_list (~> 1.1)
@@ -161,7 +161,7 @@ GEM
161
161
  concurrent-ruby (1.3.7)
162
162
  connection_pool (3.0.2)
163
163
  content_disposition (1.0.0)
164
- crass (1.0.6)
164
+ crass (1.0.7)
165
165
  css_parser (3.0.0)
166
166
  addressable
167
167
  ssrf_filter (~> 1.5)
@@ -516,7 +516,7 @@ GEM
516
516
  zeitwerk (>= 2.7)
517
517
  xpath (3.2.0)
518
518
  nokogiri (~> 1.8)
519
- yard (0.9.43)
519
+ yard (0.9.44)
520
520
  zeitwerk (2.7.5)
521
521
 
522
522
  PLATFORMS
data/Makefile CHANGED
@@ -5,6 +5,7 @@ release:
5
5
  if [ -z "$$version" ]; then echo "Version cannot be empty"; exit 1; fi; \
6
6
  sed -i '' "s/VERSION = \".*\"/VERSION = \"$$version\"/" lib/trek/version.rb; \
7
7
  sed -i '' "s/\"version\": \".*\"/\"version\": \"$$version\"/" package.json; \
8
+ awk -v v="$$version" -v d="$$(date +%F)" '!done && /^## \[Unreleased\]/ {print; print ""; print "## [" v "] - " d; done=1; next} {print}' CHANGELOG.md > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md; \
8
9
  bundle; \
9
10
  git add -A; \
10
11
  git commit -m "Version $$version"; \
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trek
4
+ module Filters
5
+ extend ActiveSupport::Concern
6
+
7
+ # Raised when a controller includes Trek::Filters but its #index never
8
+ # calls apply_filters! — to teach the developer to wire up filtering.
9
+ class NotAppliedError < StandardError
10
+ DEFAULT_MESSAGE = <<~MSG.squish
11
+ Trek::Filters is included but #index never called `apply_filters!`.
12
+ Add `apply_filters!` to your index action (or `@objects = apply_filters(@objects, params[:filters])`).
13
+ See https://trek.etamin.studio/reference/controllers#mixins
14
+ MSG
15
+
16
+ def initialize(message = DEFAULT_MESSAGE) = super
17
+ end
18
+
19
+ included do
20
+ # rubocop:disable Rails/LexicallyScopedActionFilter
21
+ after_action :ensure_filters_applied, only: :index
22
+ # rubocop:enable Rails/LexicallyScopedActionFilter
23
+ end
24
+
25
+ def apply_filters(scope, filters)
26
+ return scope if filters.blank?
27
+
28
+ filters.each do |filter, value|
29
+ next if value.blank?
30
+ value = nil if value == "nil"
31
+
32
+ scope = scope.where(filter => value)
33
+ end
34
+
35
+ scope
36
+ end
37
+
38
+ def apply_filters!
39
+ @filters_applied = true
40
+ @objects = apply_filters(@objects, params[:filters])
41
+ end
42
+
43
+ private
44
+
45
+ def ensure_filters_applied
46
+ return if @filters_applied
47
+ return if Rails.env.production?
48
+
49
+ raise NotAppliedError
50
+ end
51
+ end
52
+ end
@@ -4,6 +4,18 @@ module Trek
4
4
  module Scopes
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ # Raised when a controller includes Trek::Scopes but its #index never
8
+ # calls apply_scopes! — to teach the developer to wire up scoping.
9
+ class NotAppliedError < StandardError
10
+ DEFAULT_MESSAGE = <<~MSG.squish
11
+ Trek::Scopes is included but #index never called `apply_scopes!`.
12
+ Add `apply_scopes!` to your index action.
13
+ See https://trek.etamin.studio/reference/controllers#mixins
14
+ MSG
15
+
16
+ def initialize(message = DEFAULT_MESSAGE) = super
17
+ end
18
+
7
19
  def apply_scopes(scope, filters)
8
20
  return scope if filters.blank?
9
21
 
@@ -17,12 +29,17 @@ module Trek
17
29
  end
18
30
 
19
31
  def apply_scopes!
32
+ @scopes_applied = true
20
33
  @unscoped_objects = @objects
21
34
  @objects = apply_scopes(@objects, params[:scopes])
22
35
  scope_counts
23
36
  end
24
37
 
25
38
  included do
39
+ # rubocop:disable Rails/LexicallyScopedActionFilter
40
+ after_action :ensure_scopes_applied, only: :index
41
+ # rubocop:enable Rails/LexicallyScopedActionFilter
42
+
26
43
  helper_method :available_scopes
27
44
  helper_method :scope_params
28
45
  helper_method :unscoped_objects
@@ -51,5 +68,12 @@ module Trek
51
68
  def scope_params
52
69
  params[:scopes]&.permit(*available_scopes)
53
70
  end
71
+
72
+ private def ensure_scopes_applied
73
+ return if @scopes_applied
74
+ return if Rails.env.production?
75
+
76
+ raise NotAppliedError
77
+ end
54
78
  end
55
79
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Trek
2
4
  module Users
3
5
  module Invitable
@@ -143,6 +143,9 @@ fr:
143
143
  success: Action ajoutée
144
144
  form:
145
145
  title: Action
146
+ passwords_mailer:
147
+ reset:
148
+ subject: Nouveau mot de passe
146
149
  search_component:
147
150
  search: Rechercher
148
151
  search_user: Rechercher un utilisateur par e-mail
@@ -252,9 +255,6 @@ fr:
252
255
  admin_mailer:
253
256
  invite:
254
257
  subject: Votre invitation
255
- passwords_mailer:
256
- reset:
257
- subject: Nouveau mot de passe
258
258
  notices:
259
259
  authentication_needed: Veuillez vous connecter
260
260
  rate_limited: Trop de tentatives, veuillez réessayer plus tard
@@ -21,3 +21,75 @@ end
21
21
 
22
22
  - `include Trek::Pagination` — paginates the index with Kaminari
23
23
  - `include Trek::Search` — filters the index by `params[:search]` when the model is [Searchable](/reference/concerns/searchable)
24
+ - `include Trek::Filters` — filters the index by attribute equality (`where(column => value)`) from `params[:filters]`
25
+ - `include Trek::Scopes` — filters the index by **named scopes** from `params[:scopes]`
26
+
27
+ ### Filters
28
+
29
+ `Trek::Filters` lets users narrow an index by attribute values. Call `apply_filters!` in your `index` action — it
30
+ rewrites `@objects` from `params[:filters]`, applying a `where(column => value)` per non-blank filter (a value of
31
+ `"nil"` matches `NULL`):
32
+
33
+ ```ruby
34
+ module Admin
35
+ class CitiesController < Trek::ResourceController
36
+ include Trek::Filters
37
+
38
+ helper_method :countries
39
+
40
+ def index
41
+ super
42
+ apply_filters!
43
+ end
44
+
45
+ private
46
+
47
+ def countries
48
+ City.distinct.pluck(:country_code).index_with { |code| I18n.t("countries.#{code}") }
49
+ end
50
+ end
51
+ end
52
+ ```
53
+
54
+ Render the choices with a [`ButtonGroupComponent`](/reference/components), each button linking back to the index
55
+ with the chosen filter:
56
+
57
+ ```slim
58
+ = render Trek::ButtonGroupComponent.new do |g|
59
+ - g.with_button text: t("admin.actions.all"), \
60
+ href: [:admin, model], active: params.dig(:filters, :country_code).nil?
61
+ - countries.each do |code, name|
62
+ - g.with_button text: name, \
63
+ href: polymorphic_path([:admin, model], filters: { country_code: code }), \
64
+ active: code == params.dig(:filters, :country_code)
65
+ ```
66
+
67
+ > If a controller includes `Trek::Filters` but its `index` never calls `apply_filters!`, an
68
+ > `after_action` raises `Trek::Filters::NotAppliedError` (outside production) to flag the missing wiring.
69
+
70
+ ### Scopes
71
+
72
+ `Trek::Scopes` filters by **named model scopes** rather than raw attributes. Declare the allowed scopes via
73
+ `available_scopes`, then call `apply_scopes!` in your `index`. It also exposes `scope_counts` and
74
+ `unscoped_objects` helpers (the per-scope match counts and the pre-scope collection) for building scope tabs:
75
+
76
+ ```ruby
77
+ module Admin
78
+ class ArticlesController < Trek::ResourceController
79
+ include Trek::Scopes
80
+
81
+ def index
82
+ super
83
+ apply_scopes!
84
+ end
85
+
86
+ private
87
+
88
+ def available_scopes
89
+ %i[published draft]
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ Like `Trek::Filters`, forgetting to call `apply_scopes!` raises `Trek::Scopes::NotAppliedError` outside production.
@@ -63,6 +63,21 @@ module Trek
63
63
  def install_npm_dependencies(dependencies, dev: false)
64
64
  run "yarn add #{dependencies.map { |name, version| "#{name}@#{version}" }.join(" ")} #{dev ? "--dev" : ""}", abort_on_failure: true
65
65
  end
66
+
67
+ # Run "bundle install" outside the parent process's bundler environment.
68
+ # The generators run under `bundle exec`, so the child `bundle` process
69
+ # would otherwise auto-load `bundler/setup` against the Gemfile we just
70
+ # modified and raise GemNotFound for the not-yet-installed gem.
71
+ #
72
+ # We use with_original_env (not with_unbundled_env): both restore the
73
+ # pre-`bundle exec` environment, but with_unbundled_env also scrubs every
74
+ # BUNDLE_* var, which drops the credentials (e.g. BUNDLE_GIT__ETAMINSTUD__IO)
75
+ # needed to fetch trek itself from its private git source.
76
+ def bundle_install
77
+ Bundler.with_original_env do
78
+ run "bundle install --quiet", abort_on_failure: true
79
+ end
80
+ end
66
81
  end
67
82
  end
68
83
  end
@@ -0,0 +1,26 @@
1
+ require "rails/generators/actions"
2
+ require "generators/trek/helpers"
3
+
4
+ module Trek
5
+ module Generators
6
+ module Install
7
+ class AgentsGenerator < Rails::Generators::Base
8
+ include Trek::Generators::Helpers
9
+
10
+ desc "Create AGENTS.md and CLAUDE.md guidance files for AI coding agents."
11
+
12
+ source_root File.expand_path("../templates", __dir__)
13
+
14
+ def create_agents_md
15
+ copy_file "AGENTS.md", "AGENTS.md"
16
+ end
17
+
18
+ # CLAUDE.md imports AGENTS.md so Claude Code (which reads CLAUDE.md) and
19
+ # other agents (which read AGENTS.md) share a single source of truth.
20
+ def create_claude_md
21
+ create_file "CLAUDE.md", "See @AGENTS.md\n"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -13,13 +13,15 @@ module Trek
13
13
  source_root File.expand_path("../templates", __dir__)
14
14
 
15
15
  def add_gem
16
+ install_needed = %w[aws-sdk-s3 blurhash fastimage image_processing shrine shrine-blurhash].any? { |name| !in_gemfile?(name) }
17
+
16
18
  gem "aws-sdk-s3", "~> 1.117" unless in_gemfile?("aws-sdk-s3")
17
19
  gem "blurhash", "~> 0.1.7" unless in_gemfile?("blurhash")
18
20
  gem "fastimage", "~> 2.2" unless in_gemfile?("fastimage")
19
21
  gem "image_processing", "~> 1.8" unless in_gemfile?("image_processing")
20
22
  gem "shrine", "~> 3.0" unless in_gemfile?("shrine")
21
23
  gem "shrine-blurhash", "~> 0.2.0" unless in_gemfile?("shrine-blurhash")
22
- run "bundle install --quiet", abort_on_failure: true
24
+ bundle_install if install_needed
23
25
  end
24
26
 
25
27
  def add_initializer
@@ -16,8 +16,10 @@ module Trek
16
16
  source_root File.expand_path("../templates", __dir__)
17
17
 
18
18
  def add_bcrypt_gem
19
- gem "bcrypt", "~> 3.1.7" unless in_gemfile?("bcrypt")
20
- run "bundle install --quiet", abort_on_failure: true
19
+ return if in_gemfile?("bcrypt")
20
+
21
+ gem "bcrypt", "~> 3.1.7"
22
+ bundle_install
21
23
  end
22
24
 
23
25
  def copy_admin_authentication_concern
@@ -66,10 +68,16 @@ module Trek
66
68
  has_secure_password
67
69
  has_many :user_sessions, dependent: :destroy
68
70
 
71
+ # Both tokens invalidate once the password changes (password_salt). Invites
72
+ # get a longer window than a password reset, so they are a distinct purpose.
69
73
  generates_token_for :password_reset, expires_in: 15.minutes do
70
74
  password_salt&.last(10)
71
75
  end
72
76
 
77
+ generates_token_for :invite, expires_in: 1.week do
78
+ password_salt&.last(10)
79
+ end
80
+
73
81
  scope :privileged, -> { where(role: Trek::Users::Roles::PRIVILEGED_ROLES) }
74
82
 
75
83
  RUBY
@@ -13,8 +13,10 @@ module Trek
13
13
  source_root File.expand_path("../templates", __dir__)
14
14
 
15
15
  def add_gem
16
- gem "action_policy" unless in_gemfile?("action_policy")
17
- run "bundle install --quiet", abort_on_failure: true
16
+ return if in_gemfile?("action_policy")
17
+
18
+ gem "action_policy"
19
+ bundle_install
18
20
  end
19
21
 
20
22
  def run_install
@@ -12,8 +12,10 @@ module Trek
12
12
  source_root File.expand_path("../../../../", __dir__)
13
13
 
14
14
  def add_gem
15
- gem "brakeman", group: [:development, :test] unless in_gemfile?("brakeman")
16
- run "bundle install --quiet", abort_on_failure: true
15
+ return if in_gemfile?("brakeman")
16
+
17
+ gem "brakeman", group: [:development, :test]
18
+ bundle_install
17
19
  end
18
20
  end
19
21
  end
@@ -12,8 +12,10 @@ module Trek
12
12
  source_root File.expand_path("../../../../", __dir__)
13
13
 
14
14
  def add_gem
15
- gem "bundler-audit", group: [:development, :test] unless in_gemfile?("bundler-audit")
16
- run "bundle install --quiet", abort_on_failure: true
15
+ return if in_gemfile?("bundler-audit")
16
+
17
+ gem "bundler-audit", group: [:development, :test]
18
+ bundle_install
17
19
  end
18
20
  end
19
21
  end
@@ -22,6 +22,10 @@ module Trek
22
22
  Rails.application.routes.default_url_options[:protocol] = Rails.application.config.force_ssl ? "https" : "http"
23
23
  Rails.application.routes.default_url_options[:host] = ENV.fetch("DEFAULT_HOSTNAME", "localhost:3000")
24
24
  Rails.application.config.canonical_url = "\#{Rails.application.routes.default_url_options[:protocol]}://\#{Rails.application.routes.default_url_options[:host]}"
25
+
26
+ # Mailers run outside a request, so in production they need the same host for
27
+ # link helpers. Development and test keep the host from their environment files.
28
+ ActionMailer::Base.default_url_options = Rails.application.routes.default_url_options if Rails.env.production?
25
29
  RUBY
26
30
  end
27
31
  end
@@ -12,8 +12,10 @@ module Trek
12
12
  source_root File.expand_path("../templates", __dir__)
13
13
 
14
14
  def add_gem
15
- gem "rails-healthcheck" unless in_gemfile?("rails-healthcheck")
16
- run "bundle install --quiet", abort_on_failure: true
15
+ return if in_gemfile?("rails-healthcheck")
16
+
17
+ gem "rails-healthcheck"
18
+ bundle_install
17
19
  end
18
20
 
19
21
  def add_initializer
@@ -10,8 +10,10 @@ module Trek
10
10
  desc "Install Kaminari."
11
11
 
12
12
  def add_gem
13
- gem "kaminari" unless in_gemfile?("kaminari")
14
- run "bundle install --quiet", abort_on_failure: true
13
+ return if in_gemfile?("kaminari")
14
+
15
+ gem "kaminari"
16
+ bundle_install
15
17
  end
16
18
  end
17
19
  end
@@ -13,11 +13,13 @@ module Trek
13
13
  source_root File.expand_path("../../../../", __dir__)
14
14
 
15
15
  def add_gem
16
+ install_needed = %w[lookbook listen actioncable].any? { |name| !in_gemfile?(name) }
17
+
16
18
  gem "lookbook", ">= 2.3.13", group: :development unless in_gemfile?("lookbook")
17
19
  gem "listen", group: :development unless in_gemfile?("listen")
18
20
  gem "actioncable", group: :development unless in_gemfile?("actioncable")
19
21
 
20
- run "bundle install --quiet", abort_on_failure: true
22
+ bundle_install if install_needed
21
23
  end
22
24
  end
23
25
  end
@@ -55,8 +55,10 @@ module Trek
55
55
  end
56
56
 
57
57
  def install_closure_tree
58
- gem "closure_tree", "~> 9.0" unless in_gemfile?("closure_tree")
59
- run "bundle install --quiet", abort_on_failure: true
58
+ unless in_gemfile?("closure_tree")
59
+ gem "closure_tree", "~> 9.0"
60
+ bundle_install
61
+ end
60
62
  rails_command "db:migrate" # `pages` table must exist
61
63
  generate "closure_tree:migration Page"
62
64
  end
@@ -13,8 +13,10 @@ module Trek
13
13
  source_root File.expand_path("../templates", __dir__)
14
14
 
15
15
  def add_gem
16
- gem "postmark-rails" unless in_gemfile?("postmark-rails")
17
- run "bundle install --quiet", abort_on_failure: true
16
+ return if in_gemfile?("postmark-rails")
17
+
18
+ gem "postmark-rails"
19
+ bundle_install
18
20
  end
19
21
 
20
22
  def inject_postmark_to_application_mailer
@@ -28,7 +30,7 @@ module Trek
28
30
  def inject_postmark_to_config
29
31
  inject_into_file "config/environments/production.rb",
30
32
  config_injection,
31
- after: "# Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.\n"
33
+ after: %r{# Specify outgoing SMTP server\. Remember to add smtp/\* credentials via (?:bin/)?rails credentials:edit\.\n}
32
34
  run "rubocop --autocorrect config/environments/production.rb", abort_on_failure: true
33
35
  end
34
36
 
@@ -12,8 +12,10 @@ module Trek
12
12
  source_root File.expand_path("../../../../", __dir__)
13
13
 
14
14
  def add_gem
15
- gem "rubocop-rspec", group: [:development, :test], require: false unless in_gemfile?("rubocop-rspec")
16
- run "bundle install --quiet", abort_on_failure: true
15
+ return if in_gemfile?("rubocop-rspec")
16
+
17
+ gem "rubocop-rspec", group: [:development, :test], require: false
18
+ bundle_install
17
19
  end
18
20
 
19
21
  def copy_rubocop_config
@@ -21,8 +21,9 @@ module Trek
21
21
  def create_aptfile
22
22
  create_file "Aptfile" do
23
23
  <<~TEXT
24
- libwebp-dev
24
+ libjemalloc2
25
25
  libvips-tools --no-install-recommends
26
+ libwebp-dev
26
27
  TEXT
27
28
  end
28
29
  end
@@ -12,8 +12,10 @@ module Trek
12
12
  source_root File.expand_path("../../../../", __dir__)
13
13
 
14
14
  def add_gem
15
- gem "slim_lint", group: [:development, :test] unless in_gemfile?("slim_lint")
16
- run "bundle install --quiet", abort_on_failure: true
15
+ return if in_gemfile?("slim_lint")
16
+
17
+ gem "slim_lint", group: [:development, :test]
18
+ bundle_install
17
19
  end
18
20
 
19
21
  def copy_slim_lint_config
@@ -13,13 +13,14 @@ module Trek
13
13
  source_root File.expand_path("../templates", __dir__)
14
14
 
15
15
  def add_gems
16
+ install_needed = %w[rspec-rails capybara rspec_junit_formatter rspec-html-matchers simplecov].any? { |name| !in_gemfile?(name) }
17
+
16
18
  gem "rspec-rails", "~> 8.0", group: [:development, :test] unless in_gemfile?("rspec-rails")
17
19
  gem "capybara", "~> 3.38", group: [:test] unless in_gemfile?("capybara")
18
20
  gem "rspec_junit_formatter", "~> 0.6.0", group: [:test] unless in_gemfile?("rspec_junit_formatter")
19
21
  gem "rspec-html-matchers", "~> 0.10.0", group: [:test] unless in_gemfile?("rspec-html-matchers")
20
22
  gem "simplecov", "~> 0.22.0", group: [:test] unless in_gemfile?("simplecov")
21
-
22
- run "bundle install --quiet", abort_on_failure: true
23
+ bundle_install if install_needed
23
24
  end
24
25
 
25
26
  def run_install
@@ -12,8 +12,10 @@ module Trek
12
12
  source_root File.expand_path("../templates", __dir__)
13
13
 
14
14
  def add_gem
15
- gem "strip_attributes" unless in_gemfile?("strip_attributes")
16
- run "bundle install --quiet", abort_on_failure: true
15
+ return if in_gemfile?("strip_attributes")
16
+
17
+ gem "strip_attributes"
18
+ bundle_install
17
19
  end
18
20
  end
19
21
  end
@@ -13,11 +13,13 @@ module Trek
13
13
  source_root File.expand_path("../templates", __dir__)
14
14
 
15
15
  def add_gem
16
+ install_needed = %w[rails-i18n i18n-tasks deepl-rb mobility].any? { |name| !in_gemfile?(name) }
17
+
16
18
  gem "rails-i18n", ">= 6.0.0" unless in_gemfile?("rails-i18n")
17
19
  gem "i18n-tasks", "~> 1.0" unless in_gemfile?("i18n-tasks")
18
20
  gem "deepl-rb", "~> 3.0" unless in_gemfile?("deepl-rb")
19
21
  gem "mobility", "~> 1.3.2" unless in_gemfile?("mobility")
20
- run "bundle install --quiet", abort_on_failure: true
22
+ bundle_install if install_needed
21
23
  end
22
24
 
23
25
  def run_install
@@ -45,6 +45,14 @@ module Trek
45
45
  run "yarn plugin import interactive-tools"
46
46
  end
47
47
 
48
+ # The default bin/setup (generated by `rails new -j esbuild`) runs
49
+ # `yarn install --check-files`, but `--check-files` is not a valid flag
50
+ # under Yarn berry (the version we pin above), so it breaks `bin/setup`
51
+ # in CI. Drop the flag.
52
+ def fix_bin_setup
53
+ gsub_file "bin/setup", "yarn install --check-files", "yarn install"
54
+ end
55
+
48
56
  private
49
57
 
50
58
  def yarnrc_content
@@ -52,6 +52,9 @@ module Trek
52
52
  <<~GITIGNORE
53
53
 
54
54
  .DS_Store
55
+
56
+ # Keep the .env template tracked (Rails ignores all /.env* files).
57
+ !/.env.example
55
58
  GITIGNORE
56
59
  end
57
60
 
@@ -121,6 +124,10 @@ module Trek
121
124
  generate "trek:install:readme"
122
125
  end
123
126
 
127
+ def install_agents
128
+ generate "trek:install:agents"
129
+ end
130
+
124
131
  def install_admin
125
132
  generate "trek:install:admin"
126
133
  generate "trek:install:admin:routes"
@@ -3,7 +3,7 @@
3
3
 
4
4
  # Variables
5
5
  variables:
6
- POSTGRES_DB: "project_test"
6
+ POSTGRES_DB: "<%= application_name %>_test"
7
7
  POSTGRES_HOST_AUTH_METHOD: "trust"
8
8
  DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
9
9
  RAILS_ENV: "test"
@@ -0,0 +1,36 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI coding agents working in this repository.
4
+
5
+ ## About this project
6
+
7
+ This is a Ruby on Rails application whose back-office/admin is built with **Trek**, a Rails CMS engine. Trek was installed via `rails g trek:install` and provides the admin UI, authentication, authorization, attachments, and base content models. Project-specific work lives in `app/` — admin controllers under `app/controllers/admin/`, plus your own models, policies, and components. Prefer Trek's generators (`rails g trek:scaffold`, `rails g trek:admin:user`, …) over hand-writing admin plumbing.
8
+
9
+ ## Commands
10
+
11
+ make setup # install dependencies (yarn + bin/setup)
12
+ make dev # start the server (overmind)
13
+ make admin # create an admin user
14
+ make seed # seed the database
15
+ make test # run the RSpec suite (alias: make spec)
16
+ make lint # rubocop + slim-lint + eslint + stylelint + prettier
17
+ make fix # auto-correct lint/format issues
18
+ make scan # brakeman + bundle-audit
19
+
20
+ ## Conventions
21
+
22
+ - **Ruby**: RuboCop with rubocop-rails-omakase — double-quoted strings, 120-col lines.
23
+ - **Views**: Slim templates + ViewComponents (linted with slim-lint).
24
+ - **Assets**: esbuild + PostCSS; Stimulus/Turbo (Hotwired).
25
+ - **i18n**: keep every locale file in `config/locales/` in sync across languages.
26
+ - **Git hooks**: lefthook auto-formats staged files on pre-commit — let it run.
27
+
28
+ ## Upgrading Trek
29
+
30
+ **Whenever you bump the `trek` gem — and its companion `@etaminstudio/trek` npm package — always:**
31
+
32
+ 1. **Read Trek's `CHANGELOG.md`** for every version between the installed and target version, not just the latest.
33
+ 2. **Apply every "Upgrade note".** These are the manual steps host apps must perform (config tweaks, new initializers, regenerated files, data migrations). Do not skip them — the upgrade is not complete until each is applied.
34
+ 3. **Remove now-redundant local code.** Where the changelog says a Trek change lets host apps drop or simplify a local patch, monkey-patch, or custom implementation, delete that code as part of the upgrade.
35
+ 4. **Keep versions aligned**: the `trek` gem version and the `@etaminstudio/trek` npm version must match.
36
+ 5. **Verify**: run `make lint` and `make test` before committing the upgrade.
@@ -42,8 +42,11 @@ module Admin
42
42
  @token = params[:token]
43
43
  end
44
44
 
45
+ # The same edit/update flow is reached from a password-reset email and from
46
+ # an invitation email, which carry different token purposes (and expiries).
45
47
  def set_user_by_token
46
- @user = User.privileged.find_by_token_for!(:password_reset, @token)
48
+ @user = User.privileged.find_by_token_for(:invite, @token) ||
49
+ User.privileged.find_by_token_for!(:password_reset, @token)
47
50
  rescue ActiveRecord::RecordNotFound
48
51
  redirect_to new_admin_user_session_path, alert: t("admin.user_password_resets.not_found")
49
52
  rescue ActiveSupport::MessageVerifier::InvalidSignature
@@ -5,6 +5,13 @@ module Admin
5
5
  @object.role = "editor"
6
6
  end
7
7
 
8
+ # The form never collects a password — created users set their own through
9
+ # the invite/reset email — so seed a random one to satisfy
10
+ # has_secure_password and the non-null password_digest column.
11
+ def create_hook
12
+ @object.password = SecureRandom.alphanumeric(24) if @object.password.blank?
13
+ end
14
+
8
15
  def theme
9
16
  value = params[:theme].to_s
10
17
  Current.user.update!(theme: value) if User.themes.key?(value)
@@ -4,7 +4,7 @@ class Admin::PasswordsMailer < ApplicationMailer
4
4
  def reset(user)
5
5
  @user = user
6
6
  @url = edit_admin_user_password_reset_url(@user.generate_token_for(:password_reset))
7
- @expiration_delay = distance_of_time_in_words(0, 15.minutes)
7
+ @expiration_delay = distance_of_time_in_words(0, User.token_definitions[:password_reset].expires_in)
8
8
 
9
9
  I18n.with_locale(@user.locale || I18n.default_locale) do
10
10
  mail to: @user.email
@@ -1,7 +1,10 @@
1
1
  class AdminMailer < ApplicationMailer
2
+ include ActionView::Helpers::DateHelper
3
+
2
4
  def invite(user)
3
5
  @user = user
4
- @url = edit_admin_user_password_reset_url(@user.generate_token_for(:password_reset))
6
+ @url = edit_admin_user_password_reset_url(@user.generate_token_for(:invite))
7
+ @expiration_delay = distance_of_time_in_words(0, User.token_definitions[:invite].expires_in)
5
8
 
6
9
  I18n.with_locale(@user.locale || I18n.default_locale) do
7
10
  mail to: @user.email
@@ -25,5 +25,6 @@
25
25
  text: i18n_resource_label(:search, User).sub(" ", "\n"),
26
26
  icon: "trek/search",
27
27
  color: "purple",
28
- href: "#", # %i[admin users],
28
+ href: %i[admin users],
29
+ allowed_to: allowed_to?(:index?, User),
29
30
  )
@@ -4,5 +4,6 @@ p = link_to "Choose your password",
4
4
  @url,
5
5
  target: "_blank",
6
6
  rel: "noopener"
7
+ p This link will expire in #{@expiration_delay}.
7
8
  p Have a great day!
8
9
  p The #{t("application_name")} team
@@ -5,5 +5,7 @@ You have been invited to <%%= t("application_name") %>.
5
5
  To choose your password, just follow this link:
6
6
  <%%= @url %>
7
7
 
8
+ This link will expire in <%%= @expiration_delay %>.
9
+
8
10
  Have a great day!
9
11
  The <%%= t("application_name") %> team
@@ -4,5 +4,6 @@ p = link_to "Choisir votre mot de passe",
4
4
  @url,
5
5
  target: "_blank",
6
6
  rel: "noopener"
7
+ p Ce lien expire dans #{@expiration_delay}.
7
8
  p Bonne journée !
8
9
  p L'équipe #{t("application_name")}
@@ -5,5 +5,7 @@ Vous avez été invité·e sur <%%= t("application_name") %>.
5
5
  Pour choisir votre mot de passe, suivez ce lien :
6
6
  <%%= @url %>
7
7
 
8
+ Ce lien expire dans <%%= @expiration_delay %>.
9
+
8
10
  Bonne journée !
9
11
  L'équipe <%%= t("application_name") %>
data/lib/trek/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trek
4
- VERSION = "0.1.24"
4
+ VERSION = "0.1.25"
5
5
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etaminstudio/trek",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "A modern CMS for Ruby on Rails",
5
5
  "main": "app/javascript/trek.js",
6
6
  "repository": {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trek
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.24
4
+ version: 0.1.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mohamed Bengrich
@@ -441,7 +441,9 @@ files:
441
441
  - ".yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs"
442
442
  - ".yarn/releases/yarn-3.8.0.cjs"
443
443
  - ".yarnrc.yml"
444
+ - AGENTS.md
444
445
  - CHANGELOG.md
446
+ - CLAUDE.md
445
447
  - Gemfile
446
448
  - Gemfile.lock
447
449
  - LICENSE
@@ -635,6 +637,7 @@ files:
635
637
  - app/components/trek/toaster_component/toaster_component.js
636
638
  - app/controllers/concerns/trek/authentication.rb
637
639
  - app/controllers/concerns/trek/authorization.rb
640
+ - app/controllers/concerns/trek/filters.rb
638
641
  - app/controllers/concerns/trek/hooks.rb
639
642
  - app/controllers/concerns/trek/localization.rb
640
643
  - app/controllers/concerns/trek/model.rb
@@ -786,6 +789,7 @@ files:
786
789
  - lib/generators/trek/install/admin/user_sessions_generator.rb
787
790
  - lib/generators/trek/install/admin/users_generator.rb
788
791
  - lib/generators/trek/install/admin_generator.rb
792
+ - lib/generators/trek/install/agents_generator.rb
789
793
  - lib/generators/trek/install/application_generator.rb
790
794
  - lib/generators/trek/install/assets_generator.rb
791
795
  - lib/generators/trek/install/attachments_generator.rb
@@ -832,6 +836,7 @@ files:
832
836
  - lib/generators/trek/scaffold_generator.rb
833
837
  - lib/generators/trek/taxonomies_generator.rb
834
838
  - lib/generators/trek/templates/.gitlab-ci.yml
839
+ - lib/generators/trek/templates/AGENTS.md
835
840
  - lib/generators/trek/templates/Makefile
836
841
  - lib/generators/trek/templates/README.md
837
842
  - lib/generators/trek/templates/components/index.js