tina4ruby 3.13.151 → 3.13.152
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 +33 -0
- data/lib/tina4/ai.rb +1 -0
- data/lib/tina4/cli.rb +217 -53
- data/lib/tina4/drivers/postgres_driver.rb +60 -10
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b90a1ada9ba0ccebf6ac21e8a8c6d37f01f444265b6520c853183a9c905fca98
|
|
4
|
+
data.tar.gz: 322fb1a149eb5181881eb12f0a78d628413ca0d7cd95a5baa8525fe70e9c43c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 40ab4de52acfa82a004929d03593662e64517c900dbecbf7a549ecb9b70a84e591ea8f091ef29c3571992ee7d384126757444a925fbc81ddcca40d7ab40f8303
|
|
7
|
+
data.tar.gz: b21e7652b9fa78befd3993cab32bd44798f6886f2267f00f6efeb8d42f869f242c05feb9424033c92c5f0bd246a0af5c0b3077f5693be333b49400c106f64b2d
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,39 @@ number means the same thing everywhere.
|
|
|
6
6
|
**The authoritative release notes for every shipped version live in the documentation:**
|
|
7
7
|
https://tina4.com/ruby/36-releases
|
|
8
8
|
|
|
9
|
+
## 3.13.152 - 2026-10-20
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Database: the PostgreSQL adapter recovers after a dropped connection even
|
|
13
|
+
when the first reconnect attempt itself fails, so a query run over a dead
|
|
14
|
+
link no longer wedges once a reconnect failed (#242). Transaction control
|
|
15
|
+
(`BEGIN` / `COMMIT` / `ROLLBACK`) stays raw and surfaces its own failure, and
|
|
16
|
+
the in-transaction flag is cleared even when a commit or rollback raises.
|
|
17
|
+
Reconnect-after-drop is now at parity across Python, PHP and Node.js too
|
|
18
|
+
(#244).
|
|
19
|
+
- CLI: `--help` / `-h` on any command prints that command's usage and changes
|
|
20
|
+
nothing. Before, most handlers never looked for it and ran for real:
|
|
21
|
+
`generate model -h` wrote a model named `-h`, `generate auth --help` wrote the
|
|
22
|
+
whole auth scaffold, `lint --help` ran `bundle add rubocop`, `migrate:status
|
|
23
|
+
--help` booted the app. Commands that used OptionParser printed its own banner
|
|
24
|
+
instead; every command now prints the same format. Exception: `serve` /
|
|
25
|
+
`start` keep `-h` as `--host` (`-h 127.0.0.1` still works); `--help` is help
|
|
26
|
+
everywhere. `help --help` prints the full reference.
|
|
27
|
+
- CLI: an unknown option is refused with a did-you-mean (`--feilds` →
|
|
28
|
+
`--fields`) instead of being silently dropped, and a value option given no
|
|
29
|
+
value is refused instead of crashing (`serve --port` raised
|
|
30
|
+
`OptionParser::MissingArgument`, `--fields` generated a model with no fields
|
|
31
|
+
and exited 1) or being ignored (`--every`). `--name=value` works as well as
|
|
32
|
+
`--name value`. `test` reads no arguments, so it accepts no options.
|
|
33
|
+
- CLI: `generate` refuses a name that starts with `-`.
|
|
34
|
+
- CLI: `generate crud <Name> --skip-model` works. `generate model` has always
|
|
35
|
+
suggested it, but no generator had the option; it was silently ignored and
|
|
36
|
+
would now be refused.
|
|
37
|
+
- CLI: `ai --all` installs every context file; it crashed with
|
|
38
|
+
`uninitialized constant Tina4::VERSION`. `ai all` works too.
|
|
39
|
+
- CLI: the top-level help reads `Usage: tina4ruby <command> [options]`, the
|
|
40
|
+
same wording as the Python, PHP and Node CLIs (it said `COMMAND`).
|
|
41
|
+
|
|
9
42
|
## 3.13.151 - 2026-10-15
|
|
10
43
|
|
|
11
44
|
### Security
|
data/lib/tina4/ai.rb
CHANGED
data/lib/tina4/cli.rb
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
require "optparse"
|
|
10
|
+
require "did_you_mean" # DidYouMean::SpellChecker - suggestions for a mistyped option
|
|
10
11
|
require "fileutils"
|
|
11
12
|
require "set"
|
|
12
13
|
require "json"
|
|
@@ -27,33 +28,43 @@ module Tina4
|
|
|
27
28
|
# mirror of the Python master's COMMANDS / GENERATORS registries
|
|
28
29
|
# (tina4_python/cli/__init__.py).
|
|
29
30
|
#
|
|
30
|
-
# GENERATORS[name] = { handler: :method_symbol, usage: str, summary: str }
|
|
31
|
+
# GENERATORS[name] = { handler: :method_symbol, flags: {...}, usage: str, summary: str }
|
|
31
32
|
# COMMANDS[name] = { handler: :method_symbol, summary: str,
|
|
33
|
+
# flags: { name => bool }, # options #run accepts; true = takes a value.
|
|
34
|
+
# # nil = not checked (`help` prints the reference whatever follows)
|
|
35
|
+
# short?: { "p" => "port" }, # single-letter aliases (OptionParser ones)
|
|
32
36
|
# usage?: str, # arg/flag hint for #cmd_help (human only)
|
|
33
37
|
# args?: [str], # positional args for the manifest ("x?" = optional)
|
|
34
38
|
# subcommands?: [str] } # sub-names for the manifest (generate)
|
|
35
39
|
#
|
|
36
40
|
# Handlers are instance-method symbols dispatched via #send: GENERATORS
|
|
37
|
-
# handlers take (name, flags); COMMANDS handlers take (argv).
|
|
41
|
+
# handlers take (name, flags); COMMANDS handlers take (argv). `flags` is
|
|
42
|
+
# what #check_options holds argv to before any handler runs; `generate` and
|
|
43
|
+
# `queue` take theirs from GENERATORS / QUEUE_FLAGS instead.
|
|
38
44
|
|
|
39
45
|
GENERATORS = {
|
|
40
|
-
"model" => { handler: :generate_model, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
|
|
41
|
-
"route" => { handler: :generate_route, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
42
|
-
"crud" => { handler: :generate_crud, usage: '<Name> [--fields "..."] [--public] [--no-templates]', summary: "Model + migration + AutoCrud-backed admin page (to_crud) + editable crud/ templates + gate test" },
|
|
43
|
-
"migration" => { handler: :generate_migration, usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
44
|
-
"middleware" => { handler: :generate_middleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
|
|
45
|
-
"test" => { handler: :generate_test, usage: "<name> [--model Name]", summary: "RSpec test file" },
|
|
46
|
-
"form" => { handler: :generate_form, usage: '<Name> [--fields "..."]',
|
|
47
|
-
"view" => { handler: :generate_view, usage: '<Name> [--fields "..."]',
|
|
48
|
-
"auth" => { handler: :generate_auth, usage: "", summary: "Login/register routes + User model + templates" },
|
|
49
|
-
"service" => { handler: :generate_service, usage: '<Name> [--every 5m | --cron "..."]', summary: "Scheduled ServiceRunner task (src/services/)" },
|
|
50
|
-
"queue" => { handler: :generate_queue, usage: "<topic>", summary: "Producer + consumer worker (src/services/)" },
|
|
51
|
-
"validator" => { handler: :generate_validator, usage: "<Name>", summary: "Request-body Validator (src/validators/)" },
|
|
52
|
-
"seeder" => { handler: :generate_seeder, usage: "<Model>", summary: "FakeData + seed_orm seeder (seeds/)" },
|
|
53
|
-
"websocket" => { handler: :generate_websocket, usage: "<path>", summary: "Tina4.websocket handler (src/routes/)" },
|
|
54
|
-
"listener" => { handler: :generate_listener, usage: "<event>", summary: "Tina4::Events.on listener (src/listeners/)" },
|
|
46
|
+
"model" => { handler: :generate_model, flags: { "fields" => true, "table-name" => true, "no-migration" => false }, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
|
|
47
|
+
"route" => { handler: :generate_route, flags: { "model" => true, "public" => false }, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
48
|
+
"crud" => { handler: :generate_crud, flags: { "fields" => true, "table-name" => true, "public" => false, "no-templates" => false, "no-migration" => false, "skip-model" => false }, usage: '<Name> [--fields "..."] [--public] [--no-templates] [--skip-model]', summary: "Model + migration + AutoCrud-backed admin page (to_crud) + editable crud/ templates + gate test" },
|
|
49
|
+
"migration" => { handler: :generate_migration, flags: { "fields" => true }, usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
50
|
+
"middleware" => { handler: :generate_middleware, flags: {}, usage: "<Name>", summary: "Middleware with before/after hooks" },
|
|
51
|
+
"test" => { handler: :generate_test, flags: { "model" => true, "public" => false }, usage: "<name> [--model Name]", summary: "RSpec test file" },
|
|
52
|
+
"form" => { handler: :generate_form, flags: { "fields" => true, "table-name" => true }, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" },
|
|
53
|
+
"view" => { handler: :generate_view, flags: { "fields" => true, "table-name" => true }, usage: '<Name> [--fields "..."]', summary: "List + detail view templates" },
|
|
54
|
+
"auth" => { handler: :generate_auth, flags: {}, usage: "", summary: "Login/register routes + User model + templates" },
|
|
55
|
+
"service" => { handler: :generate_service, flags: { "every" => true, "cron" => true }, usage: '<Name> [--every 5m | --cron "..."]', summary: "Scheduled ServiceRunner task (src/services/)" },
|
|
56
|
+
"queue" => { handler: :generate_queue, flags: {}, usage: "<topic>", summary: "Producer + consumer worker (src/services/)" },
|
|
57
|
+
"validator" => { handler: :generate_validator, flags: {}, usage: "<Name>", summary: "Request-body Validator (src/validators/)" },
|
|
58
|
+
"seeder" => { handler: :generate_seeder, flags: { "table-name" => true }, usage: "<Model>", summary: "FakeData + seed_orm seeder (seeds/)" },
|
|
59
|
+
"websocket" => { handler: :generate_websocket, flags: {}, usage: "<path>", summary: "Tina4.websocket handler (src/routes/)" },
|
|
60
|
+
"listener" => { handler: :generate_listener, flags: {}, usage: "<event>", summary: "Tina4::Events.on listener (src/listeners/)" },
|
|
55
61
|
}.freeze
|
|
56
62
|
|
|
63
|
+
# Generators that emit the ADR-0063 resolution + JSON envelope, and the
|
|
64
|
+
# options every one of them accepts on top of its own.
|
|
65
|
+
RESOLUTION_TARGETS = %w[model route migration middleware form view queue].freeze
|
|
66
|
+
RESOLUTION_FLAGS = { "json" => false, "dry-run" => false }.freeze
|
|
67
|
+
|
|
57
68
|
# Sub-dispatch table for the top-level `queue` command — the SINGLE source
|
|
58
69
|
# for its subcommands (drives #cmd_queue dispatch AND the manifest's
|
|
59
70
|
# queue.subcommands). Ruby mirror of the Python master's _QUEUE_SUBCOMMANDS.
|
|
@@ -65,27 +76,43 @@ module Tina4
|
|
|
65
76
|
"clear" => :queue_clear,
|
|
66
77
|
}.freeze
|
|
67
78
|
|
|
79
|
+
# Options per queue subcommand (name => takes a value), checked by #run.
|
|
80
|
+
QUEUE_FLAGS = {
|
|
81
|
+
"work" => { "once" => false, "poll" => true, "services" => true },
|
|
82
|
+
"stats" => { "json" => false },
|
|
83
|
+
"retry" => {},
|
|
84
|
+
"clear" => {},
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
# `start` / `serve` options, as cmd_start's OptionParser defines them.
|
|
88
|
+
# `-h` is --host here (it predates per-command help), so only `--help` is
|
|
89
|
+
# help for these two.
|
|
90
|
+
SERVE_FLAGS = { "port" => true, "host" => true, "dev" => false, "managed" => false, "production" => false,
|
|
91
|
+
"no-browser" => false, "no-reload" => false, "no-kill" => false }.freeze
|
|
92
|
+
SERVE_SHORT = { "p" => "port", "h" => "host", "d" => "dev" }.freeze
|
|
93
|
+
SERVE_USAGE = "[-p PORT] [-h HOST] [--dev] [--no-browser] [--no-reload] [--no-kill] [--production]"
|
|
94
|
+
|
|
68
95
|
COMMANDS = {
|
|
69
|
-
"init" => { handler: :cmd_init, usage: "[NAME]", args: ["name?"],
|
|
70
|
-
"start" => { handler: :cmd_start, usage:
|
|
71
|
-
"serve" => { handler: :cmd_start,
|
|
72
|
-
"migrate" => { handler: :cmd_migrate, usage: "[--create NAME] [--rollback N]",
|
|
73
|
-
"migrate:create" => { handler: :cmd_migrate_create, usage: "<desc>", args: ["description"],
|
|
74
|
-
"migrate:status" => { handler: :cmd_migrate_status,
|
|
75
|
-
"migrate:rollback" => { handler: :cmd_migrate_rollback, usage: "[-n N]",
|
|
76
|
-
"seed" => { handler: :cmd_seed, usage: "[--clear]", summary: "Run all seed files in seeds/" },
|
|
77
|
-
"seed:create" => { handler: :cmd_seed_create, usage: "NAME", args: ["name"], summary: "Create a new seed file" },
|
|
78
|
-
"test" => { handler: :cmd_test,
|
|
96
|
+
"init" => { handler: :cmd_init, flags: { "template" => true }, usage: "[NAME]", args: ["name?"], summary: "Initialize a new Tina4 project" },
|
|
97
|
+
"start" => { handler: :cmd_start, flags: SERVE_FLAGS, short: SERVE_SHORT, usage: SERVE_USAGE, summary: "Start the Tina4 web server" },
|
|
98
|
+
"serve" => { handler: :cmd_start, flags: SERVE_FLAGS, short: SERVE_SHORT, usage: SERVE_USAGE, summary: "Alias for start" },
|
|
99
|
+
"migrate" => { handler: :cmd_migrate, flags: { "create" => true, "rollback" => true }, usage: "[--create NAME] [--rollback N]", summary: "Run database migrations" },
|
|
100
|
+
"migrate:create" => { handler: :cmd_migrate_create, flags: { "fields" => true, **RESOLUTION_FLAGS }, usage: "<desc>", args: ["description"], summary: "Create a new migration file" },
|
|
101
|
+
"migrate:status" => { handler: :cmd_migrate_status, flags: {}, summary: "Show migration status (completed and pending)" },
|
|
102
|
+
"migrate:rollback" => { handler: :cmd_migrate_rollback, flags: { "steps" => true }, short: { "n" => "steps" }, usage: "[-n N]", summary: "Rollback the last batch of migrations" },
|
|
103
|
+
"seed" => { handler: :cmd_seed, flags: { "clear" => false }, usage: "[--clear]", summary: "Run all seed files in seeds/" },
|
|
104
|
+
"seed:create" => { handler: :cmd_seed_create, flags: {}, usage: "NAME", args: ["name"], summary: "Create a new seed file" },
|
|
105
|
+
"test" => { handler: :cmd_test, flags: {}, summary: "Run inline tests" },
|
|
79
106
|
"queue" => { handler: :cmd_queue, usage: "<work|stats|retry|clear> [topic]", subcommands: QUEUE_SUBCOMMANDS.keys, summary: "Run queue workers and manage jobs" },
|
|
80
|
-
"build" => { handler: :cmd_build, usage: "[--tag NAME] [--file PATH]", summary: "Build the deployable Docker image" },
|
|
81
|
-
"lint" => { handler: :cmd_lint, usage: "[--fix] [--no-install]",
|
|
82
|
-
"version" => { handler: :cmd_version,
|
|
83
|
-
"routes" => { handler: :cmd_routes,
|
|
84
|
-
"console" => { handler: :cmd_console,
|
|
107
|
+
"build" => { handler: :cmd_build, flags: { "tag" => true, "file" => true }, usage: "[--tag NAME] [--file PATH]", summary: "Build the deployable Docker image" },
|
|
108
|
+
"lint" => { handler: :cmd_lint, flags: { "fix" => false, "no-install" => false }, usage: "[--fix] [--no-install]", summary: "Lint src/ + app.rb (rubocop, installed dev-only on demand, else ruby -c)" },
|
|
109
|
+
"version" => { handler: :cmd_version, flags: {}, summary: "Show Tina4 version" },
|
|
110
|
+
"routes" => { handler: :cmd_routes, flags: {}, summary: "List all registered routes" },
|
|
111
|
+
"console" => { handler: :cmd_console, flags: {}, summary: "Start an interactive console" },
|
|
85
112
|
"generate" => { handler: :cmd_generate, usage: "<what> <name> [options]", subcommands: GENERATORS.keys, summary: "Generate scaffolding (see Generators below)" },
|
|
86
|
-
"ai" => { handler: :cmd_ai, usage: "[--all]", summary: "Detect AI tools and install context files" },
|
|
87
|
-
"commands" => { handler: :cmd_commands, usage: "[--json]", summary: "List available commands (add --json for machine form)" },
|
|
88
|
-
"help" => { handler: :cmd_help,
|
|
113
|
+
"ai" => { handler: :cmd_ai, flags: { "all" => false }, usage: "[--all]", summary: "Detect AI tools and install context files" },
|
|
114
|
+
"commands" => { handler: :cmd_commands, flags: { "json" => false }, usage: "[--json]", summary: "List available commands (add --json for machine form)" },
|
|
115
|
+
"help" => { handler: :cmd_help, flags: nil, summary: "Show this help message" },
|
|
89
116
|
}.freeze
|
|
90
117
|
|
|
91
118
|
# ── Delegation to the `tina4` client ────────────────────────────────
|
|
@@ -160,16 +187,34 @@ module Tina4
|
|
|
160
187
|
new.run(argv)
|
|
161
188
|
end
|
|
162
189
|
|
|
190
|
+
HELP_FLAGS = %w[--help -h].freeze
|
|
191
|
+
|
|
163
192
|
def run(argv)
|
|
164
193
|
command = argv.shift || "help"
|
|
165
|
-
command = "help" if
|
|
194
|
+
command = "help" if HELP_FLAGS.include?(command)
|
|
166
195
|
|
|
167
196
|
# Dispatch from the single-source-of-truth COMMANDS registry. The same
|
|
168
197
|
# registry drives #cmd_help and the `commands --json` manifest, so
|
|
169
198
|
# dispatch, help, and discovery never drift.
|
|
170
199
|
spec = COMMANDS[command]
|
|
171
200
|
if spec
|
|
172
|
-
|
|
201
|
+
# `--help` / `-h` anywhere after a command prints that command's usage
|
|
202
|
+
# and touches nothing. Handlers never looked for it, so it ran the
|
|
203
|
+
# command for real: `generate model --help` wrote a model named
|
|
204
|
+
# `--help`, `migrate --help` opened the database. `serve` / `start`
|
|
205
|
+
# keep `-h` as --host. Delegated commands are left alone - the client
|
|
206
|
+
# answers --help itself.
|
|
207
|
+
if command != "help" && argv.any? { |arg| help_flags(command).include?(arg) }
|
|
208
|
+
command_help(command, argv)
|
|
209
|
+
return
|
|
210
|
+
end
|
|
211
|
+
check_options(command, argv)
|
|
212
|
+
begin
|
|
213
|
+
send(spec[:handler], argv)
|
|
214
|
+
rescue OptionParser::ParseError => e
|
|
215
|
+
# e.g. `serve --port abc`: say so, no backtrace.
|
|
216
|
+
option_error(command, "#{e.message}.")
|
|
217
|
+
end
|
|
173
218
|
elsif DELEGATED.key?(command)
|
|
174
219
|
exit delegate_to_client(command, argv)
|
|
175
220
|
else
|
|
@@ -183,6 +228,113 @@ module Tina4
|
|
|
183
228
|
|
|
184
229
|
private
|
|
185
230
|
|
|
231
|
+
# ── Per-command help and option checking ─────────────────────────────
|
|
232
|
+
|
|
233
|
+
def help_flags(command)
|
|
234
|
+
COMMANDS[command][:short]&.key?("h") ? ["--help"] : HELP_FLAGS
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Usage for one command (or one generator), from the same registries.
|
|
238
|
+
def command_help(command, argv)
|
|
239
|
+
if command == "generate"
|
|
240
|
+
kind = argv.find { |arg| !arg.start_with?("-") }&.downcase
|
|
241
|
+
if (gen = GENERATORS[kind])
|
|
242
|
+
puts "Usage: tina4ruby generate #{kind} #{gen[:usage]}".rstrip
|
|
243
|
+
puts "", " #{gen[:summary]}"
|
|
244
|
+
return
|
|
245
|
+
end
|
|
246
|
+
puts "Usage: tina4ruby generate <what> <name> [options]", "", "Generators:"
|
|
247
|
+
pad = GENERATORS.keys.map(&:length).max
|
|
248
|
+
GENERATORS.each { |name, spec| puts " #{name.ljust(pad)} #{spec[:summary]}" }
|
|
249
|
+
return
|
|
250
|
+
end
|
|
251
|
+
spec = COMMANDS[command]
|
|
252
|
+
puts "Usage: tina4ruby #{command} #{spec[:usage]}".rstrip
|
|
253
|
+
puts "", " #{spec[:summary]}"
|
|
254
|
+
puts " Subcommands: #{spec[:subcommands].join(', ')}" if spec[:subcommands]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# The options `command` accepts ({ name => takes_value }), or nil when its
|
|
258
|
+
# arguments are not checked: `help` (flags: nil), or an unknown generator /
|
|
259
|
+
# queue subcommand, which its handler reports.
|
|
260
|
+
def accepted_options(command, argv)
|
|
261
|
+
kind = argv.first.to_s.downcase
|
|
262
|
+
case command
|
|
263
|
+
when "generate"
|
|
264
|
+
gen = GENERATORS[kind] or return nil
|
|
265
|
+
RESOLUTION_TARGETS.include?(kind) ? gen[:flags].merge(RESOLUTION_FLAGS) : gen[:flags]
|
|
266
|
+
when "queue"
|
|
267
|
+
QUEUE_FLAGS[kind]
|
|
268
|
+
else
|
|
269
|
+
COMMANDS[command].fetch(:flags, {})
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def option_label(command, argv)
|
|
274
|
+
%w[generate queue].include?(command) ? "#{command} #{argv.first.downcase}" : command
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def option_error(label, message)
|
|
278
|
+
puts message
|
|
279
|
+
puts %(Run "tina4ruby #{label} --help" for its options.)
|
|
280
|
+
exit 1
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Refuse an option the command doesn't have, or a value option given no
|
|
284
|
+
# value. Both used to slip through: `--feilds` was silently dropped (the
|
|
285
|
+
# model came out without its fields) and a bare `--fields` swallowed the
|
|
286
|
+
# next option or crashed OptionParser.
|
|
287
|
+
def check_options(command, argv)
|
|
288
|
+
accepted = accepted_options(command, argv)
|
|
289
|
+
return if accepted.nil?
|
|
290
|
+
|
|
291
|
+
label = option_label(command, argv)
|
|
292
|
+
short = COMMANDS[command][:short] || {}
|
|
293
|
+
i = 0
|
|
294
|
+
while i < argv.length
|
|
295
|
+
message, consumed = option_problem(argv, i, accepted, short, label)
|
|
296
|
+
if message
|
|
297
|
+
puts message
|
|
298
|
+
exit 1
|
|
299
|
+
end
|
|
300
|
+
i += consumed
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Judge argv[i]: [message, nil] when it is refused, else [nil, args consumed].
|
|
305
|
+
def option_problem(argv, i, accepted, short, label)
|
|
306
|
+
arg = argv[i]
|
|
307
|
+
return [nil, 1] unless arg.start_with?("-") && arg != "-"
|
|
308
|
+
|
|
309
|
+
name, has_value = option_name(arg, short)
|
|
310
|
+
return [unknown_option_message(arg, accepted, label), nil] unless name && accepted.key?(name)
|
|
311
|
+
return [nil, 1] unless accepted[name] && !has_value
|
|
312
|
+
|
|
313
|
+
value = argv[i + 1]
|
|
314
|
+
return [nil, 2] if value && !value.start_with?("-")
|
|
315
|
+
|
|
316
|
+
[%(Option --#{name} for "#{label}" needs a value: --#{name} <value>), nil]
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# [long name, value attached?] for `--name`, `--name=value`, `-p` or `-p7147`;
|
|
320
|
+
# the name is nil for a short option the command doesn't alias.
|
|
321
|
+
def option_name(arg, short)
|
|
322
|
+
if arg.start_with?("--")
|
|
323
|
+
name, eq, = arg[2..].partition("=")
|
|
324
|
+
[name, !eq.empty?]
|
|
325
|
+
else
|
|
326
|
+
[short[arg[1]], arg.length > 2]
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def unknown_option_message(arg, accepted, label)
|
|
331
|
+
bare = arg.sub(/\A-+/, "").partition("=").first
|
|
332
|
+
close = DidYouMean::SpellChecker.new(dictionary: accepted.keys).correct(bare).first
|
|
333
|
+
%(Unknown option #{arg.partition('=').first} for "#{label}".) +
|
|
334
|
+
(close ? " Did you mean --#{close}?" : "") +
|
|
335
|
+
%(\nRun "tina4ruby #{label} --help" for its options.)
|
|
336
|
+
end
|
|
337
|
+
|
|
186
338
|
# Absolute path of the `tina4` client on PATH, or nil if it isn't there.
|
|
187
339
|
#
|
|
188
340
|
# Scans PATH directly rather than shelling out to which/where — one less
|
|
@@ -577,7 +729,11 @@ module Tina4
|
|
|
577
729
|
positional = []
|
|
578
730
|
i = 0
|
|
579
731
|
while i < args.length
|
|
580
|
-
if args[i].start_with?("--")
|
|
732
|
+
if args[i].start_with?("--") && args[i].include?("=")
|
|
733
|
+
key, _, value = args[i][2..].partition("=")
|
|
734
|
+
flags[key] = value
|
|
735
|
+
i += 1
|
|
736
|
+
elsif args[i].start_with?("--")
|
|
581
737
|
key = args[i][2..]
|
|
582
738
|
if boolean_flags.include?(key)
|
|
583
739
|
flags[key] = true
|
|
@@ -1444,6 +1600,7 @@ module Tina4
|
|
|
1444
1600
|
opts.on("--all", "Install context for ALL AI tools (non-interactive)") { options[:all] = true }
|
|
1445
1601
|
end
|
|
1446
1602
|
parser.parse!(argv)
|
|
1603
|
+
options[:all] = true if argv.first&.downcase == "all" # the Python master's bare form
|
|
1447
1604
|
|
|
1448
1605
|
require_relative "ai"
|
|
1449
1606
|
|
|
@@ -1486,13 +1643,28 @@ module Tina4
|
|
|
1486
1643
|
exit 1
|
|
1487
1644
|
end
|
|
1488
1645
|
|
|
1646
|
+
# Dispatch from the GENERATORS registry (single source of truth for the
|
|
1647
|
+
# generate subcommands; also feeds #cmd_help and the manifest).
|
|
1648
|
+
gen_spec = GENERATORS[what]
|
|
1649
|
+
unless gen_spec
|
|
1650
|
+
puts "Unknown generator: #{what}"
|
|
1651
|
+
puts " Available: #{all}"
|
|
1652
|
+
exit 1
|
|
1653
|
+
end
|
|
1654
|
+
|
|
1489
1655
|
# Auth doesn't require a name argument
|
|
1490
1656
|
no_name_generators = %w[auth]
|
|
1491
1657
|
unless no_name_generators.include?(what)
|
|
1492
|
-
if argv.empty?
|
|
1658
|
+
if argv.empty?
|
|
1493
1659
|
puts "Usage: tina4ruby generate #{what} <name> [options]"
|
|
1494
1660
|
exit 1
|
|
1495
1661
|
end
|
|
1662
|
+
# A name that is really a mistyped option (`--fields` with no name
|
|
1663
|
+
# before it, `-x`) would otherwise become a file called `--fields`.
|
|
1664
|
+
if argv.first.start_with?("-")
|
|
1665
|
+
puts "Invalid #{what} name: '#{argv.first}' - names can't start with '-'."
|
|
1666
|
+
exit 1
|
|
1667
|
+
end
|
|
1496
1668
|
end
|
|
1497
1669
|
|
|
1498
1670
|
name = no_name_generators.include?(what) ? "" : argv.shift
|
|
@@ -1501,21 +1673,11 @@ module Tina4
|
|
|
1501
1673
|
json_mode = flags.delete("json") ? true : false
|
|
1502
1674
|
dry_run = flags.delete("dry-run") ? true : false
|
|
1503
1675
|
|
|
1504
|
-
# Dispatch from the GENERATORS registry (single source of truth for the
|
|
1505
|
-
# generate subcommands; also feeds #cmd_help and the manifest).
|
|
1506
|
-
gen_spec = GENERATORS[what]
|
|
1507
|
-
unless gen_spec
|
|
1508
|
-
puts "Unknown generator: #{what}"
|
|
1509
|
-
puts " Available: #{all}"
|
|
1510
|
-
exit 1
|
|
1511
|
-
end
|
|
1512
|
-
|
|
1513
1676
|
# Only the resolution-aware targets emit a resolution + JSON envelope;
|
|
1514
1677
|
# every other generator keeps its existing behaviour so nothing regresses.
|
|
1515
1678
|
# ADR-0063 (3.13.121): form + view joined so their baked `{# tina4:edit ... #}`
|
|
1516
1679
|
# markers surface in resolution.edit_hints[] under the same v1.1 envelope.
|
|
1517
|
-
|
|
1518
|
-
if !resolution_aware.include?(what)
|
|
1680
|
+
unless RESOLUTION_TARGETS.include?(what)
|
|
1519
1681
|
send(gen_spec[:handler], name, flags)
|
|
1520
1682
|
return
|
|
1521
1683
|
end
|
|
@@ -2169,7 +2331,9 @@ module Tina4
|
|
|
2169
2331
|
|
|
2170
2332
|
# 1. Model + migration (emit_test: false — crud emits its own broader gate
|
|
2171
2333
|
# spec at step 4, so the sub-generators stay quiet to avoid double-emit).
|
|
2172
|
-
|
|
2334
|
+
# --skip-model: the model already exists (`generate model` suggests
|
|
2335
|
+
# exactly this), so leave it and its migration alone.
|
|
2336
|
+
generate_model(name, flags, emit_test: false) unless flags["skip-model"]
|
|
2173
2337
|
|
|
2174
2338
|
# 2. Admin page route: registers AutoCrud (secure-by-default; --public
|
|
2175
2339
|
# opens writes) and renders the to_crud admin UI at /admin/<table>.
|
|
@@ -3739,7 +3903,7 @@ module Tina4
|
|
|
3739
3903
|
" #{left.ljust(gap)} #{summary}"
|
|
3740
3904
|
end
|
|
3741
3905
|
|
|
3742
|
-
lines = ["Tina4 Ruby CLI", "", "Usage: tina4ruby
|
|
3906
|
+
lines = ["Tina4 Ruby CLI", "", "Usage: tina4ruby <command> [options]", "", "Commands:"]
|
|
3743
3907
|
lines += command_rows.map { |left, summary| row.call(left, summary) }
|
|
3744
3908
|
lines += ["", "Delegated to the #{CLIENT_BINARY} client (same behaviour in every framework):"]
|
|
3745
3909
|
lines += delegated_rows.map { |left, summary| row.call(left, summary) }
|
|
@@ -3760,7 +3924,7 @@ module Tina4
|
|
|
3760
3924
|
"",
|
|
3761
3925
|
"https://tina4.com",
|
|
3762
3926
|
"",
|
|
3763
|
-
"Run 'tina4ruby
|
|
3927
|
+
"Run 'tina4ruby <command> --help' for more information on a command.",
|
|
3764
3928
|
]
|
|
3765
3929
|
puts lines.join("\n")
|
|
3766
3930
|
end
|
|
@@ -24,6 +24,7 @@ module Tina4
|
|
|
24
24
|
attr_reader :connection
|
|
25
25
|
|
|
26
26
|
def connect(connection_string, username: nil, password: nil)
|
|
27
|
+
@connect_args = [connection_string, username, password]
|
|
27
28
|
begin
|
|
28
29
|
require "pg"
|
|
29
30
|
rescue LoadError
|
|
@@ -64,12 +65,51 @@ module Tina4
|
|
|
64
65
|
@connection&.close
|
|
65
66
|
end
|
|
66
67
|
|
|
68
|
+
# ── Reconnect after a dropped link (parity with the Python master #236/#242; #244) ──
|
|
69
|
+
# The pg gem does not reconnect itself: once the server drops the link
|
|
70
|
+
# (failover, idle reap, pg_terminate_backend) every exec raises until it is
|
|
71
|
+
# rebuilt. exec_with_reconnect heals a dead link BEFORE running, and on a
|
|
72
|
+
# connection error (not a real SQL error) reconnects and retries once when
|
|
73
|
+
# no explicit transaction owns the link. A reconnect that itself fails
|
|
74
|
+
# leaves @connection nil, so the NEXT statement rebuilds and recovers once
|
|
75
|
+
# a reachable server returns.
|
|
76
|
+
def connection_dead?
|
|
77
|
+
@connection.nil? || @connection.status == PG::CONNECTION_BAD
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reconnect
|
|
81
|
+
begin
|
|
82
|
+
@connection&.close
|
|
83
|
+
rescue StandardError
|
|
84
|
+
# already gone
|
|
85
|
+
end
|
|
86
|
+
@connection = nil
|
|
87
|
+
cs, user, pass = @connect_args
|
|
88
|
+
connect(cs, username: user, password: pass)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def exec_with_reconnect
|
|
92
|
+
reconnect if !@in_transaction && connection_dead?
|
|
93
|
+
begin
|
|
94
|
+
yield
|
|
95
|
+
rescue PG::Error => e
|
|
96
|
+
# A real SQL error leaves the link OK; only a dropped link outside an
|
|
97
|
+
# explicit transaction is reconnected and retried once.
|
|
98
|
+
raise e if @in_transaction || !connection_dead?
|
|
99
|
+
|
|
100
|
+
reconnect
|
|
101
|
+
yield
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
67
105
|
def execute_query(sql, params = [])
|
|
68
|
-
result =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
106
|
+
result = exec_with_reconnect do
|
|
107
|
+
if params.empty?
|
|
108
|
+
@connection.exec(sql) # no params: sent exactly as written
|
|
109
|
+
else
|
|
110
|
+
@connection.exec_params(convert_placeholders(sql), params)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
73
113
|
track_affected(result)
|
|
74
114
|
symbolize_result(result)
|
|
75
115
|
end
|
|
@@ -82,11 +122,13 @@ module Tina4
|
|
|
82
122
|
# Clear the cache so last_insert_id falls back to the lastval() probe,
|
|
83
123
|
# which is the correct source for a sequence-backed bare INSERT.
|
|
84
124
|
@last_returning_id = nil if sql.lstrip[0, 6].upcase == "INSERT"
|
|
85
|
-
result =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
result = exec_with_reconnect do
|
|
126
|
+
if params.empty?
|
|
127
|
+
@connection.exec(sql) # no params: sent exactly as written
|
|
128
|
+
else
|
|
129
|
+
@connection.exec_params(convert_placeholders(sql), params)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
90
132
|
track_affected(result)
|
|
91
133
|
result
|
|
92
134
|
end
|
|
@@ -206,14 +248,22 @@ module Tina4
|
|
|
206
248
|
|
|
207
249
|
def begin_transaction
|
|
208
250
|
@connection.exec("BEGIN")
|
|
251
|
+
@in_transaction = true # only after BEGIN actually opened the transaction
|
|
209
252
|
end
|
|
210
253
|
|
|
211
254
|
def commit
|
|
212
255
|
@connection.exec("COMMIT")
|
|
256
|
+
ensure
|
|
257
|
+
# The transaction is over whether COMMIT succeeded or the link died.
|
|
258
|
+
# Clearing in `ensure` stops a COMMIT/ROLLBACK that raises on a dropped
|
|
259
|
+
# link from leaving the flag set and wedging every later reconnect (#244).
|
|
260
|
+
@in_transaction = false
|
|
213
261
|
end
|
|
214
262
|
|
|
215
263
|
def rollback
|
|
216
264
|
@connection.exec("ROLLBACK")
|
|
265
|
+
ensure
|
|
266
|
+
@in_transaction = false
|
|
217
267
|
end
|
|
218
268
|
|
|
219
269
|
# v3.13.14 (#48): to_regclass resolves a (possibly schema-qualified)
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.152
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sqlite3
|