tina4ruby 3.11.32 → 3.11.35
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/database.rb +4 -31
- data/lib/tina4/dev_admin.rb +1 -463
- data/lib/tina4/drivers/firebird_driver.rb +71 -13
- data/lib/tina4/frond.rb +0 -62
- data/lib/tina4/mcp.rb +0 -190
- data/lib/tina4/orm.rb +66 -220
- data/lib/tina4/public/js/tina4-dev-admin.js +238 -1086
- data/lib/tina4/public/js/tina4-dev-admin.min.js +209 -1142
- data/lib/tina4/rack_app.rb +1 -46
- data/lib/tina4/response.rb +0 -3
- data/lib/tina4/shutdown.rb +0 -10
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +0 -14
- metadata +2 -6
- data/lib/tina4/background.rb +0 -81
- data/lib/tina4/docs.rb +0 -636
- data/lib/tina4/plan.rb +0 -471
- data/lib/tina4/project_index.rb +0 -366
data/lib/tina4/project_index.rb
DELETED
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Tina4::ProjectIndex — lightweight "where is what" map of a project.
|
|
4
|
-
# Ported from tina4_python/dev_admin/project_index.py.
|
|
5
|
-
#
|
|
6
|
-
# Storage: .tina4/project_index.json at the project root. Incremental,
|
|
7
|
-
# mtime-based refresh on every read. Per-language extractors (Ruby,
|
|
8
|
-
# ERB/Twig, SQL, JS/TS, Markdown) produce symbol/route/import summaries
|
|
9
|
-
# used by index_search and index_overview.
|
|
10
|
-
|
|
11
|
-
require "json"
|
|
12
|
-
require "digest"
|
|
13
|
-
require "fileutils"
|
|
14
|
-
|
|
15
|
-
module Tina4
|
|
16
|
-
module ProjectIndex
|
|
17
|
-
INDEX_DIRNAME = ".tina4"
|
|
18
|
-
INDEX_FILENAME = "project_index.json"
|
|
19
|
-
|
|
20
|
-
SKIP_DIRS = %w[
|
|
21
|
-
.git .hg .svn node_modules __pycache__ .venv venv .mypy_cache
|
|
22
|
-
.ruff_cache .pytest_cache dist build .tina4 logs .idea .vscode
|
|
23
|
-
vendor coverage tmp .bundle
|
|
24
|
-
].freeze
|
|
25
|
-
|
|
26
|
-
INDEX_EXT = %w[
|
|
27
|
-
.rb .erb .twig .html .sql .scss .css .js .ts .mjs .md .json .yml
|
|
28
|
-
.yaml .toml .env .rake
|
|
29
|
-
].freeze
|
|
30
|
-
|
|
31
|
-
MAX_FILE_BYTES = 256 * 1024
|
|
32
|
-
|
|
33
|
-
ROUTE_METHODS = %w[get post put patch delete any any_method secure_get secure_post].freeze
|
|
34
|
-
|
|
35
|
-
class << self
|
|
36
|
-
def project_root
|
|
37
|
-
File.expand_path(Dir.pwd)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def index_path
|
|
41
|
-
dir = File.join(project_root, INDEX_DIRNAME)
|
|
42
|
-
FileUtils.mkdir_p(dir)
|
|
43
|
-
File.join(dir, INDEX_FILENAME)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# ── Extractors ────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
def extract_ruby(text)
|
|
49
|
-
out = { "symbols" => [], "imports" => [], "routes" => [], "docstring" => "" }
|
|
50
|
-
# Pick up first non-blank comment block as pseudo-docstring.
|
|
51
|
-
first_comment = nil
|
|
52
|
-
text.each_line do |ln|
|
|
53
|
-
s = ln.strip
|
|
54
|
-
next if s.empty? || s.start_with?("#!") || s == "# frozen_string_literal: true"
|
|
55
|
-
if s.start_with?("#")
|
|
56
|
-
first_comment = s.sub(/\A#\s*/, "")[0, 200]
|
|
57
|
-
break
|
|
58
|
-
else
|
|
59
|
-
break
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
out["docstring"] = first_comment if first_comment
|
|
63
|
-
|
|
64
|
-
text.scan(/^\s*(?:class|module)\s+([A-Z][\w:]*)/) { |m| out["symbols"] << m[0] }
|
|
65
|
-
text.scan(/^\s*def\s+(self\.)?([A-Za-z_][\w!?=]*)/) { |m| out["symbols"] << m[1] }
|
|
66
|
-
text.scan(/^\s*require(?:_relative)?\s+['"]([^'"]+)['"]/) { |m| out["imports"] << m[0] }
|
|
67
|
-
# Tina4.get "/path" OR Tina4::Router.get("/path") OR get "/path" do
|
|
68
|
-
route_re = /(?:Tina4(?:::Router)?\.|^\s*)(get|post|put|patch|delete|any|any_method|secure_get|secure_post)\s*\(?\s*['"]([^'"]+)['"]/
|
|
69
|
-
text.scan(route_re) do |meth, path|
|
|
70
|
-
next unless ROUTE_METHODS.include?(meth)
|
|
71
|
-
out["routes"] << { "method" => meth.upcase, "path" => path, "handler" => "" }
|
|
72
|
-
end
|
|
73
|
-
out["symbols"].uniq!
|
|
74
|
-
out["imports"].uniq!
|
|
75
|
-
out
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
TWIG_EXTENDS = /\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}/.freeze
|
|
79
|
-
TWIG_BLOCK = /\{%\s*block\s+([A-Za-z_][\w-]*)/.freeze
|
|
80
|
-
TWIG_INCLUDE = /\{%\s*include\s+['"]([^'"]+)['"]/.freeze
|
|
81
|
-
|
|
82
|
-
def extract_twig(text)
|
|
83
|
-
{
|
|
84
|
-
"extends" => text.scan(TWIG_EXTENDS).flatten,
|
|
85
|
-
"blocks" => text.scan(TWIG_BLOCK).flatten.uniq.sort,
|
|
86
|
-
"includes" => text.scan(TWIG_INCLUDE).flatten.uniq.sort
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def extract_erb(text)
|
|
91
|
-
# ERB acts a lot like Twig here — extract partial renders.
|
|
92
|
-
renders = text.scan(/render\s+['"]([^'"]+)['"]/).flatten.uniq
|
|
93
|
-
{ "renders" => renders }
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
SQL_CREATE = /create\s+(?:unique\s+)?(table|index|view|trigger|sequence|procedure|function)\s+(?:if\s+not\s+exists\s+)?([A-Za-z_][\w.]*)/i.freeze
|
|
97
|
-
SQL_ALTER = /alter\s+(table|index|view)\s+([A-Za-z_][\w.]*)/i.freeze
|
|
98
|
-
|
|
99
|
-
def extract_sql(text)
|
|
100
|
-
out = { "creates" => [], "alters" => [] }
|
|
101
|
-
text.scan(SQL_CREATE) { |kind, name| out["creates"] << "#{kind.upcase} #{name}" }
|
|
102
|
-
text.scan(SQL_ALTER) { |kind, name| out["alters"] << "#{kind.upcase} #{name}" }
|
|
103
|
-
out
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
JS_EXPORT = /^\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/.freeze
|
|
107
|
-
JS_IMPORT = /^\s*import\s+[^'"]+?['"]([^'"]+)['"]/.freeze
|
|
108
|
-
|
|
109
|
-
def extract_js_ts(text)
|
|
110
|
-
{
|
|
111
|
-
"exports" => text.scan(JS_EXPORT).flatten.uniq.sort,
|
|
112
|
-
"imports" => text.scan(JS_IMPORT).flatten.uniq.sort
|
|
113
|
-
}
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
MD_H1 = /^#\s+(.+)$/.freeze
|
|
117
|
-
MD_H2 = /^##\s+(.+)$/.freeze
|
|
118
|
-
|
|
119
|
-
def extract_md(text)
|
|
120
|
-
{
|
|
121
|
-
"title" => (text[MD_H1, 1] || "").strip,
|
|
122
|
-
"sections" => text.scan(MD_H2).flatten.first(30)
|
|
123
|
-
}
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def extract_generic(text)
|
|
127
|
-
text.each_line do |line|
|
|
128
|
-
s = line.strip
|
|
129
|
-
next if s.empty? || s.start_with?("<!--")
|
|
130
|
-
return { "first_line" => s[0, 200] }
|
|
131
|
-
end
|
|
132
|
-
{}
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
EXTRACTORS = {
|
|
136
|
-
".rb" => :extract_ruby,
|
|
137
|
-
".rake" => :extract_ruby,
|
|
138
|
-
".erb" => :extract_erb,
|
|
139
|
-
".twig" => :extract_twig,
|
|
140
|
-
".html" => :extract_twig,
|
|
141
|
-
".sql" => :extract_sql,
|
|
142
|
-
".js" => :extract_js_ts,
|
|
143
|
-
".mjs" => :extract_js_ts,
|
|
144
|
-
".ts" => :extract_js_ts,
|
|
145
|
-
".md" => :extract_md
|
|
146
|
-
}.freeze
|
|
147
|
-
|
|
148
|
-
LANGUAGES = {
|
|
149
|
-
".rb" => "ruby", ".rake" => "ruby",
|
|
150
|
-
".erb" => "erb", ".twig" => "twig", ".html" => "html",
|
|
151
|
-
".sql" => "sql", ".scss" => "scss", ".css" => "css",
|
|
152
|
-
".js" => "javascript", ".mjs" => "javascript", ".ts" => "typescript",
|
|
153
|
-
".md" => "markdown", ".json" => "json", ".yml" => "yaml",
|
|
154
|
-
".yaml" => "yaml", ".toml" => "toml", ".env" => "env"
|
|
155
|
-
}.freeze
|
|
156
|
-
|
|
157
|
-
def language_for(path)
|
|
158
|
-
LANGUAGES[File.extname(path)] || "text"
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# ── Index core ───────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
def extract(path)
|
|
164
|
-
stat = File.stat(path)
|
|
165
|
-
rel = path.sub("#{project_root}/", "")
|
|
166
|
-
entry = {
|
|
167
|
-
"path" => rel,
|
|
168
|
-
"size" => stat.size,
|
|
169
|
-
"mtime" => stat.mtime.to_i,
|
|
170
|
-
"language" => language_for(path)
|
|
171
|
-
}
|
|
172
|
-
return entry.merge("skipped" => "too large (#{stat.size} bytes)") if stat.size > MAX_FILE_BYTES
|
|
173
|
-
|
|
174
|
-
text = begin
|
|
175
|
-
File.read(path, encoding: "utf-8", invalid: :replace, undef: :replace)
|
|
176
|
-
rescue StandardError
|
|
177
|
-
return entry
|
|
178
|
-
end
|
|
179
|
-
entry["sha256"] = Digest::SHA256.hexdigest(text)[0, 16]
|
|
180
|
-
ext = File.extname(path)
|
|
181
|
-
extractor = EXTRACTORS[ext]
|
|
182
|
-
begin
|
|
183
|
-
data = extractor ? send(extractor, text) : extract_generic(text)
|
|
184
|
-
entry.merge!(data) if data.is_a?(Hash)
|
|
185
|
-
rescue StandardError => e
|
|
186
|
-
entry["extraction_error"] = e.message[0, 200]
|
|
187
|
-
end
|
|
188
|
-
entry["summary"] = summarise(entry)
|
|
189
|
-
entry
|
|
190
|
-
rescue Errno::ENOENT, Errno::EACCES
|
|
191
|
-
{}
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def summarise(entry)
|
|
195
|
-
return entry["skipped"] if entry["skipped"]
|
|
196
|
-
return entry["docstring"] if entry["docstring"] && !entry["docstring"].to_s.empty?
|
|
197
|
-
return entry["title"] if entry["title"] && !entry["title"].to_s.empty?
|
|
198
|
-
if entry["routes"] && !entry["routes"].empty?
|
|
199
|
-
r = entry["routes"][0]
|
|
200
|
-
extra = entry["routes"].size > 1 ? " (+#{entry["routes"].size - 1} more)" : ""
|
|
201
|
-
return "#{r["method"]} #{r["path"]}#{extra}"
|
|
202
|
-
end
|
|
203
|
-
return "defines " + entry["symbols"].first(4).join(", ") if entry["symbols"] && !entry["symbols"].empty?
|
|
204
|
-
return "exports " + entry["exports"].first(4).join(", ") if entry["exports"] && !entry["exports"].empty?
|
|
205
|
-
return "schema: " + entry["creates"].first(3).join(", ") if entry["creates"] && !entry["creates"].empty?
|
|
206
|
-
return "template, extends #{entry["extends"][0]}" if entry["extends"] && !entry["extends"].empty?
|
|
207
|
-
return entry["first_line"] if entry["first_line"]
|
|
208
|
-
""
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def walk_project
|
|
212
|
-
root = project_root
|
|
213
|
-
found = []
|
|
214
|
-
prefix_len = root.length + 1
|
|
215
|
-
walker = lambda do |dir|
|
|
216
|
-
Dir.each_child(dir) do |name|
|
|
217
|
-
next if name == "." || name == ".."
|
|
218
|
-
# Skip hidden dirs (allow .env as a file below)
|
|
219
|
-
full = File.join(dir, name)
|
|
220
|
-
if File.directory?(full)
|
|
221
|
-
next if SKIP_DIRS.include?(name)
|
|
222
|
-
next if name.start_with?(".")
|
|
223
|
-
walker.call(full)
|
|
224
|
-
elsif File.file?(full)
|
|
225
|
-
if name.start_with?(".")
|
|
226
|
-
next unless name == ".env"
|
|
227
|
-
end
|
|
228
|
-
ext = File.extname(name)
|
|
229
|
-
next unless INDEX_EXT.include?(ext) || name == ".env"
|
|
230
|
-
found << full
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
rescue Errno::EACCES, Errno::ENOENT
|
|
234
|
-
# skip
|
|
235
|
-
end
|
|
236
|
-
walker.call(root)
|
|
237
|
-
found
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def load_raw
|
|
241
|
-
p = index_path
|
|
242
|
-
return { "version" => 1, "files" => {}, "generated_at" => 0 } unless File.exist?(p)
|
|
243
|
-
JSON.parse(File.read(p, encoding: "utf-8"))
|
|
244
|
-
rescue StandardError
|
|
245
|
-
{ "version" => 1, "files" => {}, "generated_at" => 0 }
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def save_raw(data)
|
|
249
|
-
data["generated_at"] = Time.now.to_i
|
|
250
|
-
File.write(index_path, JSON.pretty_generate(data), encoding: "utf-8")
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def refresh
|
|
254
|
-
data = load_raw
|
|
255
|
-
files = data["files"] || {}
|
|
256
|
-
added = 0
|
|
257
|
-
updated = 0
|
|
258
|
-
seen = {}
|
|
259
|
-
root = project_root
|
|
260
|
-
|
|
261
|
-
walk_project.each do |p|
|
|
262
|
-
rel = p.sub("#{root}/", "")
|
|
263
|
-
seen[rel] = true
|
|
264
|
-
begin
|
|
265
|
-
mtime = File.mtime(p).to_i
|
|
266
|
-
rescue Errno::ENOENT
|
|
267
|
-
next
|
|
268
|
-
end
|
|
269
|
-
existing = files[rel]
|
|
270
|
-
if existing && existing["mtime"] == mtime
|
|
271
|
-
next
|
|
272
|
-
end
|
|
273
|
-
files[rel] = extract(p)
|
|
274
|
-
if existing
|
|
275
|
-
updated += 1
|
|
276
|
-
else
|
|
277
|
-
added += 1
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
removed_paths = files.keys - seen.keys
|
|
282
|
-
removed_paths.each { |k| files.delete(k) }
|
|
283
|
-
data["files"] = files
|
|
284
|
-
save_raw(data)
|
|
285
|
-
{
|
|
286
|
-
"added" => added,
|
|
287
|
-
"updated" => updated,
|
|
288
|
-
"removed" => removed_paths.size,
|
|
289
|
-
"total" => files.size,
|
|
290
|
-
"path" => index_path.sub("#{root}/", "")
|
|
291
|
-
}
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def search(query, limit = 20)
|
|
295
|
-
refresh
|
|
296
|
-
data = load_raw
|
|
297
|
-
q = query.to_s.downcase.strip
|
|
298
|
-
return [] if q.empty?
|
|
299
|
-
hits = []
|
|
300
|
-
data["files"].each do |rel, entry|
|
|
301
|
-
score = 0
|
|
302
|
-
score += 10 if rel.downcase.include?(q)
|
|
303
|
-
(entry["symbols"] || []).each do |s|
|
|
304
|
-
sl = s.downcase
|
|
305
|
-
if sl == q
|
|
306
|
-
score += 8
|
|
307
|
-
elsif sl.include?(q)
|
|
308
|
-
score += 4
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
(entry["routes"] || []).each do |r|
|
|
312
|
-
combined = "#{r["path"]} #{r["handler"]}".downcase
|
|
313
|
-
score += 5 if combined.include?(q)
|
|
314
|
-
end
|
|
315
|
-
score += 3 if (entry["summary"] || "").downcase.include?(q)
|
|
316
|
-
(entry["imports"] || []).each { |imp| score += 1 if imp.downcase.include?(q) }
|
|
317
|
-
if score.positive?
|
|
318
|
-
hits << [score, {
|
|
319
|
-
"path" => rel,
|
|
320
|
-
"summary" => entry["summary"] || "",
|
|
321
|
-
"score" => score,
|
|
322
|
-
"language" => entry["language"] || ""
|
|
323
|
-
}]
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
hits.sort_by! { |h| -h[0] }
|
|
327
|
-
hits.first([1, limit].max).map { |_, info| info }
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def file_entry(rel_path)
|
|
331
|
-
refresh
|
|
332
|
-
data = load_raw
|
|
333
|
-
entry = data["files"][rel_path]
|
|
334
|
-
entry || { "error" => "Not in index: #{rel_path}" }
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def overview
|
|
338
|
-
refresh
|
|
339
|
-
data = load_raw
|
|
340
|
-
files = data["files"]
|
|
341
|
-
langs = Hash.new(0)
|
|
342
|
-
route_count = 0
|
|
343
|
-
model_count = 0
|
|
344
|
-
files.each_value do |e|
|
|
345
|
-
langs[e["language"] || "other"] += 1
|
|
346
|
-
route_count += (e["routes"] || []).size
|
|
347
|
-
path = e["path"].to_s
|
|
348
|
-
if (path.start_with?("src/orm/") || path.start_with?("orm/") || path.start_with?("app/models/")) && (e["symbols"] && !e["symbols"].empty?)
|
|
349
|
-
model_count += 1
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
recent = files.values.map do |e|
|
|
353
|
-
{ "path" => e["path"], "summary" => e["summary"] || "", "mtime" => e["mtime"] || 0 }
|
|
354
|
-
end.sort_by { |e| -(e["mtime"] || 0) }.first(10)
|
|
355
|
-
{
|
|
356
|
-
"total_files" => files.size,
|
|
357
|
-
"by_language" => langs,
|
|
358
|
-
"routes_declared" => route_count,
|
|
359
|
-
"orm_models" => model_count,
|
|
360
|
-
"recently_changed" => recent,
|
|
361
|
-
"index_generated_at" => data["generated_at"] || 0
|
|
362
|
-
}
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
end
|
|
366
|
-
end
|