tina4ruby 3.13.34 → 3.13.36
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/dev_admin.rb +160 -15
- data/lib/tina4/mcp.rb +16 -6
- data/lib/tina4/public/js/tina4-dev-admin.js +437 -759
- data/lib/tina4/public/js/tina4-dev-admin.min.js +437 -759
- data/lib/tina4/rack_app.rb +105 -1
- data/lib/tina4/router.rb +43 -9
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/websocket.rb +48 -0
- data/lib/tina4.rb +1 -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: 7266dfb9cb9605c62b61d64de0c2586ff6824f2a22ad87aae74403f1d171c495
|
|
4
|
+
data.tar.gz: 0f9dffee4f4242c9c03899b79742a314bde065308da3e852c1af76fb1ade6be9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad75e74701786bf08fd4fc3c143a0d5f6731dbe0aa5feb5d46afb5c2472a113daa9dd0d2962f616c0a811318fed23091e40248117aee22923ea7fa35a81b6ab5
|
|
7
|
+
data.tar.gz: c5622aff3bf9fb522969dbe5431ae1a2aa8416712ade87ccd277529418a1aea6a427a25a211198342cfca0bebcff81305db70e6be01a0a11cfd7a2e99ebd6a9a
|
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -392,12 +392,30 @@ module Tina4
|
|
|
392
392
|
reload_type = body["type"] || "reload"
|
|
393
393
|
Tina4::Log.info("External reload trigger: #{reload_type}#{@reload_file.empty? ? '' : " (#{@reload_file})"}")
|
|
394
394
|
# Re-discover so files dropped into src/routes/ register without
|
|
395
|
-
# a server restart. Idempotent — already-loaded files are skipped
|
|
395
|
+
# a server restart. Idempotent — already-loaded files are skipped,
|
|
396
|
+
# changed files are re-loaded (mtime-tracked).
|
|
396
397
|
begin
|
|
397
398
|
Tina4::Router.rescan_routes!
|
|
398
399
|
rescue StandardError => e
|
|
399
400
|
Tina4::Log.error("Re-discover on reload failed: #{e.message}")
|
|
400
401
|
end
|
|
402
|
+
# WebSocket-primary reload: push an instant message to every browser
|
|
403
|
+
# connected on /__dev_reload. The toolbar client (and the dev-admin
|
|
404
|
+
# dashboard) act on this immediately — the mtime poll above is only a
|
|
405
|
+
# fallback for when the socket is down. CSS changes swap stylesheets;
|
|
406
|
+
# everything else triggers a full page reload. We normalise the wire
|
|
407
|
+
# `type` to "css"/"reload" (the clients only react to css/reload/change)
|
|
408
|
+
# but still echo the caller's original type in the HTTP response.
|
|
409
|
+
# Wrapped so a broadcast failure — or zero connected clients — never
|
|
410
|
+
# 500s the reload endpoint.
|
|
411
|
+
begin
|
|
412
|
+
ws_type = reload_type == "css" ? "css" : "reload"
|
|
413
|
+
Tina4::DevReload.broadcast(
|
|
414
|
+
JSON.generate({ type: ws_type, file: @reload_file, mtime: @reload_mtime })
|
|
415
|
+
)
|
|
416
|
+
rescue StandardError => e
|
|
417
|
+
Tina4::Log.error("Dev-reload WebSocket broadcast failed: #{e.message}")
|
|
418
|
+
end
|
|
401
419
|
json_response({ ok: true, type: reload_type })
|
|
402
420
|
when ["GET", "/__dev/api/status"]
|
|
403
421
|
json_response(status_payload)
|
|
@@ -614,6 +632,14 @@ module Tina4
|
|
|
614
632
|
when ["POST", "/__dev/api/mcp/call"]
|
|
615
633
|
body = read_json_body(env) || {}
|
|
616
634
|
json_response(mcp_tool_call(body))
|
|
635
|
+
# JSON-RPC + SSE endpoints that real MCP clients (Claude Code/Desktop)
|
|
636
|
+
# speak. Mounted on the same dispatch as the REST shim above and gated
|
|
637
|
+
# on the same enabled? (TINA4_DEBUG) check, so disabled → 404. They
|
|
638
|
+
# share the default MCP server's tool registry with the REST shim.
|
|
639
|
+
when ["POST", "/__dev/mcp"], ["POST", "/__dev/mcp/message"]
|
|
640
|
+
mcp_jsonrpc(env)
|
|
641
|
+
when ["GET", "/__dev/mcp/sse"]
|
|
642
|
+
mcp_sse_handshake
|
|
617
643
|
when ["GET", "/__dev/api/scaffold"]
|
|
618
644
|
json_response(scaffold_templates)
|
|
619
645
|
when ["POST", "/__dev/api/scaffold/run"]
|
|
@@ -1250,24 +1276,120 @@ module Tina4
|
|
|
1250
1276
|
resolved
|
|
1251
1277
|
end
|
|
1252
1278
|
|
|
1279
|
+
# Noise dirs + hidden dot-entries are hidden from the file browser,
|
|
1280
|
+
# except the env files — verbatim parity with PHP/Python dev-admin.
|
|
1281
|
+
DEV_FILES_IGNORED = %w[__pycache__ node_modules vendor .git venv .venv dist target .tina4].freeze
|
|
1282
|
+
|
|
1283
|
+
def dev_files_hidden?(name)
|
|
1284
|
+
return true if DEV_FILES_IGNORED.include?(name)
|
|
1285
|
+
name.start_with?(".") && name != ".env" && name != ".env.example"
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
# Same 4-status mapping Python/PHP use for a porcelain code.
|
|
1289
|
+
def dev_git_status_label(code)
|
|
1290
|
+
return "untracked" if code == "??"
|
|
1291
|
+
return "modified" if code.include?("M")
|
|
1292
|
+
return "added" if code.include?("A")
|
|
1293
|
+
return "deleted" if code.include?("D")
|
|
1294
|
+
"clean"
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
# Branch + porcelain status map for the file browser (mirrors PHP/Python
|
|
1298
|
+
# dev-admin). Paths git reports are relative to the repo root (always
|
|
1299
|
+
# forward-slash); the project root may sit inside a larger repo, so the
|
|
1300
|
+
# toplevel is returned too for rebasing. Empty on any error / no git.
|
|
1301
|
+
def dev_git_info(root)
|
|
1302
|
+
require "shellwords" unless defined?(Shellwords)
|
|
1303
|
+
esc = Shellwords.escape(root)
|
|
1304
|
+
inside = `cd #{esc} && git rev-parse --is-inside-work-tree 2>/dev/null`.strip
|
|
1305
|
+
return { branch: "", git_root: nil, status: {} } unless inside == "true"
|
|
1306
|
+
|
|
1307
|
+
branch = `cd #{esc} && git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
1308
|
+
top = `cd #{esc} && git rev-parse --show-toplevel 2>/dev/null`.strip
|
|
1309
|
+
git_root = top.empty? ? nil : top.tr("\\", "/").sub(%r{/+\z}, "")
|
|
1310
|
+
|
|
1311
|
+
status = {}
|
|
1312
|
+
`cd #{esc} && git status --porcelain -uall 2>/dev/null`.each_line do |line|
|
|
1313
|
+
line = line.chomp
|
|
1314
|
+
next if line.length < 4
|
|
1315
|
+
code = line[0, 2].strip
|
|
1316
|
+
path = line[3..].to_s.strip
|
|
1317
|
+
if (idx = path.index(" -> ")) # rename/copy — keep destination
|
|
1318
|
+
path = path[(idx + 4)..]
|
|
1319
|
+
end
|
|
1320
|
+
next if path.nil? || path.empty?
|
|
1321
|
+
status[path] = code
|
|
1322
|
+
end
|
|
1323
|
+
{ branch: branch, git_root: git_root, status: status }
|
|
1324
|
+
rescue StandardError
|
|
1325
|
+
{ branch: "", git_root: nil, status: {} }
|
|
1326
|
+
end
|
|
1327
|
+
|
|
1253
1328
|
def files_list(env)
|
|
1329
|
+
# Response shape matches tina4-python / tina4-php 1:1 so the dev-admin
|
|
1330
|
+
# SPA works against every framework: each entry carries `is_dir`,
|
|
1331
|
+
# `has_children`, `git_status` and `size`; the payload carries `branch`.
|
|
1254
1332
|
rel = query_param(env, "path") || "."
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1333
|
+
root = File.expand_path(Dir.pwd)
|
|
1334
|
+
git = dev_git_info(root)
|
|
1335
|
+
|
|
1336
|
+
# Missing/invalid paths return an empty-but-valid shape (not an error
|
|
1337
|
+
# body): the SPA restores expanded-folder state from localStorage and
|
|
1338
|
+
# non-existent folders would otherwise spam the console.
|
|
1339
|
+
target = begin
|
|
1340
|
+
safe_project_path(rel)
|
|
1341
|
+
rescue StandardError
|
|
1342
|
+
nil
|
|
1343
|
+
end
|
|
1344
|
+
unless target && File.directory?(target)
|
|
1345
|
+
return { path: rel, branch: git[:branch], entries: [], error: "not a directory" }
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
root_fwd = root.tr("\\", "/")
|
|
1349
|
+
cwd_in_git = ""
|
|
1350
|
+
if git[:git_root] && git[:git_root] != root_fwd && root_fwd.start_with?(git[:git_root])
|
|
1351
|
+
cwd_in_git = root_fwd[git[:git_root].length..].to_s.sub(%r{\A/+}, "")
|
|
1352
|
+
cwd_in_git += "/" unless cwd_in_git.empty?
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
entries = Dir.children(target).sort.filter_map do |name|
|
|
1356
|
+
next if dev_files_hidden?(name)
|
|
1357
|
+
full = File.join(target, name)
|
|
1358
|
+
entry_rel = full[root.length..].to_s.sub(%r{\A/+}, "").tr("\\", "/")
|
|
1359
|
+
is_dir = File.directory?(full)
|
|
1360
|
+
|
|
1361
|
+
# git status for this entry (same mapping PHP/Python use)
|
|
1362
|
+
git_path = cwd_in_git + entry_rel
|
|
1363
|
+
label = "clean"
|
|
1364
|
+
if (code = git[:status][git_path])
|
|
1365
|
+
label = dev_git_status_label(code)
|
|
1366
|
+
elsif is_dir
|
|
1367
|
+
prefix = "#{git_path}/" # propagate dirty status from any child
|
|
1368
|
+
hit = git[:status].find { |gf, _| gf.start_with?(prefix) }
|
|
1369
|
+
label = (hit && hit[1] == "??") ? "untracked" : "modified" if hit
|
|
1266
1370
|
end
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1371
|
+
|
|
1372
|
+
# has_children: does the dir contain anything visible?
|
|
1373
|
+
has_children = nil
|
|
1374
|
+
if is_dir
|
|
1375
|
+
has_children = begin
|
|
1376
|
+
Dir.children(full).any? { |c| !dev_files_hidden?(c) }
|
|
1377
|
+
rescue StandardError
|
|
1378
|
+
false
|
|
1379
|
+
end
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1382
|
+
{
|
|
1383
|
+
name: name,
|
|
1384
|
+
path: entry_rel,
|
|
1385
|
+
is_dir: is_dir,
|
|
1386
|
+
has_children: has_children,
|
|
1387
|
+
git_status: label,
|
|
1388
|
+
size: is_dir ? nil : (File.size(full) rescue 0)
|
|
1389
|
+
}
|
|
1270
1390
|
end
|
|
1391
|
+
|
|
1392
|
+
{ path: rel, branch: git[:branch], entries: entries, count: entries.size }
|
|
1271
1393
|
end
|
|
1272
1394
|
|
|
1273
1395
|
def file_read_payload(rel)
|
|
@@ -1431,6 +1553,29 @@ module Tina4
|
|
|
1431
1553
|
JSON.parse(raw)
|
|
1432
1554
|
end
|
|
1433
1555
|
|
|
1556
|
+
# Native MCP JSON-RPC endpoint (POST /__dev/mcp[/message]). Real MCP
|
|
1557
|
+
# clients POST a JSON-RPC 2.0 request; we hand the parsed body straight
|
|
1558
|
+
# to the default server's handle_message and echo the response. A
|
|
1559
|
+
# notification (no id) yields an empty 204, mirroring Python.
|
|
1560
|
+
def mcp_jsonrpc(env)
|
|
1561
|
+
body = read_json_body(env) || {}
|
|
1562
|
+
server = Tina4._default_mcp_server
|
|
1563
|
+
raw = server.handle_message(body)
|
|
1564
|
+
if raw.nil? || raw.empty?
|
|
1565
|
+
[204, { "content-type" => "application/json; charset=utf-8" }, []]
|
|
1566
|
+
else
|
|
1567
|
+
[200, { "content-type" => "application/json; charset=utf-8" }, [raw]]
|
|
1568
|
+
end
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
# SSE handshake (GET /__dev/mcp/sse). Tells the client where to POST
|
|
1572
|
+
# JSON-RPC messages via the standard `endpoint` event, then the client
|
|
1573
|
+
# switches to POST /__dev/mcp/message. Body + content-type match Python.
|
|
1574
|
+
def mcp_sse_handshake
|
|
1575
|
+
body = "event: endpoint\ndata: /__dev/mcp/message\n\n"
|
|
1576
|
+
[200, { "content-type" => "text/event-stream" }, [body]]
|
|
1577
|
+
end
|
|
1578
|
+
|
|
1434
1579
|
def scaffold_templates
|
|
1435
1580
|
# Expose built-in scaffold targets for the dev-admin UI.
|
|
1436
1581
|
{ templates: [
|
data/lib/tina4/mcp.rb
CHANGED
|
@@ -423,8 +423,17 @@ module Tina4
|
|
|
423
423
|
|
|
424
424
|
@_default_mcp_server = nil
|
|
425
425
|
|
|
426
|
+
# The default dev MCP server, mounted at /__dev/mcp. Built lazily on first
|
|
427
|
+
# access and pre-loaded with the built-in dev tools (database_query,
|
|
428
|
+
# file_read, route_list, …) so both the REST shim (/__dev/api/mcp/*) and
|
|
429
|
+
# the JSON-RPC + SSE endpoints (/__dev/mcp[/message], /__dev/mcp/sse)
|
|
430
|
+
# share one fully-populated tool registry.
|
|
426
431
|
def self._default_mcp_server
|
|
427
|
-
@_default_mcp_server ||=
|
|
432
|
+
@_default_mcp_server ||= begin
|
|
433
|
+
server = McpServer.new("/__dev/mcp", name: "Tina4 Dev Tools")
|
|
434
|
+
McpDevTools.register(server)
|
|
435
|
+
server
|
|
436
|
+
end
|
|
428
437
|
end
|
|
429
438
|
|
|
430
439
|
# Register a block as an MCP tool.
|
|
@@ -661,12 +670,13 @@ module Tina4
|
|
|
661
670
|
|
|
662
671
|
# ── Route Tools ───────────────────────────────────
|
|
663
672
|
server.register_tool("route_list", lambda {
|
|
664
|
-
|
|
665
|
-
|
|
673
|
+
# Tina4::Router.routes returns Route objects (attr_readers), not
|
|
674
|
+
# Hashes — use accessors, never [] subscript.
|
|
675
|
+
Tina4::Router.routes.map do |route|
|
|
666
676
|
{
|
|
667
|
-
"method" => route
|
|
668
|
-
"path" => route
|
|
669
|
-
"auth_required" =>
|
|
677
|
+
"method" => route.method.to_s,
|
|
678
|
+
"path" => route.path.to_s,
|
|
679
|
+
"auth_required" => route.auth_required ? true : false
|
|
670
680
|
}
|
|
671
681
|
end
|
|
672
682
|
}, "List all registered routes")
|