tina4ruby 3.10.32 → 3.10.38
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/ai.rb +237 -211
- data/lib/tina4/cli.rb +5 -13
- data/lib/tina4/dev_admin.rb +281 -2
- data/lib/tina4/frond.rb +15 -3
- data/lib/tina4/metrics.rb +673 -0
- data/lib/tina4/rack_app.rb +57 -2
- data/lib/tina4/request.rb +40 -2
- data/lib/tina4/response.rb +7 -2
- data/lib/tina4/router.rb +5 -1
- data/lib/tina4/version.rb +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a0442856facd7be61939efc9066b395c7578a2b7bd8df31282e02fdec58b95c
|
|
4
|
+
data.tar.gz: 33170414bbc5ed736aab77ce1790b62e90a74a62d51fa3f194b154b69acd34c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 237cd6644e93ee8ed01754a550a65cd5b43d9da8771631e7cff0745bb3e26e2e2892ddbea96e619bf264b527ce207276436c9f8a5c444f0ed1c28486a27ebe31
|
|
7
|
+
data.tar.gz: 71fb5d29f52b2aa9971424fbfddcc236881764a7bf6af0bcf491aa0a587d86d1ffcbd6ecfca5296bdbf3d2ee5d504d40eff2ea3eb4753c31dbb0db5c89326690
|
data/lib/tina4/ai.rb
CHANGED
|
@@ -1,222 +1,113 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
3
5
|
module Tina4
|
|
4
|
-
#
|
|
6
|
+
# Tina4 AI -- Install AI coding assistant context files.
|
|
7
|
+
#
|
|
8
|
+
# Simple menu-driven installer for AI tool context files.
|
|
9
|
+
# The user picks which tools they use, we install the appropriate files.
|
|
5
10
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# files = Tina4::AI.install_ai_context("/path/to/project")
|
|
11
|
+
# selection = Tina4::AI.show_menu(".")
|
|
12
|
+
# Tina4::AI.install_selected(".", selection)
|
|
9
13
|
#
|
|
10
14
|
module AI
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
},
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
config_dir: ".cursor",
|
|
22
|
-
context_file: ".cursorules"
|
|
23
|
-
},
|
|
24
|
-
"copilot" => {
|
|
25
|
-
description: "GitHub Copilot",
|
|
26
|
-
detect: ->(root) { File.exist?(File.join(root, ".github", "copilot-instructions.md")) || Dir.exist?(File.join(root, ".github")) },
|
|
27
|
-
config_dir: ".github",
|
|
28
|
-
context_file: ".github/copilot-instructions.md"
|
|
29
|
-
},
|
|
30
|
-
"windsurf" => {
|
|
31
|
-
description: "Windsurf (Codeium)",
|
|
32
|
-
detect: ->(root) { File.exist?(File.join(root, ".windsurfrules")) },
|
|
33
|
-
config_dir: nil,
|
|
34
|
-
context_file: ".windsurfrules"
|
|
35
|
-
},
|
|
36
|
-
"aider" => {
|
|
37
|
-
description: "Aider",
|
|
38
|
-
detect: ->(root) { File.exist?(File.join(root, ".aider.conf.yml")) || File.exist?(File.join(root, "CONVENTIONS.md")) },
|
|
39
|
-
config_dir: nil,
|
|
40
|
-
context_file: "CONVENTIONS.md"
|
|
41
|
-
},
|
|
42
|
-
"cline" => {
|
|
43
|
-
description: "Cline (VS Code)",
|
|
44
|
-
detect: ->(root) { File.exist?(File.join(root, ".clinerules")) },
|
|
45
|
-
config_dir: nil,
|
|
46
|
-
context_file: ".clinerules"
|
|
47
|
-
},
|
|
48
|
-
"codex" => {
|
|
49
|
-
description: "OpenAI Codex CLI",
|
|
50
|
-
detect: ->(root) { File.exist?(File.join(root, "AGENTS.md")) || File.exist?(File.join(root, "codex.md")) },
|
|
51
|
-
config_dir: nil,
|
|
52
|
-
context_file: "AGENTS.md"
|
|
53
|
-
}
|
|
54
|
-
}.freeze
|
|
15
|
+
# Ordered list of supported AI tools
|
|
16
|
+
AI_TOOLS = [
|
|
17
|
+
{ name: "claude-code", description: "Claude Code", context_file: "CLAUDE.md", config_dir: ".claude" },
|
|
18
|
+
{ name: "cursor", description: "Cursor", context_file: ".cursorules", config_dir: ".cursor" },
|
|
19
|
+
{ name: "copilot", description: "GitHub Copilot", context_file: ".github/copilot-instructions.md", config_dir: ".github" },
|
|
20
|
+
{ name: "windsurf", description: "Windsurf", context_file: ".windsurfrules", config_dir: nil },
|
|
21
|
+
{ name: "aider", description: "Aider", context_file: "CONVENTIONS.md", config_dir: nil },
|
|
22
|
+
{ name: "cline", description: "Cline", context_file: ".clinerules", config_dir: nil },
|
|
23
|
+
{ name: "codex", description: "OpenAI Codex", context_file: "AGENTS.md", config_dir: nil }
|
|
24
|
+
].freeze
|
|
55
25
|
|
|
56
26
|
class << self
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# @param root [String] project root directory (default: current directory)
|
|
60
|
-
# @return [Array<Hash>] each hash has :name, :description, :config_file, :status
|
|
61
|
-
def detect_ai(root = ".")
|
|
62
|
-
root = File.expand_path(root)
|
|
63
|
-
AI_TOOLS.map do |name, tool|
|
|
64
|
-
installed = tool[:detect].call(root)
|
|
65
|
-
{
|
|
66
|
-
name: name,
|
|
67
|
-
description: tool[:description],
|
|
68
|
-
config_file: tool[:context_file],
|
|
69
|
-
status: installed ? "detected" : "not detected"
|
|
70
|
-
}
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Return just the names of detected AI tools.
|
|
27
|
+
# Check if a tool's context file already exists.
|
|
75
28
|
#
|
|
76
29
|
# @param root [String] project root directory
|
|
77
|
-
# @
|
|
78
|
-
|
|
79
|
-
|
|
30
|
+
# @param tool [Hash] tool entry from AI_TOOLS
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
def installed?(root, tool)
|
|
33
|
+
File.exist?(File.join(File.expand_path(root), tool[:context_file]))
|
|
80
34
|
end
|
|
81
35
|
|
|
82
|
-
#
|
|
36
|
+
# Print the numbered menu and return user input.
|
|
83
37
|
#
|
|
84
|
-
# @param root [String] project root directory
|
|
85
|
-
# @
|
|
86
|
-
|
|
87
|
-
# @return [Array<String>] list of files created/updated
|
|
88
|
-
def install_ai_context(root = ".", tools: nil, force: false)
|
|
38
|
+
# @param root [String] project root directory (default: ".")
|
|
39
|
+
# @return [String] user input (comma-separated numbers or "all")
|
|
40
|
+
def show_menu(root = ".")
|
|
89
41
|
root = File.expand_path(root)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
tool_names = tools || detect_ai_names(root)
|
|
93
|
-
context = generate_context
|
|
94
|
-
|
|
95
|
-
tool_names.each do |tool_name|
|
|
96
|
-
tool = AI_TOOLS[tool_name]
|
|
97
|
-
next unless tool
|
|
42
|
+
green = "\e[32m"
|
|
43
|
+
reset = "\e[0m"
|
|
98
44
|
|
|
99
|
-
|
|
100
|
-
|
|
45
|
+
puts "\n Tina4 AI Context Installer\n"
|
|
46
|
+
AI_TOOLS.each_with_index do |tool, i|
|
|
47
|
+
marker = installed?(root, tool) ? " #{green}[installed]#{reset}" : ""
|
|
48
|
+
puts format(" %d. %-20s %s%s", i + 1, tool[:description], tool[:context_file], marker)
|
|
101
49
|
end
|
|
102
50
|
|
|
103
|
-
|
|
104
|
-
|
|
51
|
+
# tina4-ai tools option
|
|
52
|
+
tina4_ai_installed = system("which mdview > /dev/null 2>&1")
|
|
53
|
+
marker = tina4_ai_installed ? " #{green}[installed]#{reset}" : ""
|
|
54
|
+
puts " 8. Install tina4-ai tools (requires Python)#{marker}"
|
|
55
|
+
puts
|
|
105
56
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
# @param root [String] project root directory
|
|
109
|
-
# @param force [Boolean] overwrite existing context files
|
|
110
|
-
# @return [Array<String>] list of files created/updated
|
|
111
|
-
def install_all(root = ".", force: false)
|
|
112
|
-
root = File.expand_path(root)
|
|
113
|
-
created = []
|
|
114
|
-
context = generate_context
|
|
115
|
-
|
|
116
|
-
AI_TOOLS.each do |tool_name, tool|
|
|
117
|
-
files = install_for_tool(root, tool_name, tool, context, force)
|
|
118
|
-
created.concat(files)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
created
|
|
57
|
+
print " Select (comma-separated, or 'all'): "
|
|
58
|
+
$stdin.gets&.strip || ""
|
|
122
59
|
end
|
|
123
60
|
|
|
124
|
-
#
|
|
61
|
+
# Install context files for the selected tools.
|
|
125
62
|
#
|
|
126
63
|
# @param root [String] project root directory
|
|
127
|
-
# @
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
missing = tools.reject { |t| t[:status] == "detected" }
|
|
132
|
-
|
|
133
|
-
lines = ["\nTina4 AI Context Status\n"]
|
|
134
|
-
|
|
135
|
-
if installed.any?
|
|
136
|
-
lines << "Detected AI tools:"
|
|
137
|
-
installed.each { |t| lines << " + #{t[:description]} (#{t[:name]})" }
|
|
138
|
-
else
|
|
139
|
-
lines << "No AI coding tools detected."
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
if missing.any?
|
|
143
|
-
lines << "\nNot detected (install context with `tina4ruby ai --all`):"
|
|
144
|
-
missing.each { |t| lines << " - #{t[:description]} (#{t[:name]})" }
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
lines << ""
|
|
148
|
-
lines.join("\n")
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
private
|
|
152
|
-
|
|
153
|
-
def install_for_tool(root, name, tool, context, force)
|
|
64
|
+
# @param selection [String] comma-separated numbers like "1,2,3" or "all"
|
|
65
|
+
# @return [Array<String>] list of created/updated file paths
|
|
66
|
+
def install_selected(root, selection)
|
|
67
|
+
root_path = File.expand_path(root)
|
|
154
68
|
created = []
|
|
155
|
-
context_path = File.join(root, tool[:context_file])
|
|
156
69
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
70
|
+
if selection.downcase == "all"
|
|
71
|
+
indices = (0...AI_TOOLS.length).to_a
|
|
72
|
+
do_tina4_ai = true
|
|
73
|
+
else
|
|
74
|
+
parts = selection.split(",").map(&:strip).reject(&:empty?)
|
|
75
|
+
indices = []
|
|
76
|
+
do_tina4_ai = false
|
|
77
|
+
parts.each do |p|
|
|
78
|
+
n = Integer(p) rescue next
|
|
79
|
+
if n == 8
|
|
80
|
+
do_tina4_ai = true
|
|
81
|
+
elsif n >= 1 && n <= AI_TOOLS.length
|
|
82
|
+
indices << (n - 1)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
160
85
|
end
|
|
161
86
|
|
|
162
|
-
|
|
163
|
-
FileUtils.mkdir_p(File.dirname(context_path))
|
|
87
|
+
context = generate_context
|
|
164
88
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
created
|
|
89
|
+
indices.each do |idx|
|
|
90
|
+
tool = AI_TOOLS[idx]
|
|
91
|
+
files = install_for_tool(root_path, tool, context)
|
|
92
|
+
created.concat(files)
|
|
169
93
|
end
|
|
170
94
|
|
|
171
|
-
|
|
172
|
-
if name == "claude-code"
|
|
173
|
-
skills = install_claude_skills(root, force)
|
|
174
|
-
created.concat(skills)
|
|
175
|
-
end
|
|
95
|
+
install_tina4_ai if do_tina4_ai
|
|
176
96
|
|
|
177
97
|
created
|
|
178
98
|
end
|
|
179
99
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# Copy .skill files from the framework's skills/ directory to project root
|
|
187
|
-
skills_source = File.join(framework_root, "skills")
|
|
188
|
-
if Dir.exist?(skills_source)
|
|
189
|
-
Dir.glob(File.join(skills_source, "*.skill")).each do |skill_file|
|
|
190
|
-
target = File.join(root, File.basename(skill_file))
|
|
191
|
-
if !File.exist?(target) || force
|
|
192
|
-
FileUtils.cp(skill_file, target)
|
|
193
|
-
created << File.basename(skill_file)
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Copy skill directories from .claude/skills/ in the framework to the project
|
|
199
|
-
framework_skills_dir = File.join(framework_root, ".claude", "skills")
|
|
200
|
-
if Dir.exist?(framework_skills_dir)
|
|
201
|
-
target_skills_dir = File.join(root, ".claude", "skills")
|
|
202
|
-
FileUtils.mkdir_p(target_skills_dir)
|
|
203
|
-
Dir.children(framework_skills_dir).each do |entry|
|
|
204
|
-
skill_dir = File.join(framework_skills_dir, entry)
|
|
205
|
-
next unless File.directory?(skill_dir)
|
|
206
|
-
|
|
207
|
-
target_dir = File.join(target_skills_dir, entry)
|
|
208
|
-
if !Dir.exist?(target_dir) || force
|
|
209
|
-
FileUtils.rm_rf(target_dir) if Dir.exist?(target_dir)
|
|
210
|
-
FileUtils.cp_r(skill_dir, target_dir)
|
|
211
|
-
rel_path = target_dir.sub("#{root}/", "")
|
|
212
|
-
created << rel_path
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
created
|
|
100
|
+
# Install context for all AI tools (non-interactive).
|
|
101
|
+
#
|
|
102
|
+
# @param root [String] project root directory
|
|
103
|
+
# @return [Array<String>] list of created/updated file paths
|
|
104
|
+
def install_all(root = ".")
|
|
105
|
+
install_selected(root, "all")
|
|
218
106
|
end
|
|
219
107
|
|
|
108
|
+
# Generate the universal Tina4 Ruby context document for any AI assistant.
|
|
109
|
+
#
|
|
110
|
+
# @return [String]
|
|
220
111
|
def generate_context
|
|
221
112
|
<<~CONTEXT
|
|
222
113
|
# Tina4 Ruby -- AI Context
|
|
@@ -230,7 +121,7 @@ module Tina4
|
|
|
230
121
|
|
|
231
122
|
```bash
|
|
232
123
|
tina4ruby init . # Scaffold project
|
|
233
|
-
tina4ruby
|
|
124
|
+
tina4ruby serve # Start dev server on port 7147
|
|
234
125
|
tina4ruby migrate # Run database migrations
|
|
235
126
|
tina4ruby test # Run test suite
|
|
236
127
|
tina4ruby routes # List all registered routes
|
|
@@ -239,38 +130,43 @@ module Tina4
|
|
|
239
130
|
## Project Structure
|
|
240
131
|
|
|
241
132
|
```
|
|
242
|
-
|
|
243
|
-
src/routes/ --
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
133
|
+
lib/tina4/ -- Core framework modules
|
|
134
|
+
src/routes/ -- Route handlers (auto-discovered, one per resource)
|
|
135
|
+
src/orm/ -- ORM models (one per file, filename = class name)
|
|
136
|
+
src/templates/ -- Twig/ERB templates (extends base template)
|
|
137
|
+
src/app/ -- Shared helpers and service classes
|
|
138
|
+
src/scss/ -- SCSS files (auto-compiled to public/css/)
|
|
139
|
+
src/public/ -- Static assets served at /
|
|
140
|
+
src/locales/ -- Translation JSON files
|
|
141
|
+
src/seeds/ -- Database seeder scripts
|
|
247
142
|
migrations/ -- SQL migration files (sequential numbered)
|
|
248
|
-
seeds/ -- Database seeder scripts
|
|
249
143
|
spec/ -- RSpec test files
|
|
250
144
|
```
|
|
251
145
|
|
|
252
146
|
## Built-in Features (No External Gems Needed for Core)
|
|
253
147
|
|
|
254
|
-
| Feature | Module |
|
|
255
|
-
|
|
256
|
-
| Routing | Tina4::Router |
|
|
257
|
-
| ORM | Tina4::ORM |
|
|
258
|
-
| Database | Tina4::Database |
|
|
259
|
-
| Templates | Tina4::Template |
|
|
260
|
-
| JWT Auth | Tina4::Auth |
|
|
261
|
-
| REST API Client | Tina4::API |
|
|
262
|
-
| GraphQL | Tina4::GraphQL |
|
|
263
|
-
| WebSocket | Tina4::WebSocket |
|
|
264
|
-
| SOAP/WSDL | Tina4::WSDL |
|
|
265
|
-
| Email (SMTP+IMAP) | Tina4::Messenger |
|
|
266
|
-
| Background Queue | Tina4::Queue |
|
|
267
|
-
| SCSS Compilation | Tina4::ScssCompiler |
|
|
268
|
-
| Migrations | Tina4::Migration |
|
|
269
|
-
| Seeder | Tina4::FakeData |
|
|
270
|
-
| i18n | Tina4::Localization |
|
|
271
|
-
| Swagger/OpenAPI | Tina4::Swagger |
|
|
272
|
-
| Sessions | Tina4::Session |
|
|
273
|
-
| Middleware | Tina4::Middleware |
|
|
148
|
+
| Feature | Module | Require |
|
|
149
|
+
|---------|--------|---------|
|
|
150
|
+
| Routing | Tina4::Router | `require "tina4/router"` |
|
|
151
|
+
| ORM | Tina4::ORM | `require "tina4/orm"` |
|
|
152
|
+
| Database | Tina4::Database | `require "tina4/database"` |
|
|
153
|
+
| Templates | Tina4::Template | `require "tina4/template"` |
|
|
154
|
+
| JWT Auth | Tina4::Auth | `require "tina4/auth"` |
|
|
155
|
+
| REST API Client | Tina4::API | `require "tina4/api"` |
|
|
156
|
+
| GraphQL | Tina4::GraphQL | `require "tina4/graphql"` |
|
|
157
|
+
| WebSocket | Tina4::WebSocket | `require "tina4/websocket"` |
|
|
158
|
+
| SOAP/WSDL | Tina4::WSDL | `require "tina4/wsdl"` |
|
|
159
|
+
| Email (SMTP+IMAP) | Tina4::Messenger | `require "tina4/messenger"` |
|
|
160
|
+
| Background Queue | Tina4::Queue | `require "tina4/queue"` |
|
|
161
|
+
| SCSS Compilation | Tina4::ScssCompiler | `require "tina4/scss_compiler"` |
|
|
162
|
+
| Migrations | Tina4::Migration | `require "tina4/migration"` |
|
|
163
|
+
| Seeder | Tina4::FakeData | `require "tina4/seeder"` |
|
|
164
|
+
| i18n | Tina4::Localization | `require "tina4/localization"` |
|
|
165
|
+
| Swagger/OpenAPI | Tina4::Swagger | `require "tina4/swagger"` |
|
|
166
|
+
| Sessions | Tina4::Session | `require "tina4/session"` |
|
|
167
|
+
| Middleware | Tina4::Middleware | `require "tina4/middleware"` |
|
|
168
|
+
| HTML Builder | Tina4::HtmlElement | `require "tina4/html_element"` |
|
|
169
|
+
| Form Tokens | Tina4::Template | `{{ form_token() }}` in Twig |
|
|
274
170
|
|
|
275
171
|
## Key Conventions
|
|
276
172
|
|
|
@@ -283,6 +179,30 @@ module Tina4
|
|
|
283
179
|
7. **Service pattern** -- complex logic goes in service classes, routes stay thin
|
|
284
180
|
8. **Use built-in features** -- never install gems for things Tina4 already provides
|
|
285
181
|
|
|
182
|
+
## AI Workflow -- Available Skills
|
|
183
|
+
|
|
184
|
+
When using an AI coding assistant with Tina4, these skills are available:
|
|
185
|
+
|
|
186
|
+
| Skill | Description |
|
|
187
|
+
|-------|-------------|
|
|
188
|
+
| `/tina4-route` | Create a new route with proper decorators and auth |
|
|
189
|
+
| `/tina4-orm` | Create an ORM model with migration |
|
|
190
|
+
| `/tina4-crud` | Generate complete CRUD (migration, ORM, routes, template, tests) |
|
|
191
|
+
| `/tina4-auth` | Set up JWT authentication with login/register |
|
|
192
|
+
| `/tina4-api` | Create an external API integration |
|
|
193
|
+
| `/tina4-queue` | Set up background job processing |
|
|
194
|
+
| `/tina4-template` | Create a server-rendered template page |
|
|
195
|
+
| `/tina4-graphql` | Set up a GraphQL endpoint |
|
|
196
|
+
| `/tina4-websocket` | Set up WebSocket communication |
|
|
197
|
+
| `/tina4-wsdl` | Create a SOAP/WSDL service |
|
|
198
|
+
| `/tina4-messenger` | Set up email send/receive |
|
|
199
|
+
| `/tina4-test` | Write tests for a feature |
|
|
200
|
+
| `/tina4-migration` | Create a database migration |
|
|
201
|
+
| `/tina4-seed` | Generate fake data for development |
|
|
202
|
+
| `/tina4-i18n` | Set up internationalization |
|
|
203
|
+
| `/tina4-scss` | Set up SCSS stylesheets |
|
|
204
|
+
| `/tina4-frontend` | Set up a frontend framework |
|
|
205
|
+
|
|
286
206
|
## Common Patterns
|
|
287
207
|
|
|
288
208
|
### Route
|
|
@@ -305,8 +225,114 @@ module Tina4
|
|
|
305
225
|
numeric_field :price
|
|
306
226
|
end
|
|
307
227
|
```
|
|
228
|
+
|
|
229
|
+
### Template
|
|
230
|
+
```twig
|
|
231
|
+
{% extends "base.twig" %}
|
|
232
|
+
{% block content %}
|
|
233
|
+
<div class="container">
|
|
234
|
+
<h1>{{ title }}</h1>
|
|
235
|
+
{% for item in items %}
|
|
236
|
+
<p>{{ item.name }}</p>
|
|
237
|
+
{% endfor %}
|
|
238
|
+
</div>
|
|
239
|
+
{% endblock %}
|
|
240
|
+
```
|
|
308
241
|
CONTEXT
|
|
309
242
|
end
|
|
243
|
+
|
|
244
|
+
# Install context file for a single tool.
|
|
245
|
+
#
|
|
246
|
+
# @param root [String] absolute project root path
|
|
247
|
+
# @param tool [Hash] tool entry from AI_TOOLS
|
|
248
|
+
# @param context [String] generated context content
|
|
249
|
+
# @return [Array<String>] list of created/updated relative file paths
|
|
250
|
+
def install_for_tool(root, tool, context)
|
|
251
|
+
created = []
|
|
252
|
+
context_path = File.join(root, tool[:context_file])
|
|
253
|
+
|
|
254
|
+
# Create directories
|
|
255
|
+
if tool[:config_dir]
|
|
256
|
+
FileUtils.mkdir_p(File.join(root, tool[:config_dir]))
|
|
257
|
+
end
|
|
258
|
+
FileUtils.mkdir_p(File.dirname(context_path))
|
|
259
|
+
|
|
260
|
+
# Always overwrite -- user chose to install
|
|
261
|
+
action = File.exist?(context_path) ? "Updated" : "Installed"
|
|
262
|
+
File.write(context_path, context)
|
|
263
|
+
rel = context_path.sub("#{root}/", "")
|
|
264
|
+
created << rel
|
|
265
|
+
puts " \e[32m✓\e[0m #{action} #{rel}"
|
|
266
|
+
|
|
267
|
+
# Claude-specific extras
|
|
268
|
+
if tool[:name] == "claude-code"
|
|
269
|
+
skills = install_claude_skills(root)
|
|
270
|
+
created.concat(skills)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
created
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Copy Claude Code skill files from the framework's templates.
|
|
277
|
+
#
|
|
278
|
+
# @param root [String] absolute project root path
|
|
279
|
+
# @return [Array<String>] list of created/updated relative file paths
|
|
280
|
+
def install_claude_skills(root)
|
|
281
|
+
created = []
|
|
282
|
+
|
|
283
|
+
# Determine the framework root (where lib/tina4/ lives)
|
|
284
|
+
framework_root = File.expand_path("../../..", __FILE__)
|
|
285
|
+
|
|
286
|
+
# Copy skill directories from .claude/skills/ in the framework to the project
|
|
287
|
+
framework_skills_dir = File.join(framework_root, ".claude", "skills")
|
|
288
|
+
if Dir.exist?(framework_skills_dir)
|
|
289
|
+
target_skills_dir = File.join(root, ".claude", "skills")
|
|
290
|
+
FileUtils.mkdir_p(target_skills_dir)
|
|
291
|
+
Dir.children(framework_skills_dir).each do |entry|
|
|
292
|
+
skill_dir = File.join(framework_skills_dir, entry)
|
|
293
|
+
next unless File.directory?(skill_dir)
|
|
294
|
+
|
|
295
|
+
target_dir = File.join(target_skills_dir, entry)
|
|
296
|
+
FileUtils.rm_rf(target_dir) if Dir.exist?(target_dir)
|
|
297
|
+
FileUtils.cp_r(skill_dir, target_dir)
|
|
298
|
+
rel = target_dir.sub("#{root}/", "")
|
|
299
|
+
created << rel
|
|
300
|
+
puts " \e[32m✓\e[0m Updated #{rel}"
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Copy claude-commands if they exist
|
|
305
|
+
commands_source = File.join(framework_root, "templates", "ai", "claude-commands")
|
|
306
|
+
if Dir.exist?(commands_source)
|
|
307
|
+
commands_dir = File.join(root, ".claude", "commands")
|
|
308
|
+
FileUtils.mkdir_p(commands_dir)
|
|
309
|
+
Dir.glob(File.join(commands_source, "*.md")).each do |skill_file|
|
|
310
|
+
target = File.join(commands_dir, File.basename(skill_file))
|
|
311
|
+
FileUtils.cp(skill_file, target)
|
|
312
|
+
rel = target.sub("#{root}/", "")
|
|
313
|
+
created << rel
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
created
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Install tina4-ai package (provides mdview for markdown viewing).
|
|
321
|
+
def install_tina4_ai
|
|
322
|
+
puts " Installing tina4-ai tools..."
|
|
323
|
+
%w[pip3 pip].each do |cmd|
|
|
324
|
+
next unless system("which #{cmd} > /dev/null 2>&1")
|
|
325
|
+
|
|
326
|
+
result = `#{cmd} install --upgrade tina4-ai 2>&1`
|
|
327
|
+
if $?.success?
|
|
328
|
+
puts " \e[32m✓\e[0m Installed tina4-ai (mdview)"
|
|
329
|
+
return
|
|
330
|
+
else
|
|
331
|
+
puts " \e[33m!\e[0m #{cmd} failed: #{result.strip[0..100]}"
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
puts " \e[33m!\e[0m Python/pip not available -- skip tina4-ai"
|
|
335
|
+
end
|
|
310
336
|
end
|
|
311
337
|
end
|
|
312
338
|
end
|
data/lib/tina4/cli.rb
CHANGED
|
@@ -361,30 +361,22 @@ module Tina4
|
|
|
361
361
|
# ── ai ────────────────────────────────────────────────────────────────
|
|
362
362
|
|
|
363
363
|
def cmd_ai(argv)
|
|
364
|
-
options = { all: false
|
|
364
|
+
options = { all: false }
|
|
365
365
|
parser = OptionParser.new do |opts|
|
|
366
366
|
opts.banner = "Usage: tina4ruby ai [options]"
|
|
367
|
-
opts.on("--all", "Install context for ALL AI tools (
|
|
368
|
-
opts.on("--force", "Overwrite existing context files") { options[:force] = true }
|
|
367
|
+
opts.on("--all", "Install context for ALL AI tools (non-interactive)") { options[:all] = true }
|
|
369
368
|
end
|
|
370
369
|
parser.parse!(argv)
|
|
371
370
|
|
|
372
371
|
require_relative "ai"
|
|
373
372
|
|
|
374
373
|
root_dir = Dir.pwd
|
|
375
|
-
puts Tina4::AI.status_report(root_dir)
|
|
376
374
|
|
|
377
375
|
if options[:all]
|
|
378
|
-
|
|
376
|
+
Tina4::AI.install_all(root_dir)
|
|
379
377
|
else
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if created.any?
|
|
384
|
-
puts "Created/updated context files:"
|
|
385
|
-
created.each { |f| puts " #{f}" }
|
|
386
|
-
else
|
|
387
|
-
puts "No context files were created (files already exist; use --force to overwrite)."
|
|
378
|
+
selection = Tina4::AI.show_menu(root_dir)
|
|
379
|
+
Tina4::AI.install_selected(root_dir, selection) unless selection.empty?
|
|
388
380
|
end
|
|
389
381
|
end
|
|
390
382
|
|