tina4ruby 0.5.2 → 3.2.1

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +434 -544
  4. data/exe/{tina4 → tina4ruby} +1 -0
  5. data/lib/tina4/ai.rb +312 -0
  6. data/lib/tina4/auth.rb +44 -3
  7. data/lib/tina4/auto_crud.rb +163 -0
  8. data/lib/tina4/cli.rb +389 -97
  9. data/lib/tina4/constants.rb +46 -0
  10. data/lib/tina4/cors.rb +74 -0
  11. data/lib/tina4/database/sqlite3_adapter.rb +139 -0
  12. data/lib/tina4/database.rb +144 -7
  13. data/lib/tina4/debug.rb +4 -79
  14. data/lib/tina4/dev_admin.rb +1162 -0
  15. data/lib/tina4/dev_mailbox.rb +191 -0
  16. data/lib/tina4/dev_reload.rb +9 -9
  17. data/lib/tina4/drivers/firebird_driver.rb +19 -3
  18. data/lib/tina4/drivers/mssql_driver.rb +3 -3
  19. data/lib/tina4/drivers/mysql_driver.rb +4 -4
  20. data/lib/tina4/drivers/postgres_driver.rb +9 -2
  21. data/lib/tina4/drivers/sqlite_driver.rb +1 -1
  22. data/lib/tina4/env.rb +42 -2
  23. data/lib/tina4/error_overlay.rb +252 -0
  24. data/lib/tina4/events.rb +90 -0
  25. data/lib/tina4/field_types.rb +4 -0
  26. data/lib/tina4/frond.rb +1497 -0
  27. data/lib/tina4/gallery/auth/meta.json +1 -0
  28. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
  29. data/lib/tina4/gallery/database/meta.json +1 -0
  30. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
  31. data/lib/tina4/gallery/error-overlay/meta.json +1 -0
  32. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
  33. data/lib/tina4/gallery/orm/meta.json +1 -0
  34. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
  35. data/lib/tina4/gallery/queue/meta.json +1 -0
  36. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -0
  37. data/lib/tina4/gallery/rest-api/meta.json +1 -0
  38. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
  39. data/lib/tina4/gallery/templates/meta.json +1 -0
  40. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
  41. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
  42. data/lib/tina4/health.rb +39 -0
  43. data/lib/tina4/html_element.rb +148 -0
  44. data/lib/tina4/localization.rb +2 -2
  45. data/lib/tina4/log.rb +203 -0
  46. data/lib/tina4/messenger.rb +562 -0
  47. data/lib/tina4/migration.rb +132 -29
  48. data/lib/tina4/orm.rb +463 -35
  49. data/lib/tina4/public/css/tina4.css +178 -1
  50. data/lib/tina4/public/css/tina4.min.css +1 -2
  51. data/lib/tina4/public/favicon.ico +0 -0
  52. data/lib/tina4/public/images/logo.svg +5 -0
  53. data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
  54. data/lib/tina4/public/js/frond.min.js +420 -0
  55. data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
  56. data/lib/tina4/public/js/tina4.min.js +93 -0
  57. data/lib/tina4/public/swagger/index.html +90 -0
  58. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
  59. data/lib/tina4/queue.rb +162 -6
  60. data/lib/tina4/queue_backends/lite_backend.rb +88 -0
  61. data/lib/tina4/rack_app.rb +331 -27
  62. data/lib/tina4/rate_limiter.rb +123 -0
  63. data/lib/tina4/request.rb +61 -15
  64. data/lib/tina4/response.rb +54 -24
  65. data/lib/tina4/response_cache.rb +551 -0
  66. data/lib/tina4/router.rb +90 -15
  67. data/lib/tina4/scss_compiler.rb +2 -2
  68. data/lib/tina4/seeder.rb +56 -61
  69. data/lib/tina4/service_runner.rb +303 -0
  70. data/lib/tina4/session.rb +85 -0
  71. data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
  72. data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
  73. data/lib/tina4/shutdown.rb +84 -0
  74. data/lib/tina4/sql_translation.rb +295 -0
  75. data/lib/tina4/template.rb +36 -6
  76. data/lib/tina4/templates/base.twig +2 -2
  77. data/lib/tina4/templates/errors/302.twig +14 -0
  78. data/lib/tina4/templates/errors/401.twig +9 -0
  79. data/lib/tina4/templates/errors/403.twig +22 -15
  80. data/lib/tina4/templates/errors/404.twig +22 -15
  81. data/lib/tina4/templates/errors/500.twig +31 -15
  82. data/lib/tina4/templates/errors/502.twig +9 -0
  83. data/lib/tina4/templates/errors/503.twig +12 -0
  84. data/lib/tina4/templates/errors/base.twig +37 -0
  85. data/lib/tina4/version.rb +1 -1
  86. data/lib/tina4/webserver.rb +28 -18
  87. data/lib/tina4.rb +118 -21
  88. metadata +68 -8
  89. data/lib/tina4/public/js/tina4.js +0 -134
  90. data/lib/tina4/public/js/tina4helper.js +0 -387
data/lib/tina4/cli.rb CHANGED
@@ -1,32 +1,93 @@
1
1
  # frozen_string_literal: true
2
- require "thor"
2
+
3
+ require "optparse"
3
4
  require "fileutils"
4
5
 
5
6
  module Tina4
6
- class CLI < Thor
7
- desc "init [NAME]", "Initialize a new Tina4 project"
8
- option :template, type: :string, default: "default", desc: "Project template"
9
- def init(name = ".")
10
- dir = name == "." ? Dir.pwd : File.join(Dir.pwd, name)
7
+ class CLI
8
+ COMMANDS = %w[init start migrate seed seed:create test version routes console generate ai help].freeze
9
+
10
+ def self.start(argv)
11
+ new.run(argv)
12
+ end
13
+
14
+ def run(argv)
15
+ command = argv.shift || "help"
16
+ case command
17
+ when "init" then cmd_init(argv)
18
+ when "start" then cmd_start(argv)
19
+ when "migrate" then cmd_migrate(argv)
20
+ when "seed" then cmd_seed(argv)
21
+ when "seed:create" then cmd_seed_create(argv)
22
+ when "test" then cmd_test(argv)
23
+ when "version" then cmd_version
24
+ when "routes" then cmd_routes
25
+ when "console" then cmd_console
26
+ when "generate" then cmd_generate(argv)
27
+ when "ai" then cmd_ai(argv)
28
+ when "help", "-h", "--help" then cmd_help
29
+ else
30
+ puts "Unknown command: #{command}"
31
+ cmd_help
32
+ exit 1
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # ── init ──────────────────────────────────────────────────────────────
39
+
40
+ def cmd_init(argv)
41
+ options = { template: "default" }
42
+ parser = OptionParser.new do |opts|
43
+ opts.banner = "Usage: tina4ruby init [PATH] [options]"
44
+ opts.on("--template TEMPLATE", "Project template (default: default)") { |v| options[:template] = v }
45
+ end
46
+ parser.parse!(argv)
47
+
48
+ name = argv.shift || "."
49
+ dir = File.expand_path(name)
11
50
  FileUtils.mkdir_p(dir)
12
51
 
52
+ project_name = File.basename(dir)
13
53
  create_project_structure(dir)
14
- create_sample_files(dir, name == "." ? File.basename(Dir.pwd) : name)
54
+ create_sample_files(dir, project_name)
15
55
 
16
- puts "Tina4 project initialized in #{dir}"
17
- puts "Run 'cd #{name} && bundle install && tina4 start' to get started" unless name == "."
56
+ puts "\nProject scaffolded at #{dir}"
57
+ if name == "."
58
+ puts " bundle install"
59
+ puts " ruby app.rb"
60
+ else
61
+ puts " cd #{dir}"
62
+ puts " bundle install"
63
+ puts " ruby app.rb"
64
+ end
18
65
  end
19
66
 
20
- desc "start", "Start the Tina4 web server"
21
- option :port, type: :numeric, default: 7145, aliases: "-p"
22
- option :host, type: :string, default: "0.0.0.0", aliases: "-h"
23
- option :dev, type: :boolean, default: false, aliases: "-d", desc: "Enable dev mode with auto-reload"
24
- def start
67
+ # ── start ─────────────────────────────────────────────────────────────
68
+
69
+ def cmd_start(argv)
70
+ options = { port: nil, host: nil, dev: false }
71
+ parser = OptionParser.new do |opts|
72
+ opts.banner = "Usage: tina4ruby start [options]"
73
+ opts.on("-p", "--port PORT", Integer, "Port (default: 7147)") { |v| options[:port] = v }
74
+ opts.on("-h", "--host HOST", "Host (default: 0.0.0.0)") { |v| options[:host] = v }
75
+ opts.on("-d", "--dev", "Enable dev mode with auto-reload") { options[:dev] = true }
76
+ end
77
+ parser.parse!(argv)
78
+
79
+ # Priority: CLI flag > ENV var > default
80
+ options[:port] = resolve_config(:port, options[:port])
81
+ options[:host] = resolve_config(:host, options[:host])
82
+
25
83
  require_relative "../tina4"
26
84
 
27
85
  root_dir = Dir.pwd
28
86
  Tina4.initialize!(root_dir)
29
87
 
88
+ # Register health check endpoint
89
+ Tina4::Health.register!
90
+
30
91
  # Load route files
31
92
  load_routes(root_dir)
32
93
 
@@ -37,39 +98,58 @@ module Tina4
37
98
 
38
99
  app = Tina4::RackApp.new(root_dir: root_dir)
39
100
 
101
+ is_debug = Tina4::Env.truthy?(ENV["TINA4_DEBUG"])
102
+
40
103
  # Try Puma first (production-grade), fall back to WEBrick
41
- begin
42
- require "puma"
43
- require "puma/configuration"
44
- require "puma/launcher"
45
-
46
- puma_host = options[:host]
47
- puma_port = options[:port]
48
-
49
- config = Puma::Configuration.new do |user_config|
50
- user_config.bind "tcp://#{puma_host}:#{puma_port}"
51
- user_config.app app
52
- user_config.threads 0, 16
53
- user_config.workers 0
54
- user_config.environment "development"
55
- user_config.log_requests false
56
- user_config.quiet
57
- end
104
+ # In debug mode, always use WEBrick for dev toolbar/reload support
105
+ if !is_debug
106
+ begin
107
+ require "puma"
108
+ require "puma/configuration"
109
+ require "puma/launcher"
110
+
111
+ puma_host = options[:host]
112
+ puma_port = options[:port]
113
+
114
+ config = Puma::Configuration.new do |user_config|
115
+ user_config.bind "tcp://#{puma_host}:#{puma_port}"
116
+ user_config.app app
117
+ user_config.threads 0, 16
118
+ user_config.workers 0
119
+ user_config.environment "production"
120
+ user_config.log_requests false
121
+ user_config.quiet
122
+ end
123
+
124
+ Tina4::Log.info("Production server: puma")
58
125
 
59
- Tina4::Debug.info("Starting Puma server on http://#{puma_host}:#{puma_port}")
60
- launcher = Puma::Launcher.new(config)
61
- launcher.run
62
- rescue LoadError
63
- Tina4::Debug.info("Puma not found, falling back to WEBrick")
64
- server = Tina4::WebServer.new(app, host: options[:host], port: options[:port])
65
- server.start
126
+ # Setup graceful shutdown (Puma manages its own signals, but we handle DB cleanup)
127
+ Tina4::Shutdown.setup
128
+
129
+ launcher = Puma::Launcher.new(config)
130
+ launcher.run
131
+ return
132
+ rescue LoadError
133
+ # Puma not installed, fall through to WEBrick
134
+ end
66
135
  end
136
+
137
+ Tina4::Log.info("Development server: WEBrick")
138
+ server = Tina4::WebServer.new(app, host: options[:host], port: options[:port])
139
+ server.start
67
140
  end
68
141
 
69
- desc "migrate", "Run database migrations"
70
- option :create, type: :string, desc: "Create a new migration"
71
- option :rollback, type: :numeric, desc: "Rollback N migrations"
72
- def migrate
142
+ # ── migrate ───────────────────────────────────────────────────────────
143
+
144
+ def cmd_migrate(argv)
145
+ options = {}
146
+ parser = OptionParser.new do |opts|
147
+ opts.banner = "Usage: tina4ruby migrate [options]"
148
+ opts.on("--create NAME", "Create a new migration") { |v| options[:create] = v }
149
+ opts.on("--rollback N", Integer, "Rollback N migrations") { |v| options[:rollback] = v }
150
+ end
151
+ parser.parse!(argv)
152
+
73
153
  require_relative "../tina4"
74
154
  Tina4.initialize!(Dir.pwd)
75
155
 
@@ -100,8 +180,59 @@ module Tina4
100
180
  end
101
181
  end
102
182
 
103
- desc "test", "Run inline tests"
104
- def test
183
+ # ── seed ──────────────────────────────────────────────────────────────
184
+
185
+ def cmd_seed(argv)
186
+ options = { clear: false }
187
+ parser = OptionParser.new do |opts|
188
+ opts.banner = "Usage: tina4ruby seed [options]"
189
+ opts.on("--clear", "Clear tables before seeding") { options[:clear] = true }
190
+ end
191
+ parser.parse!(argv)
192
+
193
+ require_relative "../tina4"
194
+ Tina4.initialize!(Dir.pwd)
195
+ load_routes(Dir.pwd)
196
+ Tina4.seed(seed_folder: "seeds", clear: options[:clear])
197
+ end
198
+
199
+ # ── seed:create ───────────────────────────────────────────────────────
200
+
201
+ def cmd_seed_create(argv)
202
+ name = argv.shift
203
+ unless name
204
+ puts "Usage: tina4ruby seed:create NAME"
205
+ exit 1
206
+ end
207
+
208
+ dir = File.join(Dir.pwd, "seeds")
209
+ FileUtils.mkdir_p(dir)
210
+
211
+ existing = Dir.glob(File.join(dir, "*.rb")).select { |f| File.basename(f)[0] =~ /\d/ }.sort
212
+ numbers = existing.map { |f| File.basename(f).match(/^(\d+)/)[1].to_i }
213
+ next_num = numbers.empty? ? 1 : numbers.max + 1
214
+
215
+ clean_name = name.strip.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")
216
+ filename = format("%03d_%s.rb", next_num, clean_name)
217
+ filepath = File.join(dir, filename)
218
+
219
+ File.write(filepath, <<~RUBY)
220
+ # Seed: #{name.strip}
221
+ #
222
+ # This file is executed by `tina4ruby seed`.
223
+ # Use Tina4.seed_orm or Tina4.seed_table to populate data.
224
+ #
225
+ # Examples:
226
+ # Tina4.seed_orm(User, count: 50)
227
+ # Tina4.seed_table("audit_log", { action: :string, created_at: :datetime }, count: 100)
228
+ RUBY
229
+
230
+ puts "Created seed file: #{filepath}"
231
+ end
232
+
233
+ # ── test ──────────────────────────────────────────────────────────────
234
+
235
+ def cmd_test(argv)
105
236
  require_relative "../tina4"
106
237
  Tina4.initialize!(Dir.pwd)
107
238
 
@@ -121,14 +252,16 @@ module Tina4
121
252
  exit(1) if results[:failed] > 0 || results[:errors] > 0
122
253
  end
123
254
 
124
- desc "version", "Show Tina4 version"
125
- def version
255
+ # ── version ───────────────────────────────────────────────────────────
256
+
257
+ def cmd_version
126
258
  require_relative "version"
127
259
  puts "Tina4 Ruby v#{Tina4::VERSION}"
128
260
  end
129
261
 
130
- desc "routes", "List all registered routes"
131
- def routes
262
+ # ── routes ────────────────────────────────────────────────────────────
263
+
264
+ def cmd_routes
132
265
  require_relative "../tina4"
133
266
  Tina4.initialize!(Dir.pwd)
134
267
  load_routes(Dir.pwd)
@@ -143,53 +276,210 @@ module Tina4
143
276
  puts "Total: #{Tina4::Router.routes.length} routes\n"
144
277
  end
145
278
 
146
- desc "seed", "Run all seed files in seeds/"
147
- option :clear, type: :boolean, default: false, desc: "Clear tables before seeding"
148
- def seed
279
+ # ── console ───────────────────────────────────────────────────────────
280
+
281
+ def cmd_console
149
282
  require_relative "../tina4"
150
283
  Tina4.initialize!(Dir.pwd)
151
284
  load_routes(Dir.pwd)
152
- Tina4.seed(seed_folder: "seeds", clear: options[:clear])
285
+
286
+ require "irb"
287
+ IRB.start
153
288
  end
154
289
 
155
- desc "seed:create NAME", "Create a new seed file"
156
- def seed_create(name)
157
- dir = File.join(Dir.pwd, "seeds")
290
+ # ── ai ────────────────────────────────────────────────────────────────
291
+
292
+ def cmd_ai(argv)
293
+ options = { all: false, force: false }
294
+ parser = OptionParser.new do |opts|
295
+ opts.banner = "Usage: tina4ruby ai [options]"
296
+ opts.on("--all", "Install context for ALL AI tools (not just detected ones)") { options[:all] = true }
297
+ opts.on("--force", "Overwrite existing context files") { options[:force] = true }
298
+ end
299
+ parser.parse!(argv)
300
+
301
+ require_relative "ai"
302
+
303
+ root_dir = Dir.pwd
304
+ puts Tina4::AI.status_report(root_dir)
305
+
306
+ if options[:all]
307
+ created = Tina4::AI.install_all(root_dir, force: options[:force])
308
+ else
309
+ created = Tina4::AI.install_ai_context(root_dir, force: options[:force])
310
+ end
311
+
312
+ if created.any?
313
+ puts "Created/updated context files:"
314
+ created.each { |f| puts " #{f}" }
315
+ else
316
+ puts "No context files were created (files already exist; use --force to overwrite)."
317
+ end
318
+ end
319
+
320
+ # ── help ──────────────────────────────────────────────────────────────
321
+
322
+ # ── generate ────────────────────────────────────────────────────────
323
+
324
+ def cmd_generate(argv)
325
+ what = argv.shift
326
+ name = argv.shift
327
+ unless what && name
328
+ puts "Usage: tina4ruby generate <what> <name>"
329
+ puts " Generators: model, route, migration, middleware"
330
+ exit 1
331
+ end
332
+
333
+ case what
334
+ when "model" then generate_model(name)
335
+ when "route" then generate_route(name)
336
+ when "migration" then generate_migration(name)
337
+ when "middleware" then generate_middleware(name)
338
+ else
339
+ puts "Unknown generator: #{what}"
340
+ puts " Available: model, route, migration, middleware"
341
+ exit 1
342
+ end
343
+ end
344
+
345
+ def generate_model(name)
346
+ dir = "src/orm"
158
347
  FileUtils.mkdir_p(dir)
348
+ snake = name.gsub(/([A-Z])/) { |m| ($~.begin(0) > 0 ? "_" : "") + m.downcase }
349
+ path = File.join(dir, "#{snake}.rb")
350
+ abort " File already exists: #{path}" if File.exist?(path)
351
+ File.write(path, <<~RUBY)
352
+ class #{name} < Tina4::ORM
353
+ integer_field :id, primary_key: true, auto_increment: true
354
+ string_field :name
355
+ string_field :email
356
+ end
357
+ RUBY
358
+ puts " Created #{path}"
359
+ end
159
360
 
160
- existing = Dir.glob(File.join(dir, "*.rb")).select { |f| File.basename(f)[0] =~ /\d/ }.sort
161
- numbers = existing.map { |f| File.basename(f).match(/^(\d+)/)[1].to_i }
162
- next_num = numbers.empty? ? 1 : numbers.max + 1
361
+ def generate_route(name)
362
+ route_path = name.sub(%r{^/}, "")
363
+ dir = "src/routes/#{route_path}"
364
+ FileUtils.mkdir_p(dir)
365
+ path = dir.chomp("/") + ".rb"
366
+ abort " File already exists: #{path}" if File.exist?(path)
367
+ File.write(path, <<~RUBY)
368
+ Tina4.get "/#{route_path}" do |request, response|
369
+ response.json(data: [])
370
+ end
163
371
 
164
- clean_name = name.strip.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")
165
- filename = format("%03d_%s.rb", next_num, clean_name)
166
- filepath = File.join(dir, filename)
372
+ Tina4.get "/#{route_path}/:id" do |request, response|
373
+ response.json(data: {})
374
+ end
167
375
 
168
- File.write(filepath, <<~RUBY)
169
- # Seed: #{name.strip}
170
- #
171
- # This file is executed by `tina4 seed`.
172
- # Use Tina4.seed_orm or Tina4.seed_table to populate data.
173
- #
174
- # Examples:
175
- # Tina4.seed_orm(User, count: 50)
176
- # Tina4.seed_table("audit_log", { action: :string, created_at: :datetime }, count: 100)
376
+ Tina4.post "/#{route_path}" do |request, response|
377
+ response.json({ message: "created" }, 201)
378
+ end
379
+
380
+ Tina4.put "/#{route_path}/:id" do |request, response|
381
+ response.json(message: "updated")
382
+ end
383
+
384
+ Tina4.delete "/#{route_path}/:id" do |request, response|
385
+ response.json(message: "deleted")
386
+ end
177
387
  RUBY
388
+ puts " Created #{path}"
389
+ end
178
390
 
179
- puts "Created seed file: #{filepath}"
391
+ def generate_migration(name)
392
+ dir = "migrations"
393
+ FileUtils.mkdir_p(dir)
394
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
395
+ table = name.sub(/^create_/, "")
396
+ table = if table.end_with?("s")
397
+ table
398
+ elsif table.end_with?("y")
399
+ table[0..-2] + "ies"
400
+ else
401
+ table + "s"
402
+ end
403
+ filename = "#{timestamp}_#{name}.sql"
404
+ path = File.join(dir, filename)
405
+ now = Time.now.strftime("%Y-%m-%d %H:%M:%S")
406
+ File.write(path, <<~SQL)
407
+ -- Migration: #{name}
408
+ -- Created: #{now}
409
+
410
+ CREATE TABLE #{table} (
411
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
412
+ name TEXT NOT NULL,
413
+ email TEXT NOT NULL,
414
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
415
+ );
416
+ SQL
417
+ puts " Created #{path}"
180
418
  end
181
419
 
182
- desc "console", "Start an interactive console"
183
- def console
184
- require_relative "../tina4"
185
- Tina4.initialize!(Dir.pwd)
186
- load_routes(Dir.pwd)
420
+ def generate_middleware(name)
421
+ dir = "src/middleware"
422
+ FileUtils.mkdir_p(dir)
423
+ snake = name.gsub(/([A-Z])/) { |m| ($~.begin(0) > 0 ? "_" : "") + m.downcase }
424
+ path = File.join(dir, "#{snake}.rb")
425
+ abort " File already exists: #{path}" if File.exist?(path)
426
+ File.write(path, <<~RUBY)
427
+ class #{name} < Tina4::Middleware
428
+ def process(request, response)
429
+ auth = request.headers["Authorization"]
430
+ return response.json({ error: "Unauthorized" }, 401) unless auth
431
+
432
+ nil
433
+ end
434
+ end
435
+ RUBY
436
+ puts " Created #{path}"
437
+ end
187
438
 
188
- require "irb"
189
- IRB.start
439
+ def cmd_help
440
+ puts <<~HELP
441
+ Tina4 Ruby CLI
442
+
443
+ Usage: tina4ruby COMMAND [options]
444
+
445
+ Commands:
446
+ init [NAME] Initialize a new Tina4 project
447
+ start Start the Tina4 web server
448
+ migrate Run database migrations
449
+ seed Run all seed files in seeds/
450
+ seed:create NAME Create a new seed file
451
+ test Run inline tests
452
+ version Show Tina4 version
453
+ routes List all registered routes
454
+ console Start an interactive console
455
+ generate <what> <name> Generate scaffolding (model, route, migration, middleware)
456
+ ai Detect AI tools and install context files
457
+ help Show this help message
458
+
459
+ Run 'tina4ruby COMMAND --help' for more information on a command.
460
+ HELP
190
461
  end
191
462
 
192
- private
463
+ # ── config resolution ──────────────────────────────────────────────────
464
+
465
+ DEFAULT_PORT = 7147
466
+ DEFAULT_HOST = "0.0.0.0"
467
+
468
+ # Priority: CLI flag > ENV var > default
469
+ def resolve_config(key, cli_value)
470
+ case key
471
+ when :port
472
+ return cli_value if cli_value
473
+ return ENV["PORT"].to_i if ENV["PORT"] && !ENV["PORT"].empty?
474
+ DEFAULT_PORT
475
+ when :host
476
+ return cli_value if cli_value
477
+ return ENV["HOST"] if ENV["HOST"] && !ENV["HOST"].empty?
478
+ DEFAULT_HOST
479
+ end
480
+ end
481
+
482
+ # ── shared helpers ────────────────────────────────────────────────────
193
483
 
194
484
  def load_routes(root_dir)
195
485
  route_dirs = %w[routes src/routes src/api api]
@@ -209,8 +499,9 @@ module Tina4
209
499
 
210
500
  def create_project_structure(dir)
211
501
  %w[
212
- routes templates public public/css public/js public/images
213
- migrations src logs
502
+ src/routes src/orm src/templates src/templates/errors
503
+ src/public src/public/css src/public/js src/public/images
504
+ migrations logs
214
505
  ].each do |subdir|
215
506
  FileUtils.mkdir_p(File.join(dir, subdir))
216
507
  end
@@ -221,14 +512,9 @@ module Tina4
221
512
  unless File.exist?(File.join(dir, "app.rb"))
222
513
  File.write(File.join(dir, "app.rb"), <<~RUBY)
223
514
  require "tina4"
224
-
225
- Tina4.get "/" do |request, response|
226
- response.html "<h1>Welcome to #{project_name}!</h1><p>Powered by Tina4 Ruby</p>"
227
- end
228
-
229
- Tina4.get "/api/hello" do |request, response|
230
- response.json({ message: "Hello from Tina4!", timestamp: Time.now.iso8601 })
231
- end
515
+ Tina4.initialize!(__dir__)
516
+ app = Tina4::RackApp.new
517
+ Tina4::WebServer.new(app, port: 7147).start
232
518
  RUBY
233
519
  end
234
520
 
@@ -236,10 +522,18 @@ module Tina4
236
522
  unless File.exist?(File.join(dir, "Gemfile"))
237
523
  File.write(File.join(dir, "Gemfile"), <<~RUBY)
238
524
  source "https://rubygems.org"
239
- gem "tina4ruby"
525
+ gem "tina4-ruby", "~> 3.0"
240
526
  RUBY
241
527
  end
242
528
 
529
+ # .env
530
+ unless File.exist?(File.join(dir, ".env"))
531
+ File.write(File.join(dir, ".env"), <<~TEXT)
532
+ TINA4_DEBUG=true
533
+ TINA4_LOG_LEVEL=ALL
534
+ TEXT
535
+ end
536
+
243
537
  # .gitignore
244
538
  unless File.exist?(File.join(dir, ".gitignore"))
245
539
  File.write(File.join(dir, ".gitignore"), <<~TEXT)
@@ -291,7 +585,7 @@ module Tina4
291
585
  # Copy application code
292
586
  COPY --from=builder /app /app
293
587
 
294
- EXPOSE 7145
588
+ EXPOSE 7147
295
589
 
296
590
  # Swagger defaults (override with env vars in docker-compose/k8s if needed)
297
591
  ENV SWAGGER_TITLE="Tina4 API"
@@ -299,9 +593,8 @@ module Tina4
299
593
  ENV SWAGGER_DESCRIPTION="Auto-generated API documentation"
300
594
 
301
595
  # Start the server on all interfaces
302
- CMD ["bundle", "exec", "tina4", "start", "-p", "7145", "-h", "0.0.0.0"]
596
+ CMD ["bundle", "exec", "tina4ruby", "start", "-p", "7147", "-h", "0.0.0.0"]
303
597
  DOCKERFILE
304
- puts " Created Dockerfile"
305
598
  end
306
599
 
307
600
  # .dockerignore
@@ -319,11 +612,10 @@ module Tina4
319
612
  spec/
320
613
  vendor/bundle
321
614
  TEXT
322
- puts " Created .dockerignore"
323
615
  end
324
616
 
325
617
  # Base template
326
- templates_dir = File.join(dir, "templates")
618
+ templates_dir = File.join(dir, "src", "templates")
327
619
  unless File.exist?(File.join(templates_dir, "base.twig"))
328
620
  File.write(File.join(templates_dir, "base.twig"), <<~HTML)
329
621
  <!DOCTYPE html>
@@ -337,8 +629,8 @@ module Tina4
337
629
  </head>
338
630
  <body>
339
631
  {% block content %}{% endblock %}
340
- <script src="/js/tina4.js"></script>
341
- <script src="/js/tina4helper.js"></script>
632
+ <script src="/js/tina4.min.js"></script>
633
+ <script src="/js/frond.min.js"></script>
342
634
  {% block scripts %}{% endblock %}
343
635
  </body>
344
636
  </html>
@@ -0,0 +1,46 @@
1
+ # Tina4 Constants — HTTP status codes and content types.
2
+ #
3
+ # Standard constants for use in route handlers across all Tina4 frameworks.
4
+ #
5
+ # Tina4.get "/api/users" do |request, response|
6
+ # response.call(users, Tina4::HTTP_OK)
7
+ # end
8
+
9
+ module Tina4
10
+ # ── HTTP Status Codes ──
11
+
12
+ HTTP_OK = 200
13
+ HTTP_CREATED = 201
14
+ HTTP_ACCEPTED = 202
15
+ HTTP_NO_CONTENT = 204
16
+
17
+ HTTP_MOVED = 301
18
+ HTTP_REDIRECT = 302
19
+ HTTP_NOT_MODIFIED = 304
20
+
21
+ HTTP_BAD_REQUEST = 400
22
+ HTTP_UNAUTHORIZED = 401
23
+ HTTP_FORBIDDEN = 403
24
+ HTTP_NOT_FOUND = 404
25
+ HTTP_METHOD_NOT_ALLOWED = 405
26
+ HTTP_CONFLICT = 409
27
+ HTTP_GONE = 410
28
+ HTTP_UNPROCESSABLE = 422
29
+ HTTP_TOO_MANY = 429
30
+
31
+ HTTP_SERVER_ERROR = 500
32
+ HTTP_BAD_GATEWAY = 502
33
+ HTTP_UNAVAILABLE = 503
34
+
35
+ # ── Content Types ──
36
+
37
+ APPLICATION_JSON = "application/json"
38
+ APPLICATION_XML = "application/xml"
39
+ APPLICATION_FORM = "application/x-www-form-urlencoded"
40
+ APPLICATION_OCTET = "application/octet-stream"
41
+
42
+ TEXT_HTML = "text/html; charset=utf-8"
43
+ TEXT_PLAIN = "text/plain; charset=utf-8"
44
+ TEXT_CSV = "text/csv"
45
+ TEXT_XML = "text/xml"
46
+ end