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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +434 -544
- data/exe/{tina4 → tina4ruby} +1 -0
- data/lib/tina4/ai.rb +312 -0
- data/lib/tina4/auth.rb +44 -3
- data/lib/tina4/auto_crud.rb +163 -0
- data/lib/tina4/cli.rb +389 -97
- data/lib/tina4/constants.rb +46 -0
- data/lib/tina4/cors.rb +74 -0
- data/lib/tina4/database/sqlite3_adapter.rb +139 -0
- data/lib/tina4/database.rb +144 -7
- data/lib/tina4/debug.rb +4 -79
- data/lib/tina4/dev_admin.rb +1162 -0
- data/lib/tina4/dev_mailbox.rb +191 -0
- data/lib/tina4/dev_reload.rb +9 -9
- data/lib/tina4/drivers/firebird_driver.rb +19 -3
- data/lib/tina4/drivers/mssql_driver.rb +3 -3
- data/lib/tina4/drivers/mysql_driver.rb +4 -4
- data/lib/tina4/drivers/postgres_driver.rb +9 -2
- data/lib/tina4/drivers/sqlite_driver.rb +1 -1
- data/lib/tina4/env.rb +42 -2
- data/lib/tina4/error_overlay.rb +252 -0
- data/lib/tina4/events.rb +90 -0
- data/lib/tina4/field_types.rb +4 -0
- data/lib/tina4/frond.rb +1497 -0
- data/lib/tina4/gallery/auth/meta.json +1 -0
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
- data/lib/tina4/gallery/database/meta.json +1 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
- data/lib/tina4/gallery/error-overlay/meta.json +1 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
- data/lib/tina4/gallery/orm/meta.json +1 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
- data/lib/tina4/gallery/queue/meta.json +1 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -0
- data/lib/tina4/gallery/rest-api/meta.json +1 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
- data/lib/tina4/gallery/templates/meta.json +1 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
- data/lib/tina4/health.rb +39 -0
- data/lib/tina4/html_element.rb +148 -0
- data/lib/tina4/localization.rb +2 -2
- data/lib/tina4/log.rb +203 -0
- data/lib/tina4/messenger.rb +562 -0
- data/lib/tina4/migration.rb +132 -29
- data/lib/tina4/orm.rb +463 -35
- data/lib/tina4/public/css/tina4.css +178 -1
- data/lib/tina4/public/css/tina4.min.css +1 -2
- data/lib/tina4/public/favicon.ico +0 -0
- data/lib/tina4/public/images/logo.svg +5 -0
- data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
- data/lib/tina4/public/js/frond.min.js +420 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
- data/lib/tina4/public/js/tina4.min.js +93 -0
- data/lib/tina4/public/swagger/index.html +90 -0
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
- data/lib/tina4/queue.rb +162 -6
- data/lib/tina4/queue_backends/lite_backend.rb +88 -0
- data/lib/tina4/rack_app.rb +331 -27
- data/lib/tina4/rate_limiter.rb +123 -0
- data/lib/tina4/request.rb +61 -15
- data/lib/tina4/response.rb +54 -24
- data/lib/tina4/response_cache.rb +551 -0
- data/lib/tina4/router.rb +90 -15
- data/lib/tina4/scss_compiler.rb +2 -2
- data/lib/tina4/seeder.rb +56 -61
- data/lib/tina4/service_runner.rb +303 -0
- data/lib/tina4/session.rb +85 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
- data/lib/tina4/shutdown.rb +84 -0
- data/lib/tina4/sql_translation.rb +295 -0
- data/lib/tina4/template.rb +36 -6
- data/lib/tina4/templates/base.twig +2 -2
- data/lib/tina4/templates/errors/302.twig +14 -0
- data/lib/tina4/templates/errors/401.twig +9 -0
- data/lib/tina4/templates/errors/403.twig +22 -15
- data/lib/tina4/templates/errors/404.twig +22 -15
- data/lib/tina4/templates/errors/500.twig +31 -15
- data/lib/tina4/templates/errors/502.twig +9 -0
- data/lib/tina4/templates/errors/503.twig +12 -0
- data/lib/tina4/templates/errors/base.twig +37 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -18
- data/lib/tina4.rb +118 -21
- metadata +68 -8
- data/lib/tina4/public/js/tina4.js +0 -134
- 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
|
-
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
3
4
|
require "fileutils"
|
|
4
5
|
|
|
5
6
|
module Tina4
|
|
6
|
-
class CLI
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def
|
|
10
|
-
|
|
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,
|
|
54
|
+
create_sample_files(dir, project_name)
|
|
15
55
|
|
|
16
|
-
puts "
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
def
|
|
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
|
-
|
|
285
|
+
|
|
286
|
+
require "irb"
|
|
287
|
+
IRB.start
|
|
153
288
|
end
|
|
154
289
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
372
|
+
Tina4.get "/#{route_path}/:id" do |request, response|
|
|
373
|
+
response.json(data: {})
|
|
374
|
+
end
|
|
167
375
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
|
213
|
-
|
|
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.
|
|
226
|
-
|
|
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 "
|
|
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
|
|
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", "
|
|
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/
|
|
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
|