one-for-all-framework 1.0.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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +4 -0
  3. data/Dockerfile +19 -0
  4. data/Gemfile +20 -0
  5. data/LICENSE +21 -0
  6. data/Procfile +1 -0
  7. data/README.md +99 -0
  8. data/app/controllers/api_controller.rb +19 -0
  9. data/app/controllers/application_controller.rb +77 -0
  10. data/app/controllers/auth_controller.rb +35 -0
  11. data/app/controllers/dashboard_controller.rb +41 -0
  12. data/app/controllers/pages_controller.rb +49 -0
  13. data/app/controllers/posts_controller.rb +48 -0
  14. data/app/controllers/projects_controller.rb +48 -0
  15. data/app/helpers/cloudinary_helper.rb +14 -0
  16. data/app/middleware/auth_middleware.rb +52 -0
  17. data/app/middleware/csrf_middleware.rb +27 -0
  18. data/app/models/page.rb +8 -0
  19. data/app/models/post.rb +8 -0
  20. data/app/models/project.rb +7 -0
  21. data/app/models/user.rb +83 -0
  22. data/app/views/blog_home.erb +70 -0
  23. data/app/views/cms/pages_form.erb +156 -0
  24. data/app/views/cms/pages_index.erb +104 -0
  25. data/app/views/cms/posts_form.erb +182 -0
  26. data/app/views/cms/posts_index.erb +100 -0
  27. data/app/views/cms/projects_form.erb +176 -0
  28. data/app/views/cms/projects_index.erb +96 -0
  29. data/app/views/dashboard.erb +122 -0
  30. data/app/views/docs.erb +140 -0
  31. data/app/views/error.erb +159 -0
  32. data/app/views/index.erb +70 -0
  33. data/app/views/layout.erb +251 -0
  34. data/app/views/login.erb +45 -0
  35. data/app/views/page.erb +21 -0
  36. data/app/views/portfolio.erb +73 -0
  37. data/app/views/post.erb +33 -0
  38. data/app/views/posts/hello_world.erb +3 -0
  39. data/app/views/project.erb +39 -0
  40. data/bin/ofa +499 -0
  41. data/config/boot.rb +134 -0
  42. data/config/database.json +4 -0
  43. data/config/database.rb +142 -0
  44. data/config/features.json +7 -0
  45. data/config/locales/en.json +6 -0
  46. data/config/locales/id.json +6 -0
  47. data/config/routes.rb +87 -0
  48. data/config.eks +11 -0
  49. data/config.ru +11 -0
  50. data/db/data.sqlite3 +0 -0
  51. data/db/development.sqlite3 +0 -0
  52. data/ofa +2 -0
  53. data/public/css/cms.css +134 -0
  54. data/public/images/logo.jpg +0 -0
  55. data/public/images/logo.png +0 -0
  56. metadata +259 -0
@@ -0,0 +1,21 @@
1
+ <div class="glass-card anime-element">
2
+ <h1><%= @title %></h1>
3
+ <div class="page-content markdown-content" style="margin-top: 2rem;">
4
+ <%= markdown(@content) %>
5
+ </div>
6
+ <div class="mt-12 pt-8 border-t border-white/5">
7
+ <a href="/" class="btn-secondary">
8
+ <i class="fas fa-arrow-left mr-2"></i> Back to Home
9
+ </a>
10
+ </div>
11
+ </div>
12
+
13
+ <script>
14
+ anime({
15
+ targets: '.anime-element',
16
+ translateY: [30, 0],
17
+ opacity: [0, 1],
18
+ duration: 1000,
19
+ easing: 'easeOutExpo'
20
+ });
21
+ </script>
@@ -0,0 +1,73 @@
1
+ <div class="header-section anime-element" style="margin-bottom: 3rem;">
2
+ <h1>Creative Portfolio</h1>
3
+ <p>Showcasing my best work, creative journey, and engineering experiments.</p>
4
+ </div>
5
+
6
+ <div class="portfolio-grid grid grid-cols-1 md:grid-cols-2 gap-8 text-left">
7
+ <% if @projects.empty? %>
8
+ <div class="glass-card col-span-full py-20 text-center">
9
+ <div class="w-20 h-20 bg-primary/10 text-primary rounded-3xl flex items-center justify-center mx-auto mb-6 text-3xl">
10
+ <i class="fas fa-folder-open"></i>
11
+ </div>
12
+ <p class="text-xl font-bold text-slate-400">No projects displayed yet.</p>
13
+ </div>
14
+ <% end %>
15
+
16
+ <% @projects.each do |project| %>
17
+ <div class="project-card glass-card flex flex-col h-full !p-0">
18
+ <div class="card-glow"></div>
19
+
20
+ <div class="relative overflow-hidden aspect-video group-hover:scale-[1.02] transition-transform duration-700">
21
+ <% if project.image_url && !project.image_url.empty? %>
22
+ <img src="<%= project.image_url %>" alt="<%= project.title %>" class="w-full h-full object-cover">
23
+ <% else %>
24
+ <div class="w-full h-full bg-gradient-to-br from-primary to-secondary opacity-20"></div>
25
+ <% end %>
26
+ <div class="absolute inset-0 bg-gradient-to-t from-bg via-transparent to-transparent opacity-60"></div>
27
+ </div>
28
+
29
+ <div class="p-8 flex flex-col flex-grow relative z-10">
30
+ <div class="badge-premium">Featured Project</div>
31
+ <h3 class="text-2xl font-black mb-4 tracking-tight group-hover:text-primary transition-colors"><%= project.title %></h3>
32
+
33
+ <div class="text-slate-500 dark:text-slate-400 text-sm leading-relaxed mb-8 flex-grow">
34
+ <%= markdown(project.description.split("\n")[0..2].join("\n") + "...") %>
35
+ </div>
36
+
37
+ <div class="flex items-center justify-between mt-auto pt-6 border-t border-white/5">
38
+ <% if project.link && !project.link.empty? %>
39
+ <a href="<%= project.link %>" target="_blank" class="btn-premium !px-6 !py-2.5 !text-sm">
40
+ <i class="fas fa-external-link-alt mr-2"></i> Live Demo
41
+ </a>
42
+ <% end %>
43
+ <a href="/projects/<%= project.slug %>" class="group/link flex items-center gap-2 text-sm font-bold text-slate-500 hover:text-primary transition-colors">
44
+ Case Study <i class="fas fa-arrow-right group-hover/link:translate-x-1 transition-transform"></i>
45
+ </a>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <% end %>
50
+ </div>
51
+
52
+ <script>
53
+ document.addEventListener('DOMContentLoaded', () => {
54
+ anime({
55
+ targets: '.header-section',
56
+ translateY: [20, 0],
57
+ opacity: [0, 1],
58
+ duration: 800,
59
+ easing: 'easeOutCubic',
60
+ delay: 400
61
+ });
62
+
63
+ anime({
64
+ targets: '.project-card',
65
+ translateY: [50, 0],
66
+ scale: [0.95, 1],
67
+ opacity: [0, 1],
68
+ duration: 800,
69
+ delay: anime.stagger(150, {start: 600}),
70
+ easing: 'easeOutExpo'
71
+ });
72
+ });
73
+ </script>
@@ -0,0 +1,33 @@
1
+ <div class="glass-card anime-element" style="text-align: left; max-width: 800px; margin: 0 auto;">
2
+ <div style="margin-bottom: 2rem;">
3
+ <span class="badge" style="background: var(--primary); color: white; padding: 0.3rem 0.8rem; border-radius: 0.5rem; font-size: 0.8rem; font-weight: 700;">
4
+ <%= @post.category.upcase %>
5
+ </span>
6
+ <h1 style="margin: 1rem 0 0.5rem 0; font-size: 3rem; line-height: 1.1;"><%= @post.title %></h1>
7
+ <p style="color: var(--text-muted); font-size: 0.95rem;">Published on <%= @post.created_at.strftime('%B %d, %Y') %></p>
8
+ </div>
9
+
10
+ <% if @post.image_url && !@post.image_url.empty? %>
11
+ <img src="<%= @post.image_url %>" alt="<%= @post.title %>" style="width: 100%; max-height: 400px; object-fit: cover; border-radius: 1.5rem; margin-bottom: 2.5rem; border: 1px solid var(--border);">
12
+ <% end %>
13
+
14
+ <div class="post-content markdown-content">
15
+ <%= markdown(@post.content) %>
16
+ </div>
17
+
18
+ <div class="mt-12 pt-8 border-t border-white/5">
19
+ <a href="/" class="btn-secondary">
20
+ <i class="fas fa-arrow-left mr-2"></i> Back to Blog
21
+ </a>
22
+ </div>
23
+ </div>
24
+
25
+ <script>
26
+ anime({
27
+ targets: '.anime-element',
28
+ translateY: [30, 0],
29
+ opacity: [0, 1],
30
+ duration: 800,
31
+ easing: 'easeOutExpo'
32
+ });
33
+ </script>
@@ -0,0 +1,3 @@
1
+ <h1>Hello World</h1>
2
+ <p>Author: Antigravity</p>
3
+ <p>Category: Tutorial</p>
@@ -0,0 +1,39 @@
1
+ <div class="glass-card anime-element" style="text-align: left; max-width: 800px; margin: 0 auto;">
2
+ <div style="margin-bottom: 2rem;">
3
+ <h1 style="margin: 0 0 0.5rem 0; font-size: 3rem; line-height: 1.1;"><%= @project.title %></h1>
4
+ <p style="color: var(--text-muted); font-size: 0.95rem;">Project created on <%= @project.created_at.strftime('%B %d, %Y') %></p>
5
+ </div>
6
+
7
+ <% if @project.image_url && !@project.image_url.empty? %>
8
+ <img src="<%= @project.image_url %>" alt="<%= @project.title %>" style="width: 100%; max-height: 400px; object-fit: cover; border-radius: 1.5rem; margin-bottom: 2.5rem; border: 1px solid var(--border);">
9
+ <% else %>
10
+ <div style="width: 100%; height: 300px; background: linear-gradient(135deg, var(--primary), var(--accent)); border-radius: 1.5rem; margin-bottom: 2.5rem; opacity: 0.8; display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 800;">
11
+ <%= @project.title %>
12
+ </div>
13
+ <% end %>
14
+
15
+ <div class="project-content markdown-content">
16
+ <%= markdown(@project.description) %>
17
+ </div>
18
+
19
+ <div class="flex flex-wrap gap-4 items-center mt-12 pt-8 border-t border-white/5">
20
+ <% if @project.link && !@project.link.empty? %>
21
+ <a href="<%= @project.link %>" target="_blank" class="btn-premium">
22
+ <i class="fas fa-rocket mr-2"></i> Visit Live Demo
23
+ </a>
24
+ <% end %>
25
+ <a href="/" class="btn-secondary">
26
+ <i class="fas fa-arrow-left mr-2"></i> Back to Home
27
+ </a>
28
+ </div>
29
+ </div>
30
+
31
+ <script>
32
+ anime({
33
+ targets: '.anime-element',
34
+ translateY: [30, 0],
35
+ opacity: [0, 1],
36
+ duration: 800,
37
+ easing: 'easeOutExpo'
38
+ });
39
+ </script>
data/bin/ofa ADDED
@@ -0,0 +1,499 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ require 'optparse'
5
+ require 'json'
6
+
7
+ # Framework core path
8
+ FRAMEWORK_ROOT = File.expand_path('..', __dir__)
9
+ # Current project path (where the user is running the command)
10
+ PROJECT_ROOT = Dir.pwd
11
+
12
+ # Load dotenv from project root if exists
13
+ begin
14
+ require 'dotenv'
15
+ Dotenv.load(File.join(PROJECT_ROOT, '.env'))
16
+ rescue LoadError
17
+ end
18
+
19
+ def help
20
+ puts "✨ One-For-All Framework CLI ✨"
21
+ puts "-----------------------------"
22
+ puts "Usage:"
23
+ puts " ofa new NAME [TYPE] - Create a new project + automatic bundle install"
24
+ puts " ofa init [TYPE] - Initialize project in current folder"
25
+ puts " ofa g controller NAME - Generate a new controller"
26
+ puts " ofa g model NAME - Generate a new model"
27
+ puts " ofa g migration NAME - Generate a new migration"
28
+ puts " ofa g post TITLE - Create a new post [args: --category, --author, --image]"
29
+ puts " ofa feature ACTION F - Toggle features: action=enable/disable, F=cms/auth"
30
+ puts " ofa type NAME - Set application type: portfolio, blog, landing_page"
31
+ puts " ofa theme NAME - Set UI theme: light_glass, dark_glass"
32
+ puts " ofa storage NAME - Set image storage: local, cloudinary"
33
+ puts " ofa reset-password USR PWD - Reset admin account password"
34
+ puts " ofa db switch TYPE [NAME] - Switch DB: sqlite, mysql, mariadb, mongodb, postgres"
35
+ puts " ofa db migrate - Run database migrations"
36
+ puts " ofa migrate - Run database migrations (alias for db migrate)"
37
+ puts " ofa run - Start the application server"
38
+ puts " ofa deploy - Deploy project to production"
39
+ puts "-----------------------------"
40
+ exit
41
+ end
42
+
43
+ def run_migrations
44
+ Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
45
+ require File.join(FRAMEWORK_ROOT, 'config', 'boot')
46
+ puts "Running migrations..."
47
+ migration_path = File.join(PROJECT_ROOT, "db/migrations")
48
+ FileUtils.mkdir_p(migration_path)
49
+
50
+ if defined?(DB) && DB
51
+ if Dir.glob(File.join(migration_path, '*.rb')).empty?
52
+ puts "No custom migrations found in #{migration_path}."
53
+ puts "Core tables are automatically created. You're ready to go!"
54
+ else
55
+ Sequel.extension :migration
56
+ Sequel::Migrator.run(DB, migration_path)
57
+ puts "Migrations completed."
58
+ end
59
+ end
60
+ end
61
+
62
+ def ensure_initialized!
63
+ config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
64
+ unless File.exist?(config_path)
65
+ puts "❌ Error: Project not initialized in this folder."
66
+ puts " Please run 'ofa init' first."
67
+ exit 1
68
+ end
69
+ end
70
+
71
+ command = ARGV.shift
72
+ help if command.nil? || ['help', '-h', '--help'].include?(command)
73
+
74
+ case command
75
+ when 'new'
76
+ project_name = ARGV.shift
77
+ app_type = ARGV.shift || 'landing_page'
78
+ if project_name.nil? || project_name.empty?
79
+ puts "Usage: ofa new PROJECT_NAME [TYPE]"
80
+ exit 1
81
+ end
82
+ target_dir = File.join(PROJECT_ROOT, project_name)
83
+ if Dir.exist?(target_dir)
84
+ puts "❌ Folder '#{project_name}' already exists."
85
+ exit 1
86
+ end
87
+ FileUtils.mkdir_p(target_dir)
88
+ puts "✨ Creating new project '#{project_name}' (#{app_type})..."
89
+ # Delegate to 'init' logic by re-invoking ofa in the new directory using absolute path
90
+ absolute_ofa_path = File.expand_path(__FILE__)
91
+ system("cd #{target_dir} && ruby #{absolute_ofa_path} init #{app_type}")
92
+ puts "📦 Running bundle install..."
93
+ success = system("cd #{target_dir} && bundle install --quiet")
94
+ if success
95
+ puts ""
96
+ puts "✅ Project '#{project_name}' is ready!"
97
+ puts " cd #{project_name} && ofa run"
98
+ else
99
+ puts "⚠️ bundle install failed. Please run it manually in folder #{project_name}."
100
+ end
101
+
102
+ when 'init'
103
+ app_type = ARGV.shift || 'landing_page'
104
+ puts "Initializing One-For-All project as '#{app_type}' in #{PROJECT_ROOT}..."
105
+
106
+ # --- Interactive Wizard ---
107
+ puts "\n🛠️ Project Configuration (Press Enter for default)"
108
+
109
+ # 1. Database Configuration
110
+ print "💾 Choose Database [1. SQLite (default), 2. MongoDB Atlas]: "
111
+ db_choice = STDIN.gets.chomp
112
+ db_type = db_choice == '2' ? 'mongodb' : 'sqlite'
113
+
114
+ db_url = ""
115
+ if db_type == 'mongodb'
116
+ print "🔗 Enter MongoDB Connection String: "
117
+ db_url = STDIN.gets.chomp
118
+ end
119
+
120
+ # 2. Image Storage Configuration
121
+ print "🖼️ Choose Image Storage [1. Local (default), 2. Cloudinary]: "
122
+ img_choice = STDIN.gets.chomp
123
+ img_storage = img_choice == '2' ? 'cloudinary' : 'local'
124
+
125
+ cloudinary_url = ""
126
+ if img_storage == 'cloudinary'
127
+ print "☁️ Enter Cloudinary URL (cloudinary://KEY:SECRET@NAME): "
128
+ cloudinary_url = STDIN.gets.chomp
129
+ end
130
+ puts "-----------------------------\n"
131
+
132
+ # Create directories
133
+ dirs = ['app/controllers', 'app/models', 'app/views', 'config', 'db/migrations', 'public/css', 'public/js', 'public/img', 'public/img/uploads']
134
+ dirs.each { |dir| FileUtils.mkdir_p(File.join(PROJECT_ROOT, dir)) }
135
+
136
+ # Copy framework core migrations
137
+ framework_migrations = File.join(FRAMEWORK_ROOT, 'db/migrations')
138
+ project_migrations = File.join(PROJECT_ROOT, 'db/migrations')
139
+ if File.directory?(framework_migrations)
140
+ Dir.glob(File.join(framework_migrations, '*.rb')).each do |mig|
141
+ FileUtils.cp(mig, project_migrations)
142
+ end
143
+ end
144
+
145
+ # Create .env
146
+ env_content = []
147
+ env_content << "EKS_ENV=development"
148
+ env_content << "DATABASE_URL=#{db_url}" if db_type == 'mongodb'
149
+ env_content << "CLOUDINARY_URL=#{cloudinary_url}" if img_storage == 'cloudinary'
150
+ File.write(File.join(PROJECT_ROOT, '.env'), env_content.join("\n"))
151
+
152
+ # Create config.ru
153
+ puts " Creating system files..."
154
+ config_ru_content = <<~RUBY
155
+ # Generated by One-For-All CLI
156
+ # Target framework: #{FRAMEWORK_ROOT}
157
+
158
+ # Add framework to load path
159
+ $LOAD_PATH.unshift '#{FRAMEWORK_ROOT}'
160
+ APP_ROOT = File.expand_path(__dir__)
161
+ require 'config/boot'
162
+
163
+ # Middleware setup
164
+ use EksCent::Middleware::ShowExceptions
165
+ use EksCent::Middleware::Session, secret: EksCent.secret_key_base
166
+ use AuthMiddleware
167
+ use CSRFMiddleware
168
+ use EksCent::Middleware::Static, root: 'public'
169
+ use EksCent::Middleware::Logger
170
+
171
+ run ROUTES
172
+ RUBY
173
+ File.write(File.join(PROJECT_ROOT, 'config.ru'), config_ru_content)
174
+
175
+ # Create Gemfile
176
+ gemfile_content = <<~RUBY
177
+ source 'https://rubygems.org'
178
+
179
+ gem 'eks-cent', '4.0.0'
180
+ gem 'eksa-server'
181
+ gem 'sequel'
182
+ gem 'sqlite3'
183
+ gem 'bcrypt'
184
+ gem 'dotenv'
185
+ gem 'cloudinary'
186
+ gem 'mongo'
187
+ gem 'mysql2'
188
+ gem 'kramdown'
189
+ gem 'kramdown-parser-gfm'
190
+ RUBY
191
+ File.write(File.join(PROJECT_ROOT, 'Gemfile'), gemfile_content)
192
+
193
+ # Create base ApplicationController
194
+ app_controller_content = <<~'RUBY'
195
+ class ApplicationController
196
+ attr_reader :req, :res
197
+
198
+ def initialize(req, res)
199
+ @req = req
200
+ @res = res
201
+ end
202
+
203
+ def params
204
+ @req.params
205
+ end
206
+
207
+ def session
208
+ @req.env['eks_cent.session'] ||= {}
209
+ end
210
+
211
+ def render(template, **locals)
212
+ # Pass controller instance variables to the view
213
+ instance_variables.each do |var|
214
+ locals[var.to_s.delete('@').to_sym] ||= instance_variable_get(var)
215
+ end
216
+ @res.render(template, **locals)
217
+ end
218
+
219
+ def redirect_to(path)
220
+ @res.status = 302
221
+ @res.headers['Location'] = path
222
+ end
223
+
224
+ # Validation Helper
225
+ def validate!(required_params)
226
+ missing = required_params.select { |p| params[p.to_s].nil? || params[p.to_s].empty? }
227
+ unless missing.empty?
228
+ @res.status = 400
229
+ render 'error', message: "Missing required parameters: #{missing.join(', ')}"
230
+ throw(:halt)
231
+ end
232
+ end
233
+
234
+ # CSRF Helper for views
235
+ def csrf_token
236
+ @req.env['eks_cent.csrf_token']
237
+ end
238
+ end
239
+ RUBY
240
+ File.write(File.join(PROJECT_ROOT, 'app/controllers/application_controller.rb'), app_controller_content)
241
+
242
+ # Setup features.json
243
+ config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
244
+ unless File.exist?(config_path)
245
+ default_config = {
246
+ "auth" => true,
247
+ "cms" => true,
248
+ "type" => app_type,
249
+ "theme" => "dark_glass",
250
+ "storage" => img_storage
251
+ }
252
+ File.write(config_path, JSON.pretty_generate(default_config))
253
+ end
254
+
255
+ # Setup database.json
256
+ db_config_path = File.join(PROJECT_ROOT, 'config', 'database.json')
257
+ unless File.exist?(db_config_path)
258
+ if db_type == 'mongodb'
259
+ db_default = { "adapter" => "env" }
260
+ else
261
+ db_default = { "adapter" => "sqlite", "database" => "db/development.sqlite3" }
262
+ end
263
+ File.write(db_config_path, JSON.pretty_generate(db_default))
264
+ end
265
+
266
+ # Premium Scaffolding
267
+ puts " Generating premium UI components..."
268
+ # Copy views from framework to project
269
+ framework_views = File.join(FRAMEWORK_ROOT, 'app', 'views')
270
+ project_views = File.join(PROJECT_ROOT, 'app', 'views')
271
+
272
+ # Recursive copy for views (including cms folder)
273
+ FileUtils.cp_r(File.join(framework_views, '.'), project_views)
274
+
275
+ # Copy models from framework to project
276
+ framework_models = File.join(FRAMEWORK_ROOT, 'app', 'models')
277
+ project_models = File.join(PROJECT_ROOT, 'app', 'models')
278
+ FileUtils.cp_r(File.join(framework_models, '.'), project_models)
279
+
280
+ # Copy controllers from framework to project
281
+ framework_controllers = File.join(FRAMEWORK_ROOT, 'app', 'controllers')
282
+ project_controllers = File.join(PROJECT_ROOT, 'app', 'controllers')
283
+ # Filter out specific internal framework controllers if needed, but here we want them
284
+ Dir.glob(File.join(framework_controllers, '*.rb')).each do |file|
285
+ # Skip ApplicationController because we generated a custom one for the project
286
+ next if File.basename(file) == 'application_controller.rb'
287
+ FileUtils.cp(file, project_controllers)
288
+ end
289
+
290
+ # Ensure public/css exists but we no longer rely on style.css for layout
291
+ css_dir = File.join(PROJECT_ROOT, 'public/css')
292
+ FileUtils.mkdir_p(css_dir)
293
+ File.write(File.join(css_dir, 'style.css'), "/* Custom styles here. Framework uses layout.erb for core UI. */")
294
+
295
+ # Scaffolding based on type
296
+ case app_type
297
+ when 'blog'
298
+ puts " Scaffolding Blog starter..."
299
+ File.write(File.join(PROJECT_ROOT, 'app/controllers/posts_controller.rb'), <<~RUBY)
300
+ require_relative 'application_controller'
301
+ class PostsController < ApplicationController
302
+ def index; end
303
+ end
304
+ RUBY
305
+ end
306
+
307
+ puts "Done! Run 'ofa run' to start your app."
308
+
309
+ when 'g'
310
+ ensure_initialized!
311
+ sub = ARGV.shift
312
+ name = ARGV.shift
313
+ if sub == 'controller' && name
314
+ class_name = name.capitalize + "Controller"
315
+ file_name = File.join(PROJECT_ROOT, "app/controllers/#{name.downcase}_controller.rb")
316
+ content = <<~RUBY
317
+ require_relative 'application_controller'
318
+
319
+ class #{class_name} < ApplicationController
320
+ def index
321
+ # Logic for #{name}
322
+ end
323
+ end
324
+ RUBY
325
+ File.write(file_name, content)
326
+ puts "Created: #{file_name}"
327
+ elsif sub == 'model' && name
328
+ class_name = name.capitalize
329
+ file_name = File.join(PROJECT_ROOT, "app/models/#{name.downcase}.rb")
330
+ content = <<~RUBY
331
+ class #{class_name} < Sequel::Model
332
+ end
333
+ RUBY
334
+ File.write(file_name, content)
335
+ puts "Created: #{file_name}"
336
+ elsif sub == 'migration' && name
337
+ version = Time.now.strftime('%Y%m%d%H%M%S')
338
+ dir_path = File.join(PROJECT_ROOT, "db/migrations")
339
+ FileUtils.mkdir_p(dir_path)
340
+ file_name = File.join(dir_path, "#{version}_#{name.downcase}.rb")
341
+ content = <<~RUBY
342
+ Sequel.migration do
343
+ change do
344
+ end
345
+ end
346
+ RUBY
347
+ File.write(file_name, content)
348
+ puts "Created migration: #{file_name}"
349
+ elsif sub == 'post' && name
350
+ options = {}
351
+ OptionParser.new do |opts|
352
+ opts.on("--category CAT") { |v| options[:category] = v }
353
+ opts.on("--author AUTH") { |v| options[:author] = v }
354
+ opts.on("--image IMG") { |v| options[:image] = v }
355
+ end.parse!(ARGV)
356
+ dir_path = File.join(PROJECT_ROOT, "app/views/posts")
357
+ FileUtils.mkdir_p(dir_path)
358
+ file_name = File.join(dir_path, "#{name.downcase.gsub(' ', '_')}.erb")
359
+ content = "<h1>#{name}</h1>\n<p>Author: #{options[:author] || 'Admin'}</p>\n<p>Category: #{options[:category] || 'General'}</p>"
360
+ File.write(file_name, content)
361
+ puts "Created post: #{file_name}"
362
+ else
363
+ puts "Usage: ofa g [controller|model|migration|post] NAME"
364
+ end
365
+
366
+ when 'feature'
367
+ ensure_initialized!
368
+ action = ARGV.shift
369
+ feature = ARGV.shift
370
+ config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
371
+ config = JSON.parse(File.read(config_path))
372
+ config[feature] = (action == 'enable')
373
+ File.write(config_path, JSON.pretty_generate(config))
374
+ puts "Feature '#{feature}' has been #{action}d."
375
+
376
+ when 'type'
377
+ ensure_initialized!
378
+ name = ARGV.shift
379
+ unless %w[landing_page portfolio blog].include?(name)
380
+ puts "❌ Error: Invalid app type '#{name}'. Choose: landing_page, portfolio, blog."
381
+ exit 1
382
+ end
383
+ config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
384
+ config = JSON.parse(File.read(config_path))
385
+ config['type'] = name
386
+ File.write(config_path, JSON.pretty_generate(config))
387
+ puts "Application type set to '#{name}'."
388
+
389
+ when 'theme'
390
+ ensure_initialized!
391
+ name = ARGV.shift
392
+ unless %w[light_glass dark_glass].include?(name)
393
+ puts "❌ Error: Invalid theme '#{name}'. Choose: light_glass, dark_glass."
394
+ exit 1
395
+ end
396
+ config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
397
+ config = JSON.parse(File.read(config_path))
398
+ config['theme'] = name
399
+ File.write(config_path, JSON.pretty_generate(config))
400
+ puts "Application theme set to '#{name}'."
401
+
402
+ when 'storage'
403
+ ensure_initialized!
404
+ name = ARGV.shift
405
+ unless %w[local cloudinary].include?(name)
406
+ puts "❌ Error: Invalid storage type '#{name}'. Choose: local, cloudinary."
407
+ exit 1
408
+ end
409
+ config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
410
+ config = JSON.parse(File.read(config_path))
411
+ config['storage'] = name
412
+ File.write(config_path, JSON.pretty_generate(config))
413
+ puts "Application storage set to '#{name}'."
414
+
415
+ when 'reset-password'
416
+ Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
417
+ require File.join(FRAMEWORK_ROOT, 'config', 'boot')
418
+ user_name = ARGV.shift
419
+ new_pwd = ARGV.shift
420
+ if user_name.nil? || new_pwd.nil?
421
+ puts "Usage: ofa reset-password USERNAME PASSWORD"
422
+ exit 1
423
+ end
424
+ begin
425
+ user = User.find(username: user_name) rescue nil
426
+ if user
427
+ user.password = new_pwd
428
+ user.save
429
+ puts "✅ Password for '#{user_name}' has been reset."
430
+ else
431
+ user = User.new(username: user_name)
432
+ user.password = new_pwd
433
+ user.save
434
+ puts "✅ User '#{user_name}' created with the provided password."
435
+ end
436
+ rescue ArgumentError => e
437
+ puts "❌ Error: #{e.message}"
438
+ puts " Requirements: min 8 characters, one uppercase, one number."
439
+ exit 1
440
+ end
441
+
442
+ when 'migrate'
443
+ run_migrations
444
+
445
+ when 'db'
446
+ sub = ARGV.shift
447
+ config_path = File.join(PROJECT_ROOT, 'config', 'database.json')
448
+ case sub
449
+ when 'switch'
450
+ type = ARGV.shift
451
+ db_name = ARGV.shift
452
+ case type
453
+ when 'env'
454
+ config = { "adapter" => "env" }
455
+ when 'sqlite'
456
+ config = { "adapter" => "sqlite", "database" => db_name || "db/development.sqlite3" }
457
+ else
458
+ config = { "adapter" => type, "host" => "localhost", "user" => "root", "password" => "", "database" => db_name || "one_for_all" }
459
+ end
460
+ File.write(config_path, JSON.pretty_generate(config))
461
+ puts "Switched to #{type} mode."
462
+ when 'migrate'
463
+ run_migrations
464
+ end
465
+
466
+ when 'run'
467
+ ensure_initialized!
468
+ # Check for config.ru
469
+ unless File.exist?(File.join(PROJECT_ROOT, 'config.ru'))
470
+ puts "Error: config.ru not found. Ensure you are in the project root or run 'ofa init'."
471
+ exit 1
472
+ end
473
+ puts "Starting One-For-All server..."
474
+ exec "bundle exec eksa-server"
475
+
476
+ when 'deploy'
477
+ puts "🚀 Starting Deployment process..."
478
+ puts "-----------------------------"
479
+
480
+ # 1. Check for Git
481
+ unless system("git rev-parse --is-inside-work-tree > /dev/null 2>&1")
482
+ puts "❌ Error: Project not initialized with Git."
483
+ puts " Please run 'git init' and commit your changes first."
484
+ exit 1
485
+ end
486
+
487
+ # 2. Check for Railway CLI as an example of deployment target
488
+ if system("railway --version > /dev/null 2>&1")
489
+ puts "📦 Railway CLI detected. Deploying to Railway..."
490
+ exec "railway up"
491
+ else
492
+ puts "⚠️ Deployment target not detected (Railway/Vercel)."
493
+ puts " Suggestion: Install Railway CLI or prepare a Dockerfile for VPS."
494
+ puts " This framework supports deployment via Docker or Git Push."
495
+ end
496
+
497
+ else
498
+ help
499
+ end