tina4ruby 3.13.150 → 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 +49 -0
- data/lib/tina4/ai.rb +1 -0
- data/lib/tina4/auth.rb +3 -3
- data/lib/tina4/cli.rb +218 -54
- data/lib/tina4/crud.rb +7 -7
- data/lib/tina4/drivers/mongodb_driver.rb +48 -13
- data/lib/tina4/drivers/mssql_driver.rb +6 -1
- data/lib/tina4/drivers/postgres_driver.rb +60 -10
- data/lib/tina4/form_parser.rb +38 -4
- data/lib/tina4/frond.rb +151 -20
- data/lib/tina4/mcp.rb +36 -12
- data/lib/tina4/middleware.rb +10 -1
- data/lib/tina4/plan.rb +2 -2
- data/lib/tina4/point.rb +1 -1
- data/lib/tina4/project_index.rb +1 -1
- data/lib/tina4/rack_app.rb +4 -1
- data/lib/tina4/realtime/s3_client.rb +240 -0
- data/lib/tina4/realtime/s3_storage.rb +15 -28
- data/lib/tina4/realtime/storage.rb +4 -5
- data/lib/tina4/realtime.rb +2 -1
- data/lib/tina4/request.rb +41 -3
- data/lib/tina4/response_cache.rb +9 -1
- data/lib/tina4/router.rb +1 -1
- data/lib/tina4/sql_translator.rb +5 -3
- data/lib/tina4/template.rb +32 -17
- data/lib/tina4/templates/crud/form.twig +5 -5
- data/lib/tina4/templates/crud/table.twig +2 -2
- data/lib/tina4/templates/errors/302.twig +4 -3
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +1 -1
- metadata +3 -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,55 @@ 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
|
+
|
|
42
|
+
## 3.13.151 - 2026-10-15
|
|
43
|
+
|
|
44
|
+
### Security
|
|
45
|
+
- Cross-Origin-Opener-Policy joins the default security-header set (`TINA4_COOP`,
|
|
46
|
+
default `same-origin-allow-popups`). It pairs with a COEP header to unlock
|
|
47
|
+
`SharedArrayBuffer` and WASM threads in the browser.
|
|
48
|
+
- S3 storage is a zero-dependency SigV4 client (no aws-sdk-s3). It now fails loud on
|
|
49
|
+
a missing credential, retries 429/5xx and dropped connections with capped backoff,
|
|
50
|
+
honours `Retry-After`, and keeps the endpoint path prefix.
|
|
51
|
+
- MongoDB keeps bound values out of the parsed SQL text.
|
|
52
|
+
- CRUD attributes are de-raw'd, the 302 redirect is guarded, CodeQL core findings
|
|
53
|
+
are cleared, and uploaded files are created owner-only (0600).
|
|
54
|
+
|
|
55
|
+
### Docs
|
|
56
|
+
- The README built-in feature count is corrected (55 -> 140).
|
|
57
|
+
|
|
9
58
|
## 3.13.150 — 2026-10-11
|
|
10
59
|
|
|
11
60
|
### Dev admin
|
data/lib/tina4/ai.rb
CHANGED
data/lib/tina4/auth.rb
CHANGED
|
@@ -466,7 +466,7 @@ module Tina4
|
|
|
466
466
|
# is now honoured here too (explicit > env > HS256).
|
|
467
467
|
def authenticate_request(headers, secret: nil, algorithm: nil)
|
|
468
468
|
auth_header = headers["HTTP_AUTHORIZATION"] || headers["Authorization"] || ""
|
|
469
|
-
return nil unless auth_header =~ /\ABearer\s
|
|
469
|
+
return nil unless auth_header =~ /\ABearer\s++(.+)\z/i
|
|
470
470
|
|
|
471
471
|
token = Regexp.last_match(1)
|
|
472
472
|
|
|
@@ -518,7 +518,7 @@ module Tina4
|
|
|
518
518
|
def bearer_auth
|
|
519
519
|
lambda do |env|
|
|
520
520
|
auth_header = env["HTTP_AUTHORIZATION"] || ""
|
|
521
|
-
return false unless auth_header =~ /\ABearer\s
|
|
521
|
+
return false unless auth_header =~ /\ABearer\s++(.+)\z/i
|
|
522
522
|
|
|
523
523
|
token = Regexp.last_match(1)
|
|
524
524
|
|
|
@@ -664,7 +664,7 @@ module Tina4
|
|
|
664
664
|
auth_header = env["HTTP_AUTHORIZATION"] || ""
|
|
665
665
|
return true if auth_header.empty?
|
|
666
666
|
|
|
667
|
-
if auth_header =~ /\ABearer\s
|
|
667
|
+
if auth_header =~ /\ABearer\s++(.+)\z/i
|
|
668
668
|
valid_token(Regexp.last_match(1))
|
|
669
669
|
else
|
|
670
670
|
false
|
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
|
|
@@ -457,7 +609,7 @@ module Tina4
|
|
|
457
609
|
# per-instance to_snake_case when only the class name is known.
|
|
458
610
|
def self.to_snake_case_static(name)
|
|
459
611
|
s = name.to_s
|
|
460
|
-
s.gsub(/([A-Z]
|
|
612
|
+
s.gsub(/([A-Z])(?=[A-Z][a-z])/, '\1_')
|
|
461
613
|
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
462
614
|
.downcase
|
|
463
615
|
end
|
|
@@ -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
|
data/lib/tina4/crud.rb
CHANGED
|
@@ -112,7 +112,7 @@ module Tina4
|
|
|
112
112
|
verb = method.to_s.upcase
|
|
113
113
|
Tina4::Template.render("crud/form.twig", {
|
|
114
114
|
"wrap" => true,
|
|
115
|
-
"
|
|
115
|
+
"form_id" => "",
|
|
116
116
|
"action" => h(action),
|
|
117
117
|
"form_method" => h(verb),
|
|
118
118
|
"method_override" => (%w[PUT PATCH DELETE].include?(verb) ? verb : nil),
|
|
@@ -204,14 +204,14 @@ module Tina4
|
|
|
204
204
|
"value" => "",
|
|
205
205
|
"placeholder" => h("Enter #{label.downcase}"),
|
|
206
206
|
"type" => "text",
|
|
207
|
-
"
|
|
207
|
+
"required" => false,
|
|
208
208
|
"input" => true
|
|
209
209
|
}
|
|
210
210
|
end
|
|
211
211
|
|
|
212
212
|
Tina4::Template.render("crud/form.twig", {
|
|
213
213
|
"wrap" => true,
|
|
214
|
-
"
|
|
214
|
+
"form_id" => "form-#{mode}",
|
|
215
215
|
"action" => "",
|
|
216
216
|
"form_method" => "POST",
|
|
217
217
|
"method_override" => nil,
|
|
@@ -263,7 +263,7 @@ module Tina4
|
|
|
263
263
|
"readonly" => !editable,
|
|
264
264
|
"inline_script" => inline_script,
|
|
265
265
|
"table_name" => h(table_name),
|
|
266
|
-
"
|
|
266
|
+
"table_id" => (table_id ? table_id.to_s : "")
|
|
267
267
|
}
|
|
268
268
|
end
|
|
269
269
|
|
|
@@ -309,7 +309,7 @@ module Tina4
|
|
|
309
309
|
name = field[:name].to_s
|
|
310
310
|
label = field[:label] || name.capitalize
|
|
311
311
|
value = field[:value]
|
|
312
|
-
required = field[:required] ?
|
|
312
|
+
required = field[:required] ? true : false
|
|
313
313
|
|
|
314
314
|
base = {
|
|
315
315
|
"id" => h(name),
|
|
@@ -317,14 +317,14 @@ module Tina4
|
|
|
317
317
|
"label" => h(label.to_s),
|
|
318
318
|
"value" => h(value.to_s),
|
|
319
319
|
"placeholder" => "",
|
|
320
|
-
"
|
|
320
|
+
"required" => required
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
case (field[:type] || :string).to_sym
|
|
324
324
|
when :text
|
|
325
325
|
base.merge("textarea" => true)
|
|
326
326
|
when :boolean
|
|
327
|
-
base.merge("checkbox" => true, "
|
|
327
|
+
base.merge("checkbox" => true, "checked" => (value ? true : false))
|
|
328
328
|
when :select
|
|
329
329
|
base.merge("select" => true, "options_html" => build_options(field[:options], value))
|
|
330
330
|
when :date
|