Class: Tina4::CLI
- Inherits:
-
Object
- Object
- Tina4::CLI
- Defined in:
- lib/tina4/cli.rb
Constant Summary collapse
- GENERATORS =
── Command registries — the single source of truth ─────────────────
ONE entry per command/generator drives dispatch (#run / #cmd_generate), the human help (#cmd_help), AND the machine-readable manifest (
commands --json). Add a command in ONE place and it appears in dispatch, help, and discovery — there is no second list to sync. Ruby mirror of the Python master's COMMANDS / GENERATORS registries (tina4_python/cli/init.py).GENERATORS[name] = { handler: :method_symbol, usage: str, summary: str } COMMANDS[name] = { handler: :method_symbol, summary: str, usage?: str, # arg/flag hint for #cmd_help (human only) args?: [str], # positional args for the manifest ("x?" = optional) subcommands?: [str] } # sub-names for the manifest (generate)Handlers are instance-method symbols dispatched via #send: GENERATORS handlers take (name, flags); COMMANDS handlers take (argv).
{ "model" => { handler: :generate_model, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" }, "route" => { handler: :generate_route, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" }, "crud" => { handler: :generate_crud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" }, "migration" => { handler: :generate_migration, usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" }, "middleware" => { handler: :generate_middleware, usage: "<Name>", summary: "Middleware with before/after hooks" }, "test" => { handler: :generate_test, usage: "<name> [--model Name]", summary: "RSpec test file" }, "form" => { handler: :generate_form, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" }, "view" => { handler: :generate_view, usage: '<Name> [--fields "..."]', summary: "List + detail view templates" }, "auth" => { handler: :generate_auth, usage: "", summary: "Login/register routes + User model + templates" }, "service" => { handler: :generate_service, usage: '<Name> [--every 5m | --cron "..."]', summary: "Scheduled ServiceRunner task (src/services/)" }, "queue" => { handler: :generate_queue, usage: "<topic>", summary: "Producer + consumer worker (src/services/)" }, "validator" => { handler: :generate_validator, usage: "<Name>", summary: "Request-body Validator (src/validators/)" }, "seeder" => { handler: :generate_seeder, usage: "<Model>", summary: "FakeData + seed_orm seeder (seeds/)" }, "websocket" => { handler: :generate_websocket, usage: "<path>", summary: "Tina4.websocket handler (src/routes/)" }, "listener" => { handler: :generate_listener, usage: "<event>", summary: "Tina4::Events.on listener (src/listeners/)" }, }.freeze
- QUEUE_SUBCOMMANDS =
Sub-dispatch table for the top-level
queuecommand — the SINGLE source for its subcommands (drives #cmd_queue dispatch AND the manifest's queue.subcommands). Ruby mirror of the Python master's _QUEUE_SUBCOMMANDS. Distinct from thequeueGENERATOR, which SCAFFOLDS a consumer file. { "work" => :queue_work, "stats" => :queue_stats, "retry" => :queue_retry, "clear" => :queue_clear, }.freeze
- COMMANDS =
{ "init" => { handler: :cmd_init, usage: "[NAME]", args: ["name?"], summary: "Initialize a new Tina4 project" }, "start" => { handler: :cmd_start, usage: "[options]", summary: "Start the Tina4 web server" }, "serve" => { handler: :cmd_start, summary: "Alias for start" }, "migrate" => { handler: :cmd_migrate, usage: "[--create NAME] [--rollback N]", summary: "Run database migrations" }, "migrate:create" => { handler: :cmd_migrate_create, usage: "<desc>", args: ["description"], summary: "Create a new migration file" }, "migrate:status" => { handler: :cmd_migrate_status, summary: "Show migration status (completed and pending)" }, "migrate:rollback" => { handler: :cmd_migrate_rollback, usage: "[-n N]", summary: "Rollback the last batch of migrations" }, "seed" => { handler: :cmd_seed, usage: "[--clear]", summary: "Run all seed files in seeds/" }, "seed:create" => { handler: :cmd_seed_create, usage: "NAME", args: ["name"], summary: "Create a new seed file" }, "test" => { handler: :cmd_test, summary: "Run inline tests" }, "queue" => { handler: :cmd_queue, usage: "<work|stats|retry|clear> [topic]", subcommands: QUEUE_SUBCOMMANDS.keys, summary: "Run queue workers and manage jobs" }, "build" => { handler: :cmd_build, usage: "[--tag NAME] [--file PATH]", summary: "Build the deployable Docker image" }, "version" => { handler: :cmd_version, summary: "Show Tina4 version" }, "routes" => { handler: :cmd_routes, summary: "List all registered routes" }, "console" => { handler: :cmd_console, summary: "Start an interactive console" }, "generate" => { handler: :cmd_generate, usage: "<what> <name> [options]", subcommands: GENERATORS.keys, summary: "Generate scaffolding (see Generators below)" }, "ai" => { handler: :cmd_ai, usage: "[--all]", summary: "Detect AI tools and install context files" }, "metrics" => { handler: :cmd_metrics, usage: "[--top N] [--json] [--fail-on warn|error] [--path DIR]", summary: "Rank top code-quality offenders" }, "commands" => { handler: :cmd_commands, usage: "[--json]", summary: "List available commands (add --json for machine form)" }, "help" => { handler: :cmd_help, summary: "Show this help message" }, }.freeze
- DELEGATED =
── Delegation to the
tina4client ────────────────────────────────doctor,setupanddeployare owned by the Rusttina4client, not by any framework.doctorprobes ALL FOUR runtimes plus package managers, ports and global AI-skills currency;setupinstalls language runtimes (Homebrew / Chocolatey, with UAC elevation on Windows) and scaffolds a project from nothing;deploywrites deployment boilerplate baked into the client binary. Cloning any of them into four languages would duplicate hundreds of lines per language for zero new capability — and four copies would immediately drift.So the framework CLI DELEGATES: it resolves
tina4on PATH, runs it with the same argv, and exits with the client's exit code. All four frameworks reach the SAME implementation, which is a stronger parity guarantee than four ports.Delegation is ALLOW-LISTED, never blind. The client forwards ITS unknown commands to the framework CLI, so a framework that forwarded its unknowns back would ping-pong an unknown command between two processes forever. This closed set contains only commands the client dispatches natively, so no loop is possible by construction, and a real typo still gets "Unknown command".
There are no handlers here: #run runs
tina4 <name> <args...>and exits with its code. Keep this set closed and identical in all four frameworks. Summaries are the client's own wording, verbatim. Ruby mirror of the Python master's DELEGATED / _delegate_to_client. { "doctor" => { summary: "Check installed languages and tools" }, "setup" => { summary: "Guided, menu-driven setup: install everything + scaffold a ready-to-run project" }, "deploy" => { usage: "<docker|systemd|nginx|cpanel> [--force]", args: ["target"], summary: "Generate deployment scaffolding (Dockerfile, systemd unit, nginx block, cPanel)" }, }.freeze
- CLIENT_BINARY =
"tina4"- DELEGATION_GUARD_ENV =
Internal process marker (same class as the client's own TINA4_SETUP_ELEVATED): set on the child so a client that resolves back to a framework CLI is caught instead of spawning forever. NOT user configuration — deliberately absent from the CLI's known_vars().
"TINA4_CLI_DELEGATED"- EXIT_CLIENT_UNAVAILABLE =
127 is the conventional "command not found" and covers both ways the client can be unreachable (absent from PATH, or the loop guard tripping).
127- EXIT_UNKNOWN_COMMAND =
1- CLIENT_INSTALL_HINT =
<<~HINT.freeze Install it: curl -fsSL https://tina4.com/install.sh | sh Windows: irm https://tina4.com/install.ps1 | iex HINT
- FIELD_TYPE_MAP =
── Field type mapping ──────────────────────────────────────────────
{ "string" => { orm: "string_field", sql: "VARCHAR(255)", default: "''" }, "str" => { orm: "string_field", sql: "VARCHAR(255)", default: "''" }, "int" => { orm: "integer_field", sql: "INTEGER", default: "0" }, "integer" => { orm: "integer_field", sql: "INTEGER", default: "0" }, "float" => { orm: "float_field", sql: "REAL", default: "0" }, "numeric" => { orm: "float_field", sql: "REAL", default: "0" }, "decimal" => { orm: "float_field", sql: "REAL", default: "0" }, "bool" => { orm: "boolean_field", sql: "INTEGER", default: "0" }, "boolean" => { orm: "boolean_field", sql: "INTEGER", default: "0" }, "text" => { orm: "string_field", sql: "TEXT", default: "''" }, "datetime" => { orm: "string_field", sql: "TEXT", default: "NULL" }, "blob" => { orm: "string_field", sql: "BLOB", default: "NULL" }, }.freeze
Class Method Summary collapse
Instance Method Summary collapse
Class Method Details
.start(argv) ⇒ Object
146 147 148 |
# File 'lib/tina4/cli.rb', line 146 def self.start(argv) new.run(argv) end |
Instance Method Details
#run(argv) ⇒ Object
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/tina4/cli.rb', line 150 def run(argv) command = argv.shift || "help" command = "help" if %w[-h --help].include?(command) # Dispatch from the single-source-of-truth COMMANDS registry. The same # registry drives #cmd_help and the `commands --json` manifest, so # dispatch, help, and discovery never drift. spec = COMMANDS[command] if spec send(spec[:handler], argv) elsif DELEGATED.key?(command) exit delegate_to_client(command, argv) else # A genuinely unknown command is an ERROR: exit non-zero so a typo in a # script or CI step fails loudly instead of reporting success. puts "Unknown command: #{command}" cmd_help exit EXIT_UNKNOWN_COMMAND end end |