用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/UitbreidenOS/UitKit --skill rails命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
Guidelines and instructions for Agent execution state rollback rules
Guidelines and instructions for Agent execution step counters limits
Guidelines and instructions for Agent execution timeout limits setups
基于 SOC 职业分类
正在显示 SKILL.md
| name | rails |
| description | - Building or maintaining a Rails 7+ application |
Add the bullet gem to development:
# Gemfile
gem "bullet", group: :development
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.rails_logger = true
Bullet.add_footer = true
end
Fix detected N+1s with includes (or preload/eager_load when you need to filter on the association):
# N+1: each post fires a query for its author
Post.all.each { |p| p.author.name }
# Fixed
Post.includes(:author).each { |p| p.author.name }
# When filtering on the association use eager_load (LEFT OUTER JOIN)
Post.eager_load(:author).where(authors: { active: true })
# Use select to limit columns fetched
Post.includes(:author).select("posts.id, posts.title, authors.name")
Add query count assertions to request specs to catch regressions:
expect { get "/posts" }.to make_database_queries(count: 2)
Turbo Frames replace a bounded region of the page with the server response. One frame per request.
<%# app/views/posts/index.html.erb %>
<%= turbo_frame_tag "posts_list" do %>
<%= render @posts %>
<% end %>
<%# Clicking this link replaces the frame content, not the whole page %>
<%= link_to "Load more", posts_path(page: 2), data: { turbo_frame: "posts_list" } %>
Turbo Streams push multiple fine-grained DOM mutations from one response (or from ActionCable). Use when a single action must update several parts of the page simultaneously.
# app/controllers/posts_controller.rb
def create
@post = Post.create!(post_params)
respond_to do |format|
format.turbo_stream # renders create.turbo_stream.erb
format.html { redirect_to posts_path }
end
end
<%# app/views/posts/create.turbo_stream.erb %>
<%= turbo_stream.prepend "posts_list", partial: "posts/post", locals: { post: @post } %>
<%= turbo_stream.replace "post_count", partial: "posts/count" %>
<%= turbo_stream.update "flash", partial: "shared/flash" %>
Decision rule: use Frames for simple navigation replacements; use Streams when one action mutates multiple DOM targets or when broadcasting updates over ActionCable.
// app/javascript/controllers/toggle_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["menu"]
static values = { open: Boolean }
// Lifecycle callbacks
connect() { /* DOM is ready, controller connected */ }
disconnect() { /* controller removed from DOM */ }
// Action handler
toggle() {
this.openValue = !this.openValue
}
// Value change callback (fires on connect and on every mutation)
openValueChanged(value) {
this.menuTarget.hidden = !value
}
}
Wire up in HTML:
<div data-controller="toggle">
<button data-action="click->toggle#toggle">Menu</button>
<nav data-toggle-target="menu" hidden>...</nav>
</div>
Keep controllers small and composable. One controller per behavior unit, not per page.
# app/jobs/invoice_mailer_job.rb
class InvoiceMailerJob
include Sidekiq::Job
sidekiq_options queue: :mailers, retry: 5, backtrace: true
sidekiq_retries_exhausted do |msg, ex|
Sentry.capture_exception(ex, extra: { job: msg })
# notify or dead-letter here
end
def perform(invoice_id)
invoice = Invoice.find(invoice_id)
InvoiceMailer.with(invoice:).receipt.deliver_now
end
end
# Enqueue
InvoiceMailerJob.perform_async(invoice.id)
InvoiceMailerJob.perform_in(5.minutes, invoice.id)
InvoiceMailerJob.set(queue: :critical).perform_async(invoice.id)
Configure retry back-off in config/initializers/sidekiq.rb:
Sidekiq.configure_server do |config|
config.redis = { url: ENV.fetch("REDIS_URL") }
end
Sidekiq.configure_client do |config|
config.redis = { url: ENV.fetch("REDIS_URL") }
end
Sidekiq Pro/Enterprise is needed for true unique jobs — for open-source, use sidekiq-unique-jobs.
Rails 7 uses per-environment credentials. Edit with:
EDITOR=vim rails credentials:edit --environment production
Access in code:
Rails.application.credentials.dig(:stripe, :secret_key)
Rails.application.credentials.sendgrid_api_key! # raises if missing
Never commit config/master.key or config/credentials/production.key. Add them to .gitignore and supply via environment variable (RAILS_MASTER_KEY) in CI and deployment.
Request specs test the full HTTP stack without mounting the full Rack server. Prefer them over controller specs.
# spec/requests/posts_spec.rb
require "rails_helper"
RSpec.describe "Posts", type: :request do
let(:user) { create(:user) }
before { sign_in user } # Devise test helper
describe "GET /posts" do
let!(:posts) { create_list(:post, 3, author: user) }
it "returns published posts" do
get posts_path
expect(response).to have_http_status(:ok)
expect(response.body).to include(posts.first.title)
end
end
describe "POST /posts" do
context "with valid params" do
it "creates a post and redirects" do
expect {
post posts_path, params: { post: attributes_for(:post) }
}.to change(Post, :count).by(1)
expect(response).to redirect_to(Post.last)
end
end
context "with invalid params" do
it "renders unprocessable entity" do
post posts_path, params: { post: { title: "" } }
expect(response).to have_http_status(:unprocessable_entity)
FactoryBot conventions:
spec/factories/create for DB-persisted records, build for in-memorycreate_list for collectionsattributes_for to get a plain hash for params# config/deploy.yml
service: myapp
image: registry.example.com/myapp
servers:
web:
hosts:
- 203.0.113.10
labels:
traefik.http.routers.myapp.rule: Host(`myapp.example.com`)
worker:
hosts:
- 203.0.113.11
cmd: bundle exec sidekiq
registry:
server: registry.example.com
username:
- KAMAL_REGISTRY_USERNAME
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
RAILS_ENV: production
WEB_CONCURRENCY: "2"
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
- REDIS_URL
accessories:
db:
image: postgres:16
host: 203.0.113.10
port:
Deploy:
kamal setup # first deploy — provisions and starts everything
kamal deploy # subsequent deploys
kamal rollback # roll back to previous image
kamal app logs -f # tail logs
Adding a CommentJob that posts a Slack notification after a comment is created, tested end-to-end:
# app/jobs/comment_notify_job.rb
class CommentNotifyJob
include Sidekiq::Job
sidekiq_options queue: :default, retry: 3
def perform(comment_id)
comment = Comment.includes(:post, :author).find(comment_id)
SlackNotifier.post(
channel: "#activity",
text: "#{comment.author.name} commented on #{comment.post.title}"
)
end
end
# app/models/comment.rb
after_create_commit -> { CommentNotifyJob.perform_async(id) }
# spec/jobs/comment_notify_job_spec.rb
RSpec.describe CommentNotifyJob, type: :job do
it "posts to Slack" do
comment = create(:comment)
allow(SlackNotifier).to receive(:post)
described_class.new.perform(comment.id)
expect(SlackNotifier).to have_received(:post).with(
hash_including(channel: "#activity")
)
end
end