outset 0.1.0
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.
- checksums.yaml +7 -0
- data/exe/outset +6 -0
- data/lib/outset/cli.rb +53 -0
- data/lib/outset/commands/config_cmd.rb +47 -0
- data/lib/outset/commands/doctor.rb +61 -0
- data/lib/outset/commands/new.rb +235 -0
- data/lib/outset/config.rb +108 -0
- data/lib/outset/recipes.rb +55 -0
- data/lib/outset/ui.rb +27 -0
- data/lib/outset/version.rb +5 -0
- data/lib/outset.rb +7 -0
- metadata +150 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 863fabe79965bc0d21477932bf97e165ad54fbcd385ba972d198f40dec78e3df
|
|
4
|
+
data.tar.gz: 3ea461e5243abbff2cf8ed693b919edc7d23467c7c039255f3bbf914009fb7df
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3faf1b252d4b2b15126422ba2c83771be0a39fcb865194526d654a421a408062e3542e560bf4f5a6aec4926b1583eed8d5931915d22bb112fab8f69b1ac172b2
|
|
7
|
+
data.tar.gz: 3d06676a5c1a7b330b90ad703b530bd963c2d2384f902a42d492b9200e28342ce6d5bd22e2f18b00433a69e08241d2fe1665b53a101d4cc2362217a73ac96af4
|
data/exe/outset
ADDED
data/lib/outset/cli.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "commands/new"
|
|
5
|
+
require_relative "commands/config_cmd"
|
|
6
|
+
require_relative "commands/doctor"
|
|
7
|
+
require_relative "recipes"
|
|
8
|
+
|
|
9
|
+
module Outset
|
|
10
|
+
class CLI < Thor
|
|
11
|
+
def self.exit_on_failure? = true
|
|
12
|
+
|
|
13
|
+
desc "new APP_NAME", "Bootstrap a new Rails application"
|
|
14
|
+
option :database, aliases: "-d", type: :string, default: nil, desc: "Database (postgresql, mysql, sqlite3)"
|
|
15
|
+
option :css, aliases: "-c", type: :string, default: nil, desc: "CSS framework (tailwind, bootstrap, sass, postcss, none)"
|
|
16
|
+
option :js, aliases: "-j", type: :string, default: nil, desc: "JavaScript bundler (importmap, esbuild, bun, webpack, rollup)"
|
|
17
|
+
option :recipe, aliases: "-r", type: :string, default: nil, desc: "Use a predefined recipe"
|
|
18
|
+
option :yes, aliases: "-y", type: :boolean, default: false, desc: "Accept all defaults, skip prompts"
|
|
19
|
+
def new(app_name)
|
|
20
|
+
UI.banner
|
|
21
|
+
Commands::New.new(app_name, options).run
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "config [ACTION]", "View or edit your outset config (~/.outset/config.toml)"
|
|
25
|
+
def config(action = "show")
|
|
26
|
+
Commands::ConfigCmd.new(action).run
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "doctor", "Check that your environment is ready to use outset"
|
|
30
|
+
def doctor
|
|
31
|
+
UI.banner
|
|
32
|
+
Commands::Doctor.new.run
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "recipes", "List available recipes"
|
|
36
|
+
def recipes
|
|
37
|
+
UI.info("Available recipes:")
|
|
38
|
+
puts
|
|
39
|
+
Recipes.all.each do |name, recipe|
|
|
40
|
+
puts " #{UI::PASTEL.bold(name.ljust(10))} #{recipe[:description]}"
|
|
41
|
+
UI.muted(" db=#{recipe[:database]} css=#{recipe[:css]} js=#{recipe[:js]} gems=#{recipe[:gems].empty? ? "none" : recipe[:gems].join(", ")}")
|
|
42
|
+
puts
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "version", "Print outset version"
|
|
47
|
+
def version
|
|
48
|
+
puts "outset v#{Outset::VERSION}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
map %w[--version -v] => :version
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Outset
|
|
4
|
+
module Commands
|
|
5
|
+
class ConfigCmd
|
|
6
|
+
def initialize(action)
|
|
7
|
+
@action = action
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
case @action
|
|
12
|
+
when "show" then show
|
|
13
|
+
when "init" then Config.init!
|
|
14
|
+
when "edit" then edit
|
|
15
|
+
when "path" then puts Config::CONFIG_FILE
|
|
16
|
+
else
|
|
17
|
+
UI.error("Unknown config action: '#{@action}'")
|
|
18
|
+
UI.muted(" Available: show, init, edit, path")
|
|
19
|
+
exit(1)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def show
|
|
26
|
+
if File.exist?(Config::CONFIG_FILE)
|
|
27
|
+
UI.info("Config file: #{Config::CONFIG_FILE}")
|
|
28
|
+
puts
|
|
29
|
+
puts File.read(Config::CONFIG_FILE)
|
|
30
|
+
else
|
|
31
|
+
UI.warn("No config file found. Run `outset config init` to create one.")
|
|
32
|
+
puts
|
|
33
|
+
UI.muted("Default values:")
|
|
34
|
+
puts Config.default_toml
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def edit
|
|
39
|
+
unless File.exist?(Config::CONFIG_FILE)
|
|
40
|
+
Config.init!
|
|
41
|
+
end
|
|
42
|
+
editor = ENV["VISUAL"] || ENV["EDITOR"] || "nano"
|
|
43
|
+
system("#{editor} #{Config::CONFIG_FILE}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Outset
|
|
4
|
+
module Commands
|
|
5
|
+
class Doctor
|
|
6
|
+
CHECKS = [
|
|
7
|
+
{
|
|
8
|
+
name: "Ruby >= 3.1",
|
|
9
|
+
check: -> { RUBY_VERSION >= "3.1.0" },
|
|
10
|
+
fix: "Install Ruby 3.1+ via rbenv or asdf"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "Rails installed",
|
|
14
|
+
check: -> { system("which rails > /dev/null 2>&1") },
|
|
15
|
+
fix: "Run: gem install rails"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "Git installed",
|
|
19
|
+
check: -> { system("which git > /dev/null 2>&1") },
|
|
20
|
+
fix: "Install git from https://git-scm.com"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "Git user.name configured",
|
|
24
|
+
check: -> { !`git config user.name`.strip.empty? rescue false },
|
|
25
|
+
fix: "Run: git config --global user.name 'Your Name'"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "Git user.email configured",
|
|
29
|
+
check: -> { !`git config user.email`.strip.empty? rescue false },
|
|
30
|
+
fix: "Run: git config --global user.email 'you@example.com'"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "Outset config file",
|
|
34
|
+
check: -> { File.exist?(Config::CONFIG_FILE) },
|
|
35
|
+
fix: "Run: outset config init"
|
|
36
|
+
}
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
def run
|
|
40
|
+
UI.info("Checking your environment...\n")
|
|
41
|
+
all_passed = true
|
|
42
|
+
|
|
43
|
+
CHECKS.each do |check|
|
|
44
|
+
if check[:check].call
|
|
45
|
+
UI.success(check[:name])
|
|
46
|
+
else
|
|
47
|
+
UI.warn("#{check[:name]} → #{check[:fix]}")
|
|
48
|
+
all_passed = false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
puts
|
|
53
|
+
if all_passed
|
|
54
|
+
UI.success("All checks passed. You're ready to use outset!")
|
|
55
|
+
else
|
|
56
|
+
UI.warn("Some checks failed. Fix the issues above and run `outset doctor` again.")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-prompt"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module Outset
|
|
7
|
+
module Commands
|
|
8
|
+
class New
|
|
9
|
+
DATABASES = %w[postgresql mysql sqlite3].freeze
|
|
10
|
+
CSS_OPTIONS = %w[tailwind bootstrap sass postcss none].freeze
|
|
11
|
+
JS_OPTIONS = %w[importmap esbuild bun webpack rollup].freeze
|
|
12
|
+
|
|
13
|
+
OPTIONAL_GEMS = [
|
|
14
|
+
{ name: "Devise (Authentication)", value: "devise" },
|
|
15
|
+
{ name: "Pundit (Authorization)", value: "pundit" },
|
|
16
|
+
{ name: "Sidekiq (Background Jobs)", value: "sidekiq" },
|
|
17
|
+
{ name: "RSpec + FactoryBot (Tests)", value: "rspec" },
|
|
18
|
+
{ name: "Annotate (Model Annotations)", value: "annotate" },
|
|
19
|
+
{ name: "Letter Opener (Email preview)",value: "letter_opener" },
|
|
20
|
+
{ name: "Pagy (Pagination)", value: "pagy" },
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def initialize(app_name, options = {})
|
|
24
|
+
@app_name = app_name
|
|
25
|
+
@options = options
|
|
26
|
+
@resolved = Config.resolve(options)
|
|
27
|
+
@prompt = TTY::Prompt.new(interrupt: :exit)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def run
|
|
31
|
+
validate_app_name!
|
|
32
|
+
validate_rails_installed!
|
|
33
|
+
|
|
34
|
+
selections = if effective_recipe
|
|
35
|
+
recipe_selections
|
|
36
|
+
elsif @options[:yes]
|
|
37
|
+
default_selections
|
|
38
|
+
else
|
|
39
|
+
prompt_user
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
confirm_and_run(selections)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def effective_recipe
|
|
48
|
+
@options[:recipe] || @resolved["default_recipe"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_app_name!
|
|
52
|
+
unless @app_name.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
53
|
+
UI.error("Invalid app name: '#{@app_name}'")
|
|
54
|
+
UI.muted(" App names must start with a letter and contain only lowercase letters, numbers, and underscores.")
|
|
55
|
+
exit(1)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if Dir.exist?(@app_name)
|
|
59
|
+
UI.error("Directory '#{@app_name}' already exists.")
|
|
60
|
+
exit(1)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_rails_installed!
|
|
65
|
+
unless system("which rails > /dev/null 2>&1")
|
|
66
|
+
UI.error("Rails is not installed. Run: gem install rails")
|
|
67
|
+
exit(1)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def recipe_selections
|
|
72
|
+
name = effective_recipe
|
|
73
|
+
recipe = Recipes.find(name)
|
|
74
|
+
UI.info("Using recipe: #{name} — #{recipe[:description]}")
|
|
75
|
+
puts
|
|
76
|
+
{
|
|
77
|
+
database: @options[:database] || recipe[:database],
|
|
78
|
+
css: @options[:css] || recipe[:css],
|
|
79
|
+
js: @options[:js] || recipe[:js],
|
|
80
|
+
gems: (recipe[:gems] + @resolved["gems"]).uniq
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def default_selections
|
|
85
|
+
{
|
|
86
|
+
database: @resolved["database"],
|
|
87
|
+
css: @resolved["css"],
|
|
88
|
+
js: @resolved["javascript"],
|
|
89
|
+
gems: @resolved["gems"]
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def prompt_user
|
|
94
|
+
UI.info("Configuring: #{@app_name}")
|
|
95
|
+
puts
|
|
96
|
+
|
|
97
|
+
database = if @options[:database]
|
|
98
|
+
UI.muted(" Database: #{@options[:database]} (from flag)")
|
|
99
|
+
@options[:database]
|
|
100
|
+
else
|
|
101
|
+
@prompt.select("Database:", DATABASES, default: @resolved["database"])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
css = if @options[:css]
|
|
105
|
+
UI.muted(" CSS: #{@options[:css]} (from flag)")
|
|
106
|
+
@options[:css]
|
|
107
|
+
else
|
|
108
|
+
@prompt.select("CSS framework:", CSS_OPTIONS, default: @resolved["css"])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
js = if @options[:js]
|
|
112
|
+
UI.muted(" JS: #{@options[:js]} (from flag)")
|
|
113
|
+
@options[:js]
|
|
114
|
+
else
|
|
115
|
+
@prompt.select("JavaScript bundler:", JS_OPTIONS, default: @resolved["javascript"])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
always_gems = @resolved["gems"]
|
|
119
|
+
gems = @prompt.multi_select("Optional gems: (space to select, enter to confirm)") do |menu|
|
|
120
|
+
OPTIONAL_GEMS.each do |gem_opt|
|
|
121
|
+
preselected = always_gems.include?(gem_opt[:value])
|
|
122
|
+
menu.choice gem_opt[:name], gem_opt[:value], disabled: (preselected ? "(always)" : false)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
gems += always_gems
|
|
126
|
+
|
|
127
|
+
{ database: database, css: css, js: js, gems: gems.uniq }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def confirm_and_run(selections)
|
|
131
|
+
puts
|
|
132
|
+
UI.info("Ready to create '#{@app_name}' with:")
|
|
133
|
+
UI.muted(" Recipe : #{effective_recipe}") if effective_recipe
|
|
134
|
+
UI.muted(" Database : #{selections[:database]}")
|
|
135
|
+
UI.muted(" CSS : #{selections[:css]}")
|
|
136
|
+
UI.muted(" JS : #{selections[:js]}")
|
|
137
|
+
UI.muted(" Gems : #{selections[:gems].empty? ? "none" : selections[:gems].join(", ")}")
|
|
138
|
+
puts
|
|
139
|
+
|
|
140
|
+
return unless @options[:yes] || @prompt.yes?("Proceed?")
|
|
141
|
+
|
|
142
|
+
generate(selections)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def generate(selections)
|
|
146
|
+
rails_flags = build_rails_flags(selections)
|
|
147
|
+
|
|
148
|
+
if selections[:gems].any?
|
|
149
|
+
template_path = write_template(selections[:gems])
|
|
150
|
+
rails_flags << "--template=#{template_path}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
cmd = "rails new #{@app_name} #{rails_flags.join(" ")}"
|
|
154
|
+
UI.info("Running: #{cmd}")
|
|
155
|
+
puts
|
|
156
|
+
|
|
157
|
+
success = Bundler.with_unbundled_env { system(cmd) }
|
|
158
|
+
|
|
159
|
+
File.delete(template_path) if template_path && File.exist?(template_path)
|
|
160
|
+
|
|
161
|
+
if success
|
|
162
|
+
puts
|
|
163
|
+
UI.success("App created! Next steps:")
|
|
164
|
+
UI.muted(" cd #{@app_name}")
|
|
165
|
+
UI.muted(" bin/setup")
|
|
166
|
+
UI.muted(" bin/dev")
|
|
167
|
+
else
|
|
168
|
+
UI.error("rails new failed. See output above for details.")
|
|
169
|
+
exit(1)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def build_rails_flags(selections)
|
|
174
|
+
flags = []
|
|
175
|
+
flags << "--database=#{selections[:database]}"
|
|
176
|
+
flags << "--css=#{selections[:css]}" unless selections[:css] == "none"
|
|
177
|
+
flags << "--javascript=#{selections[:js]}"
|
|
178
|
+
flags << "--skip-test" if selections[:gems].include?("rspec")
|
|
179
|
+
flags
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Writes a temporary Rails template.rb file and returns its path
|
|
183
|
+
def write_template(gems)
|
|
184
|
+
template = build_template(gems)
|
|
185
|
+
path = File.join(Dir.tmpdir, "outset_template_#{@app_name}_#{Process.pid}.rb")
|
|
186
|
+
File.write(path, template)
|
|
187
|
+
path
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def build_template(gems)
|
|
191
|
+
lines = ["# Generated by outset v#{Outset::VERSION}", ""]
|
|
192
|
+
|
|
193
|
+
# Gem declarations
|
|
194
|
+
lines << "# ── Gems ─────────────────────────────────────────────"
|
|
195
|
+
gems.each { |g| lines << gem_declaration(g) }
|
|
196
|
+
lines << ""
|
|
197
|
+
|
|
198
|
+
# after_bundle block
|
|
199
|
+
lines << "after_bundle do"
|
|
200
|
+
gems.each do |g|
|
|
201
|
+
after = after_bundle_steps(g)
|
|
202
|
+
lines += after.map { |l| " #{l}" } if after.any?
|
|
203
|
+
end
|
|
204
|
+
lines << " git add: '.', commit: %(-m 'Initial scaffold via outset')"
|
|
205
|
+
lines << "end"
|
|
206
|
+
lines << ""
|
|
207
|
+
|
|
208
|
+
lines.join("\n")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def gem_declaration(gem_name)
|
|
212
|
+
case gem_name
|
|
213
|
+
when "rspec"
|
|
214
|
+
"gem_group :development, :test do\n gem 'rspec-rails'\n gem 'factory_bot_rails'\n gem 'faker'\nend"
|
|
215
|
+
when "letter_opener"
|
|
216
|
+
"gem_group :development do\n gem 'letter_opener'\nend"
|
|
217
|
+
when "sidekiq"
|
|
218
|
+
"gem 'sidekiq'"
|
|
219
|
+
else
|
|
220
|
+
"gem '#{gem_name}'"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def after_bundle_steps(gem_name)
|
|
225
|
+
case gem_name
|
|
226
|
+
when "devise" then ["generate 'devise:install'", "generate 'devise', 'User'"]
|
|
227
|
+
when "pundit" then ["generate 'pundit:install'"]
|
|
228
|
+
when "rspec" then ["generate 'rspec:install'", "rails_command 'db:create'"]
|
|
229
|
+
when "sidekiq" then ["# Add Sidekiq as ActiveJob backend in config/application.rb manually"]
|
|
230
|
+
else []
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Outset
|
|
6
|
+
class Config
|
|
7
|
+
CONFIG_DIR = File.expand_path("~/.outset")
|
|
8
|
+
CONFIG_FILE = File.join(CONFIG_DIR, "config.toml")
|
|
9
|
+
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
"defaults" => {
|
|
12
|
+
"database" => "postgresql",
|
|
13
|
+
"css" => "tailwind",
|
|
14
|
+
"javascript" => "importmap"
|
|
15
|
+
},
|
|
16
|
+
"skip" => {
|
|
17
|
+
"rubocop" => false,
|
|
18
|
+
"brakeman" => false,
|
|
19
|
+
"docker" => false
|
|
20
|
+
},
|
|
21
|
+
"gems" => {
|
|
22
|
+
"always" => []
|
|
23
|
+
},
|
|
24
|
+
"recipes" => {
|
|
25
|
+
"default" => nil
|
|
26
|
+
}
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
def self.load
|
|
30
|
+
new.load
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def load
|
|
34
|
+
return DEFAULTS.dup unless File.exist?(CONFIG_FILE)
|
|
35
|
+
|
|
36
|
+
require "toml-rb"
|
|
37
|
+
user_config = TomlRB.load_file(CONFIG_FILE)
|
|
38
|
+
deep_merge(DEFAULTS.dup, user_config)
|
|
39
|
+
rescue => e
|
|
40
|
+
UI.warn("Could not read config file: #{e.message}. Using defaults.")
|
|
41
|
+
DEFAULTS.dup
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.resolve(options = {})
|
|
45
|
+
config = load
|
|
46
|
+
default_recipe = config.dig("recipes", "default")
|
|
47
|
+
default_recipe = nil if default_recipe.nil? || default_recipe.to_s.strip.empty?
|
|
48
|
+
{
|
|
49
|
+
"database" => options[:database] || ENV["OUTSET_DATABASE"] || config.dig("defaults", "database"),
|
|
50
|
+
"css" => options[:css] || ENV["OUTSET_CSS"] || config.dig("defaults", "css"),
|
|
51
|
+
"javascript" => options[:js] || ENV["OUTSET_JS"] || config.dig("defaults", "javascript"),
|
|
52
|
+
"gems" => config.dig("gems", "always") || [],
|
|
53
|
+
"default_recipe" => default_recipe
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.init!
|
|
58
|
+
return if File.exist?(CONFIG_FILE)
|
|
59
|
+
|
|
60
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
|
61
|
+
File.write(CONFIG_FILE, default_toml)
|
|
62
|
+
UI.success("Created config file at #{CONFIG_FILE}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.default_toml
|
|
66
|
+
<<~TOML
|
|
67
|
+
# ~/.outset/config.toml
|
|
68
|
+
# Edit this file to set your personal defaults.
|
|
69
|
+
|
|
70
|
+
[defaults]
|
|
71
|
+
database = "postgresql"
|
|
72
|
+
css = "tailwind"
|
|
73
|
+
javascript = "importmap"
|
|
74
|
+
|
|
75
|
+
[skip]
|
|
76
|
+
rubocop = false
|
|
77
|
+
brakeman = false
|
|
78
|
+
docker = false
|
|
79
|
+
|
|
80
|
+
[gems]
|
|
81
|
+
always = [] # Gems added to every new app, e.g. ["annotate", "letter_opener"]
|
|
82
|
+
|
|
83
|
+
[recipes]
|
|
84
|
+
default = "" # Name of your default recipe, e.g. "saas"
|
|
85
|
+
|
|
86
|
+
# Define custom recipes (uncomment and edit to add your own):
|
|
87
|
+
# [recipes.mystartup]
|
|
88
|
+
# description = "My startup stack"
|
|
89
|
+
# database = "postgresql"
|
|
90
|
+
# css = "tailwind"
|
|
91
|
+
# js = "esbuild"
|
|
92
|
+
# gems = ["devise", "sidekiq", "pagy"]
|
|
93
|
+
TOML
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def deep_merge(base, override)
|
|
99
|
+
base.merge(override) do |_key, base_val, override_val|
|
|
100
|
+
if base_val.is_a?(Hash) && override_val.is_a?(Hash)
|
|
101
|
+
deep_merge(base_val, override_val)
|
|
102
|
+
else
|
|
103
|
+
override_val
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Outset
|
|
4
|
+
module Recipes
|
|
5
|
+
REGISTRY = {
|
|
6
|
+
"saas" => {
|
|
7
|
+
description: "Full SaaS stack — auth, jobs, pagination",
|
|
8
|
+
database: "postgresql",
|
|
9
|
+
css: "tailwind",
|
|
10
|
+
js: "importmap",
|
|
11
|
+
gems: %w[devise pundit sidekiq pagy annotate letter_opener]
|
|
12
|
+
},
|
|
13
|
+
"api" => {
|
|
14
|
+
description: "API-only app — no frontend assets",
|
|
15
|
+
database: "postgresql",
|
|
16
|
+
css: "none",
|
|
17
|
+
js: "importmap",
|
|
18
|
+
gems: %w[devise rspec]
|
|
19
|
+
},
|
|
20
|
+
"minimal" => {
|
|
21
|
+
description: "Bare minimum — SQLite, no extras",
|
|
22
|
+
database: "sqlite3",
|
|
23
|
+
css: "none",
|
|
24
|
+
js: "importmap",
|
|
25
|
+
gems: []
|
|
26
|
+
}
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
def self.find(name)
|
|
30
|
+
REGISTRY[name] || user_recipes[name] || begin
|
|
31
|
+
UI.error("Unknown recipe: '#{name}'")
|
|
32
|
+
UI.muted(" Available recipes: #{all.keys.join(", ")}")
|
|
33
|
+
exit(1)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.all
|
|
38
|
+
REGISTRY.merge(user_recipes)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.user_recipes
|
|
42
|
+
config = Config.load
|
|
43
|
+
(config["recipes"] || {}).each_with_object({}) do |(name, value), hash|
|
|
44
|
+
next unless value.is_a?(Hash)
|
|
45
|
+
hash[name] = {
|
|
46
|
+
description: value["description"] || "Custom recipe",
|
|
47
|
+
database: value["database"] || "postgresql",
|
|
48
|
+
css: value["css"] || "tailwind",
|
|
49
|
+
js: value["js"] || "importmap",
|
|
50
|
+
gems: value["gems"] || []
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/outset/ui.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Outset
|
|
6
|
+
module UI
|
|
7
|
+
PASTEL = Pastel.new
|
|
8
|
+
|
|
9
|
+
def self.banner
|
|
10
|
+
puts PASTEL.bold.blue(<<~BANNER)
|
|
11
|
+
___ _ _ _____ ____ _____ _____
|
|
12
|
+
/ _ \\ | | | ||_ _|/ ___| | ____|_ _|
|
|
13
|
+
| | | || | | | | | \\___ \\ | _| | |
|
|
14
|
+
| |_| || |_| | | | ___) || |___ | |
|
|
15
|
+
\\___/ \\___/ |_| |____/ |_____| |_|
|
|
16
|
+
BANNER
|
|
17
|
+
puts PASTEL.dim(" Rails Application Bootstrapper v#{Outset::VERSION}")
|
|
18
|
+
puts
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.success(msg) = puts PASTEL.green("✓ #{msg}")
|
|
22
|
+
def self.error(msg) = puts PASTEL.red("✗ #{msg}")
|
|
23
|
+
def self.info(msg) = puts PASTEL.cyan("→ #{msg}")
|
|
24
|
+
def self.warn(msg) = puts PASTEL.yellow("! #{msg}")
|
|
25
|
+
def self.muted(msg) = puts PASTEL.dim(msg)
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/outset.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: outset
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kingsley Chijioke
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: tty-prompt
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.23'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.23'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: toml-rb
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: pastel
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.8'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.8'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: minitest
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '5.20'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '5.20'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: minitest-reporters
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.6'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.6'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rake
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '13.0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '13.0'
|
|
110
|
+
description: A personal Rails application bootstrapper with interactive prompts, a
|
|
111
|
+
config file, and predefined recipes — callable as `outset new <app_name>`.
|
|
112
|
+
email:
|
|
113
|
+
- dev@kingsleychijioke.me
|
|
114
|
+
executables:
|
|
115
|
+
- outset
|
|
116
|
+
extensions: []
|
|
117
|
+
extra_rdoc_files: []
|
|
118
|
+
files:
|
|
119
|
+
- exe/outset
|
|
120
|
+
- lib/outset.rb
|
|
121
|
+
- lib/outset/cli.rb
|
|
122
|
+
- lib/outset/commands/config_cmd.rb
|
|
123
|
+
- lib/outset/commands/doctor.rb
|
|
124
|
+
- lib/outset/commands/new.rb
|
|
125
|
+
- lib/outset/config.rb
|
|
126
|
+
- lib/outset/recipes.rb
|
|
127
|
+
- lib/outset/ui.rb
|
|
128
|
+
- lib/outset/version.rb
|
|
129
|
+
homepage: https://github.com/kinsomicrote/outset
|
|
130
|
+
licenses:
|
|
131
|
+
- MIT
|
|
132
|
+
metadata: {}
|
|
133
|
+
rdoc_options: []
|
|
134
|
+
require_paths:
|
|
135
|
+
- lib
|
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - ">="
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: 3.1.0
|
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
requirements: []
|
|
147
|
+
rubygems_version: 4.0.3
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: Bootstrap new Rails applications your way
|
|
150
|
+
test_files: []
|