tina4ruby 3.10.48 → 3.10.54
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/lib/tina4/cli.rb +32 -4
- data/lib/tina4/cors.rb +1 -1
- data/lib/tina4/database/sqlite3_adapter.rb +1 -1
- data/lib/tina4/database.rb +4 -2
- data/lib/tina4/orm.rb +8 -1
- data/lib/tina4/rack_app.rb +23 -3
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +108 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f97899d98f7c34c914f158fc57c2b0253b79e544ad9feacdca10d6a6479209f
|
|
4
|
+
data.tar.gz: 8370cfeefc28525f4cd918543e51e5f444c7da5845c80e58c2dc548d513f26de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dde7f333c0474ff7add07c7693ebbb27b8a11d565f6f031ee0aeee106cd46e3e696a70bed388ceeebd40640b7022eb68e7e12aee55668a8914c3d5ac205cee0b
|
|
7
|
+
data.tar.gz: 0da6d2ab9ea1bb916f305f5f1dc2426dd92e1a54b3ac601c5b9cc607587ca64e7b40e0141e9cee6d7889bf001647c35b52ba2d19441027fe14bba2727d53f235
|
data/lib/tina4/cli.rb
CHANGED
|
@@ -155,7 +155,7 @@ module Tina4
|
|
|
155
155
|
# ── start ─────────────────────────────────────────────────────────────
|
|
156
156
|
|
|
157
157
|
def cmd_start(argv)
|
|
158
|
-
options = { port: nil, host: nil, dev: false, no_browser: false, production: false }
|
|
158
|
+
options = { port: nil, host: nil, dev: false, no_browser: false, no_reload: false, production: false }
|
|
159
159
|
parser = OptionParser.new do |opts|
|
|
160
160
|
opts.banner = "Usage: tina4ruby start [options]"
|
|
161
161
|
opts.on("-p", "--port PORT", Integer, "Port (default: 7147)") { |v| options[:port] = v }
|
|
@@ -163,14 +163,21 @@ module Tina4
|
|
|
163
163
|
opts.on("-d", "--dev", "Enable dev mode with auto-reload") { options[:dev] = true }
|
|
164
164
|
opts.on("--production", "Use production server (Puma)") { options[:production] = true }
|
|
165
165
|
opts.on("--no-browser", "Do not open browser on start") { options[:no_browser] = true }
|
|
166
|
+
opts.on("--no-reload", "Disable file watcher / live-reload") { options[:no_reload] = true }
|
|
166
167
|
end
|
|
167
168
|
parser.parse!(argv)
|
|
168
169
|
|
|
169
|
-
# --no-browser from env
|
|
170
|
-
|
|
170
|
+
# --no-browser from env (TINA4_NO_BROWSER=true)
|
|
171
|
+
no_browser_env = ENV.fetch("TINA4_NO_BROWSER", "").downcase
|
|
172
|
+
if no_browser_env.match?(/\A(true|1|yes)\z/)
|
|
171
173
|
options[:no_browser] = true
|
|
172
174
|
end
|
|
173
175
|
|
|
176
|
+
# --no-reload flag sets TINA4_NO_RELOAD so the existing env check picks it up
|
|
177
|
+
if options[:no_reload]
|
|
178
|
+
ENV["TINA4_NO_RELOAD"] = "true"
|
|
179
|
+
end
|
|
180
|
+
|
|
174
181
|
# Priority: CLI flag > ENV var > default
|
|
175
182
|
options[:port] = resolve_config(:port, options[:port])
|
|
176
183
|
options[:host] = resolve_config(:host, options[:host])
|
|
@@ -190,7 +197,8 @@ module Tina4
|
|
|
190
197
|
load_routes(root_dir)
|
|
191
198
|
|
|
192
199
|
if options[:dev]
|
|
193
|
-
|
|
200
|
+
no_reload = %w[true 1 yes].include?(ENV.fetch("TINA4_NO_RELOAD", "").downcase)
|
|
201
|
+
Tina4::DevReload.start(root_dir: root_dir) unless no_reload
|
|
194
202
|
Tina4::ScssCompiler.compile_all(root_dir)
|
|
195
203
|
end
|
|
196
204
|
|
|
@@ -1281,6 +1289,26 @@ module Tina4
|
|
|
1281
1289
|
].each do |subdir|
|
|
1282
1290
|
FileUtils.mkdir_p(File.join(dir, subdir))
|
|
1283
1291
|
end
|
|
1292
|
+
|
|
1293
|
+
# Copy framework public assets into the project so they're visible
|
|
1294
|
+
framework_public = File.join(File.dirname(__FILE__), "public")
|
|
1295
|
+
project_public = File.join(dir, "src", "public")
|
|
1296
|
+
assets_to_copy = %w[
|
|
1297
|
+
css/tina4.css
|
|
1298
|
+
css/tina4.min.css
|
|
1299
|
+
js/tina4.min.js
|
|
1300
|
+
js/frond.min.js
|
|
1301
|
+
images/tina4-logo-icon.webp
|
|
1302
|
+
]
|
|
1303
|
+
assets_to_copy.each do |asset|
|
|
1304
|
+
src = File.join(framework_public, asset)
|
|
1305
|
+
dst = File.join(project_public, asset)
|
|
1306
|
+
FileUtils.mkdir_p(File.dirname(dst))
|
|
1307
|
+
if File.exist?(src) && !File.exist?(dst)
|
|
1308
|
+
FileUtils.cp(src, dst)
|
|
1309
|
+
puts " Copied #{asset}"
|
|
1310
|
+
end
|
|
1311
|
+
end
|
|
1284
1312
|
end
|
|
1285
1313
|
|
|
1286
1314
|
def create_sample_files(dir, project_name)
|
data/lib/tina4/cors.rb
CHANGED
data/lib/tina4/database.rb
CHANGED
|
@@ -169,12 +169,14 @@ module Tina4
|
|
|
169
169
|
end
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
def fetch(sql, params = [], limit:
|
|
172
|
+
def fetch(sql, params = [], limit: 100, offset: nil)
|
|
173
173
|
offset ||= 0
|
|
174
174
|
drv = current_driver
|
|
175
175
|
|
|
176
176
|
effective_sql = sql
|
|
177
|
-
if
|
|
177
|
+
# Skip appending LIMIT if SQL already has one
|
|
178
|
+
has_limit = sql.upcase.split("--")[0].include?("LIMIT")
|
|
179
|
+
if limit && !has_limit
|
|
178
180
|
effective_sql = drv.apply_limit(effective_sql, limit, offset)
|
|
179
181
|
end
|
|
180
182
|
|
data/lib/tina4/orm.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Tina4
|
|
|
18
18
|
|
|
19
19
|
class << self
|
|
20
20
|
def db
|
|
21
|
-
@db || Tina4.database
|
|
21
|
+
@db || Tina4.database || auto_discover_db
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Per-model database binding
|
|
@@ -345,6 +345,13 @@ module Tina4
|
|
|
345
345
|
|
|
346
346
|
private
|
|
347
347
|
|
|
348
|
+
def auto_discover_db
|
|
349
|
+
url = ENV["DATABASE_URL"]
|
|
350
|
+
return nil unless url
|
|
351
|
+
Tina4.database = Tina4::Database.new(url, username: ENV.fetch("DATABASE_USERNAME", ""), password: ENV.fetch("DATABASE_PASSWORD", ""))
|
|
352
|
+
Tina4.database
|
|
353
|
+
end
|
|
354
|
+
|
|
348
355
|
def find_by_id(id)
|
|
349
356
|
pk = primary_key_field || :id
|
|
350
357
|
sql = "SELECT * FROM #{table_name} WHERE #{pk} = ?"
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -3,6 +3,19 @@ require "json"
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
5
|
module Tina4
|
|
6
|
+
# Middleware wrapper that tags requests arriving on the AI dev port.
|
|
7
|
+
# Suppresses live-reload behaviour so AI tools get stable responses.
|
|
8
|
+
class AiPortRackApp
|
|
9
|
+
def initialize(app)
|
|
10
|
+
@app = app
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
env["tina4.ai_port"] = true
|
|
15
|
+
@app.call(env)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
6
19
|
class RackApp
|
|
7
20
|
STATIC_DIRS = %w[public src/public src/assets assets].freeze
|
|
8
21
|
|
|
@@ -43,6 +56,10 @@ module Tina4
|
|
|
43
56
|
|
|
44
57
|
# Dev dashboard routes (handled before anything else)
|
|
45
58
|
if path.start_with?("/__dev")
|
|
59
|
+
# Block live-reload endpoint on the AI port — AI tools must get stable responses
|
|
60
|
+
if path == "/__dev_reload" && env["tina4.ai_port"]
|
|
61
|
+
return [404, { "content-type" => "text/plain" }, ["Not available on AI port"]]
|
|
62
|
+
end
|
|
46
63
|
dev_response = Tina4::DevAdmin.handle_request(env)
|
|
47
64
|
return dev_response if dev_response
|
|
48
65
|
end
|
|
@@ -95,7 +112,7 @@ module Tina4
|
|
|
95
112
|
matched_pattern: matched_pattern || "(no match)",
|
|
96
113
|
}
|
|
97
114
|
joined = body_parts.join
|
|
98
|
-
overlay = inject_dev_overlay(joined, request_info)
|
|
115
|
+
overlay = inject_dev_overlay(joined, request_info, ai_port: env["tina4.ai_port"])
|
|
99
116
|
rack_response = [status, headers, [overlay]]
|
|
100
117
|
end
|
|
101
118
|
end
|
|
@@ -630,7 +647,7 @@ module Tina4
|
|
|
630
647
|
[-1, {}, []]
|
|
631
648
|
end
|
|
632
649
|
|
|
633
|
-
def inject_dev_overlay(body, request_info)
|
|
650
|
+
def inject_dev_overlay(body, request_info, ai_port: false)
|
|
634
651
|
version = Tina4::VERSION
|
|
635
652
|
method = request_info[:method]
|
|
636
653
|
path = request_info[:path]
|
|
@@ -638,9 +655,11 @@ module Tina4
|
|
|
638
655
|
request_id = Tina4::Log.request_id || "-"
|
|
639
656
|
route_count = Tina4::Router.routes.length
|
|
640
657
|
|
|
658
|
+
ai_badge = ai_port ? '<span style="background:#7c3aed;color:#fff;font-size:10px;padding:1px 6px;border-radius:3px;font-weight:bold;">AI PORT</span>' : ""
|
|
659
|
+
|
|
641
660
|
toolbar = <<~HTML.strip
|
|
642
661
|
<div id="tina4-dev-toolbar" style="position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px;">
|
|
643
|
-
<span id="tina4-ver-btn" style="color:#d32f2f;font-weight:bold;cursor:pointer;text-decoration:underline dotted;" onclick="tina4VersionModal()" title="Click to check for updates">Tina4 v#{version}</span>
|
|
662
|
+
#{ai_badge}<span id="tina4-ver-btn" style="color:#d32f2f;font-weight:bold;cursor:pointer;text-decoration:underline dotted;" onclick="tina4VersionModal()" title="Click to check for updates">Tina4 v#{version}</span>
|
|
644
663
|
<div id="tina4-ver-modal" style="display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #d32f2f;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:monospace;font-size:13px;color:#cdd6f4;">
|
|
645
664
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
|
646
665
|
<strong style="color:#89b4fa;">Version Info</strong>
|
|
@@ -709,6 +728,7 @@ module Tina4
|
|
|
709
728
|
el.style.color='#f38ba8';
|
|
710
729
|
});
|
|
711
730
|
}
|
|
731
|
+
#{ai_port ? "" : "/* tina4:reload-js */"}
|
|
712
732
|
</script>
|
|
713
733
|
HTML
|
|
714
734
|
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4/webserver.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Tina4
|
|
|
11
11
|
def start
|
|
12
12
|
require "webrick"
|
|
13
13
|
require "stringio"
|
|
14
|
+
require "socket"
|
|
14
15
|
Tina4.print_banner(host: @host, port: @port)
|
|
15
16
|
Tina4::Log.info("Starting Tina4 WEBrick server on http://#{@host}:#{@port}")
|
|
16
17
|
@server = WEBrick::HTTPServer.new(
|
|
@@ -101,10 +102,117 @@ module Tina4
|
|
|
101
102
|
servlet.define_method(:webrick_req_port) { port }
|
|
102
103
|
|
|
103
104
|
@server.mount("/", servlet, rack_app)
|
|
105
|
+
|
|
106
|
+
# AI dev port (port + 1) — no-reload, no-browser
|
|
107
|
+
@ai_server = nil
|
|
108
|
+
@ai_thread = nil
|
|
109
|
+
no_ai_port = %w[true 1 yes].include?(ENV.fetch("TINA4_NO_AI_PORT", "").downcase)
|
|
110
|
+
is_debug = %w[true 1 yes].include?(ENV.fetch("TINA4_DEBUG", "").downcase)
|
|
111
|
+
|
|
112
|
+
if is_debug && !no_ai_port
|
|
113
|
+
ai_port = @port + 1
|
|
114
|
+
begin
|
|
115
|
+
test = TCPServer.new("0.0.0.0", ai_port)
|
|
116
|
+
test.close
|
|
117
|
+
|
|
118
|
+
@ai_server = WEBrick::HTTPServer.new(
|
|
119
|
+
BindAddress: @host,
|
|
120
|
+
Port: ai_port,
|
|
121
|
+
Logger: WEBrick::Log.new(File::NULL),
|
|
122
|
+
AccessLog: []
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Wrap the rack app so AI-port requests are tagged
|
|
126
|
+
ai_rack_app = Tina4::AiPortRackApp.new(@app)
|
|
127
|
+
|
|
128
|
+
# Build a servlet identical to the main one but bound to the AI port host/port
|
|
129
|
+
ai_host = @host
|
|
130
|
+
ai_port_str = ai_port.to_s
|
|
131
|
+
ai_servlet = Class.new(WEBrick::HTTPServlet::AbstractServlet) do
|
|
132
|
+
define_method(:initialize) do |server, app|
|
|
133
|
+
super(server)
|
|
134
|
+
@app = app
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
%w[GET POST PUT DELETE PATCH HEAD OPTIONS].each do |http_method|
|
|
138
|
+
define_method("do_#{http_method}") do |webrick_req, webrick_res|
|
|
139
|
+
handle_request(webrick_req, webrick_res)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
define_method(:handle_request) do |webrick_req, webrick_res|
|
|
144
|
+
if Tina4::Shutdown.shutting_down?
|
|
145
|
+
webrick_res.status = 503
|
|
146
|
+
webrick_res.body = '{"error":"Service shutting down"}'
|
|
147
|
+
webrick_res["content-type"] = "application/json"
|
|
148
|
+
return
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
Tina4::Shutdown.track_request do
|
|
152
|
+
env = build_rack_env(webrick_req)
|
|
153
|
+
status, headers, body = @app.call(env)
|
|
154
|
+
|
|
155
|
+
webrick_res.status = status
|
|
156
|
+
headers.each do |key, value|
|
|
157
|
+
if key.downcase == "set-cookie"
|
|
158
|
+
Array(value.split("\n")).each { |c| webrick_res.cookies << WEBrick::Cookie.parse_set_cookie(c) }
|
|
159
|
+
else
|
|
160
|
+
webrick_res[key] = value
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
response_body = ""
|
|
165
|
+
body.each { |chunk| response_body += chunk }
|
|
166
|
+
webrick_res.body = response_body
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
define_method(:build_rack_env) do |req|
|
|
171
|
+
input = StringIO.new(req.body || "")
|
|
172
|
+
env = {
|
|
173
|
+
"REQUEST_METHOD" => req.request_method,
|
|
174
|
+
"PATH_INFO" => req.path,
|
|
175
|
+
"QUERY_STRING" => req.query_string || "",
|
|
176
|
+
"SERVER_NAME" => webrick_req_host,
|
|
177
|
+
"SERVER_PORT" => webrick_req_port,
|
|
178
|
+
"CONTENT_TYPE" => req.content_type || "",
|
|
179
|
+
"CONTENT_LENGTH" => (req.content_length rescue 0).to_s,
|
|
180
|
+
"REMOTE_ADDR" => req.peeraddr&.last || "127.0.0.1",
|
|
181
|
+
"rack.input" => input,
|
|
182
|
+
"rack.errors" => $stderr,
|
|
183
|
+
"rack.url_scheme" => "http",
|
|
184
|
+
"rack.version" => [1, 3],
|
|
185
|
+
"rack.multithread" => true,
|
|
186
|
+
"rack.multiprocess" => false,
|
|
187
|
+
"rack.run_once" => false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
req.header.each do |key, values|
|
|
191
|
+
env_key = "HTTP_#{key.upcase.gsub('-', '_')}"
|
|
192
|
+
env[env_key] = values.join(", ")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
env
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
ai_servlet.define_method(:webrick_req_host) { ai_host }
|
|
200
|
+
ai_servlet.define_method(:webrick_req_port) { ai_port_str }
|
|
201
|
+
|
|
202
|
+
@ai_server.mount("/", ai_servlet, ai_rack_app)
|
|
203
|
+
@ai_thread = Thread.new { @ai_server.start }
|
|
204
|
+
puts " AI Port: http://localhost:#{ai_port} (no-reload)"
|
|
205
|
+
rescue Errno::EADDRINUSE
|
|
206
|
+
puts " AI Port: SKIPPED (port #{ai_port} in use)"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
104
210
|
@server.start
|
|
105
211
|
end
|
|
106
212
|
|
|
107
213
|
def stop
|
|
214
|
+
@ai_server&.shutdown
|
|
215
|
+
@ai_thread&.join(5)
|
|
108
216
|
@server&.shutdown
|
|
109
217
|
end
|
|
110
218
|
end
|