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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2628729439684b6bb6c01c45a812c8e2e811bc8ad3da8848d4213311d3ecffa2
4
- data.tar.gz: 78842ef5d13e8fd555baec8b1f25321390060d2be22fab7948de599a8c204fa4
3
+ metadata.gz: b90a1ada9ba0ccebf6ac21e8a8c6d37f01f444265b6520c853183a9c905fca98
4
+ data.tar.gz: 322fb1a149eb5181881eb12f0a78d628413ca0d7cd95a5baa8525fe70e9c43c0
5
5
  SHA512:
6
- metadata.gz: c036143a0e8fc8206c6091e93d0a5f9d5e9af59f721afb5e33ebd48a86ba4a124b39fa8540ed8a0403e3474941484ad767152ad91775997289685e0c4c2cee7d
7
- data.tar.gz: b61e7f1708946b5a9b3528a7b9cf1244bb0c9c064bd84661c76075b8f56c9ccf63f17f2b5733ee7514a0412a6c3b9387dab64ad01635c5c65cfccbeed9683072
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
@@ -9,6 +9,7 @@
9
9
  require "fileutils"
10
10
  require "net/http"
11
11
  require "uri"
12
+ require_relative "version" # the context files name Tina4::VERSION; `ai` runs before lib/tina4 loads
12
13
 
13
14
  module Tina4
14
15
  # Tina4 AI -- Install AI coding assistant context files.
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 "..."]', summary: "Form template with inputs matching model fields" },
47
- "view" => { handler: :generate_view, usage: '<Name> [--fields "..."]', summary: "List + detail view templates" },
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?"], summary: "Initialize a new Tina4 project" },
70
- "start" => { handler: :cmd_start, usage: "[options]", summary: "Start the Tina4 web server" },
71
- "serve" => { handler: :cmd_start, summary: "Alias for start" },
72
- "migrate" => { handler: :cmd_migrate, usage: "[--create NAME] [--rollback N]", summary: "Run database migrations" },
73
- "migrate:create" => { handler: :cmd_migrate_create, usage: "<desc>", args: ["description"], summary: "Create a new migration file" },
74
- "migrate:status" => { handler: :cmd_migrate_status, summary: "Show migration status (completed and pending)" },
75
- "migrate:rollback" => { handler: :cmd_migrate_rollback, usage: "[-n N]", summary: "Rollback the last batch of migrations" },
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, summary: "Run inline tests" },
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]", summary: "Lint src/ + app.rb (rubocop, installed dev-only on demand, else ruby -c)" },
82
- "version" => { handler: :cmd_version, summary: "Show Tina4 version" },
83
- "routes" => { handler: :cmd_routes, summary: "List all registered routes" },
84
- "console" => { handler: :cmd_console, summary: "Start an interactive 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, summary: "Show this help message" },
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 %w[-h --help].include?(command)
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
- send(spec[:handler], argv)
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? || argv.first.start_with?("--")
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
- resolution_aware = %w[model route migration middleware form view queue]
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
- generate_model(name, flags, emit_test: false)
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 COMMAND [options]", "", "Commands:"]
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 COMMAND --help' for more information on a command.",
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 = if params.empty?
69
- @connection.exec(sql) # no params: sent exactly as written
70
- else
71
- @connection.exec_params(convert_placeholders(sql), params)
72
- end
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 = if params.empty?
86
- @connection.exec(sql) # no params: sent exactly as written
87
- else
88
- @connection.exec_params(convert_placeholders(sql), params)
89
- end
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
@@ -7,5 +7,5 @@
7
7
 
8
8
 
9
9
  module Tina4
10
- VERSION = "3.13.151"
10
+ VERSION = "3.13.152"
11
11
  end
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.151
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-15 00:00:00.000000000 Z
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3