Chapter 30: Tina4 CLI
1. Getting a New Developer Up to Speed
Monday morning. A new developer joins your team. You hand them the repo URL. By 10am they have a running project, a new database model, CRUD routes, a migration, and a deployment to staging. All from the command line. No documentation scavenger hunt. No boilerplate copy-paste.
The Tina4 CLI is a single Rust binary. It manages all four Tina4 frameworks (PHP, Python, Ruby, Node.js). The commands are identical across languages. Learn the CLI for Node.js. You know it for Python.
2. tina4 init -- Project Scaffolding
You saw this in Chapter 1. Now the details.
tina4 init my-projectCreating Tina4 project in ./my-project ...
Detected language: Node.js (package.json)
Created .env
Created .env.example
Created .gitignore
Created src/routes/
Created src/orm/
Created src/migrations/
Created src/seeds/
Created src/templates/
Created src/templates/errors/
Created src/public/
Created src/public/js/
Created src/public/css/
Created src/public/scss/
Created src/public/images/
Created src/public/icons/
Created src/locales/
Created data/
Created logs/
Created secrets/
Created tests/
Project created! Next steps:
cd my-project
npm install
tina4 serveLanguage Detection
The CLI detects the language from existing files:
| File Present | Language |
|---|---|
composer.json | PHP |
pyproject.toml or requirements.txt | Python |
Gemfile | Ruby |
package.json | Node.js |
If no language-specific file exists, the CLI asks:
tina4 init my-projectNo language detected. Which language?
1. PHP
2. Python
3. Ruby
4. Node.js
> 4
Creating Tina4 Node.js project in ./my-project ...Explicit Language Selection
Skip the prompt by specifying the language:
tina4 init nodejs my-projectThis creates a Node.js project with package.json, app.ts, and the full directory structure.
Init into an Existing Directory
Already have a project? Add Tina4 structure:
cd existing-project
tina4 init .The CLI creates only files and directories that do not exist. It never overwrites.
3. tina4 serve -- Dev Server
tina4 serve Tina4 Node.js v3.10.3
HTTP server running at http://0.0.0.0:7148
WebSocket server running at ws://0.0.0.0:7148
Live reload enabled
Press Ctrl+C to stoptina4 serve detects the language and starts the appropriate server. For Node.js, it runs npx tsx app.ts with live reload enabled.
Options
tina4 serve --port 8080 # Custom port
tina4 serve --host 127.0.0.1 # Bind to localhost only
tina4 serve --production # Production mode (no live reload, debug off)Direct Node.js Execution
You can start the server with Node.js directly:
npx tsx app.tsThis is identical to tina4 serve but gives you more control over the Node.js runtime.
4. tina4 generate model -- ORM Scaffolding
The generate model command creates an ORM model file and a matching migration. One command produces both.
tina4 generate model ProductCreated src/orm/Product.ts
Created src/migrations/20260322120000_create_products_table.sqlThe generated model:
import { BaseModel } from "tina4-nodejs/orm";
export class Product extends BaseModel {
static tableName = "products";
static primaryKey = "id";
id!: number;
createdAt!: string;
updatedAt!: string;
}The generated migration:
-- UP
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- DOWN
DROP TABLE IF EXISTS products;The model is ready to use. Add your fields and run migrations.
Adding Fields
Specify fields on the command line:
tina4 generate model Product --fields "name:string,price:float,category:string,inStock:bool"The generated model includes all the fields:
import { BaseModel } from "tina4-nodejs/orm";
export class Product extends BaseModel {
static tableName = "products";
static primaryKey = "id";
id!: number;
name!: string;
price!: number;
category!: string;
inStock!: boolean;
createdAt!: string;
updatedAt!: string;
}And the migration:
-- UP
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '',
price REAL NOT NULL DEFAULT 0,
category TEXT NOT NULL DEFAULT '',
in_stock INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- DOWN
DROP TABLE IF EXISTS products;Field Types
| CLI Type | TypeScript Type | SQLite Column |
|---|---|---|
string | string | TEXT |
int | number | INTEGER |
float | number | REAL |
bool | boolean | INTEGER |
text | string | TEXT |
date | string | TEXT |
Options
| Flag | Description | Example |
|---|---|---|
--fields | Comma-separated field definitions | --fields "name:string,price:float" |
--auto-crud | Enable auto-CRUD on the model | --auto-crud |
--soft-delete | Add soft delete support | --soft-delete |
--no-migration | Skip migration generation | --no-migration |
--with-route | Also generate a CRUD route file | --with-route |
5. tina4 generate route -- CRUD Route Scaffolding
The generate route command creates a complete CRUD route file with all five REST endpoints. It reads the model's properties and builds routes with proper imports and response handling.
tina4 generate route productsCreated src/routes/products.tsThe generated route file:
import { Router } from "tina4-nodejs";
Router.get("/api/products", async (req, res) => {
const page = parseInt(req.query.page ?? "1", 10);
const perPage = parseInt(req.query.per_page ?? "20", 10);
const offset = (page - 1) * perPage;
const products = await Product.findAll({ limit: perPage, offset });
const results = products.map(p => p.toDict());
return res.json({
data: results,
page,
per_page: perPage,
count: results.length
});
});
Router.get("/api/products/{productId:int}", async (req, res) => {
const product = await Product.findById(req.params.productId);
if (product === null) {
return res.status(404).json({ error: "Product not found" });
}
return res.json(product.toDict());
});
Router.post("/api/products", async (req, res) => {
const body = req.body;
const product = new Product();
product.name = body.name ?? "";
product.price = parseFloat(body.price ?? "0");
product.category = body.category ?? "";
product.inStock = Boolean(body.inStock ?? false);
await product.save();
return res.status(201).json(product.toDict());
});
Router.put("/api/products/{productId:int}", async (req, res) => {
const product = await Product.findById(req.params.productId);
if (product === null) {
return res.status(404).json({ error: "Product not found" });
}
const body = req.body;
if (body.name !== undefined) product.name = body.name;
if (body.price !== undefined) product.price = parseFloat(body.price);
if (body.category !== undefined) product.category = body.category;
if (body.inStock !== undefined) product.inStock = Boolean(body.inStock);
await product.save();
return res.json(product.toDict());
});
Router.delete("/api/products/{productId:int}", async (req, res) => {
const product = await Product.findById(req.params.productId);
if (product === null) {
return res.status(404).json({ error: "Product not found" });
}
await product.delete();
return res.status(204).end();
});The generator reads the model's properties and creates routes with type casting and null checks. Customize the generated code immediately. It is regular TypeScript, not magic.
Options
| Flag | Description | Example |
|---|---|---|
--prefix | Custom route prefix (default: /api) | --prefix /api/v2 |
--middleware | Add middleware to all routes | --middleware authMiddleware |
6. tina4 generate migration -- Migration Scaffolding
The generate migration command creates a timestamped migration file with UP and DOWN sections. The timestamp ensures migrations run in order.
tina4 generate migration add_category_to_productsCreated src/migrations/20260322120500_add_category_to_products.sqlThe generated file:
-- UP
-- Add your forward migration SQL here
-- DOWN
-- Add your rollback migration SQL hereFill in the SQL:
-- UP
ALTER TABLE products ADD COLUMN category TEXT DEFAULT '';
-- DOWN
ALTER TABLE products DROP COLUMN category;The timestamp prefix (20260322120500) ensures migrations run in order. Each migration runs once. The framework tracks which ones have been applied.
When to Generate a Migration
Generate a new migration when you need to change the database schema after the initial model migration. Adding a column. Creating an index. Renaming a table. Each change gets its own migration file with a unique timestamp.
7. tina4 generate middleware -- Middleware Scaffolding
The generate middleware command creates a middleware function with the correct signature and a placeholder for your logic.
tina4 generate middleware rateLimitCreated src/middleware/rateLimit.tsThe generated file:
import { Middleware } from "tina4-nodejs";
async function rateLimit(req: any, res: any, next: Function) {
// Add your middleware logic here
// Continue to the next middleware or route handler
return next();
// Or return early to block the request:
// return res.status(429).json({ error: "Rate limit exceeded" });
}
export default rateLimit;The middleware is a named function. Reference it in route definitions:
Router.get("/api/data", async (req, res) => {
return res.json({ data: "protected" });
}, "rateLimit");8. Generate All at Once
Combine flags to generate multiple files in a single command:
tina4 generate model Product --with-route --with-migrationCreated src/orm/Product.ts
Created src/routes/products.ts
Created src/migrations/20260322120000_create_products_table.sqlModel. CRUD routes. Migration. All wired together. Ready to use.
9. tina4 doctor -- Health Check
The doctor command checks your project for common issues:
tina4 doctorTina4 Doctor -- Checking your project...
[OK] Node.js v20.11.1 detected
[OK] npm package manager found
[OK] tina4-nodejs package installed (v3.10.3)
[OK] .env file exists
[OK] Database connection: sqlite:///data/app.db
[OK] Database is accessible
[OK] src/routes/ directory exists (3 route files)
[OK] src/orm/ directory exists (2 model files)
[OK] src/templates/ directory exists (5 templates)
[OK] src/public/ directory exists (static files served)
[OK] tests/ directory exists (4 test files)
[WARN] No migrations found in src/migrations/
[OK] AI context: Claude Code detected, CLAUDE.md present
[OK] .gitignore includes .env, data/, logs/
12 checks passed, 1 warning, 0 errorsDoctor checks:
- Node.js version and package manager
- Tina4 package installation and version
.envfile existence and critical variables- Database connectivity
- Directory structure
- Missing files or configurations
- AI tool context files
- Git configuration
The warnings give actionable advice. If your database is not configured, it tells you exactly what to add to .env.
10. tina4 test -- Running Tests
tina4 testRunning tests...
ProductTest
[PASS] test_create_product
[PASS] test_load_product
2 tests, 2 passed, 0 failed (0.12s)This runs all tests in the tests/ directory. See Chapter 18 for full testing documentation.
Test Options
tina4 test --filter ProductTest # Specific test class
tina4 test --verbose # Show assertion detailsOr use npm:
npm test11. tina4 routes -- Route Listing
See all registered routes in your project:
tina4 routesRegistered Routes:
Method Path Handler Middleware
------ -------------------------- -------------------------- ----------
GET /health healthCheck -
GET /api/products listProducts ResponseCache:300
GET /api/products/{productId} getProduct -
POST /api/products createProduct authMiddleware
PUT /api/products/{productId} updateProduct authMiddleware
DELETE /api/products/{productId} deleteProduct authMiddleware
GET /api/auth/login - -
POST /api/auth/login login -
GET /admin adminDashboard -
9 routes registeredThis is useful for verifying that your routes are registered and for finding the handler function for a specific URL.
Filtering
tina4 routes --method POST # Filter by HTTP method
tina4 routes --filter products # Filter by path pattern
tina4 routes --middleware auth # Filter by middlewareWhen debugging routing issues, check here first. If a route does not match, tina4 routes shows whether it was registered and what middleware is attached.
12. tina4 migrate -- Database Migrations
Run pending migrations:
tina4 migrateRunning migrations...
[UP] 20260322000100_create_users_table.sql
[UP] 20260322000200_create_products_table.sql
[UP] 20260322000300_add_category_to_products.sql
3 migrations appliedRollback
tina4 migrate --downRolling back last migration...
[DOWN] 20260322000300_add_category_to_products.sql
1 migration rolled backMigration Table Auto-Upgrade
If your project was created with an earlier version of Tina4, the tina4_migration tracking table may use the older v2 schema. Running tina4 migrate detects the old layout and adds the missing migration_id, batch, and executed_at columns, backfilling existing data. No manual intervention needed.
Status
tina4 migrate --statusMigration Status:
Status Migration
-------- -----------------------------------------
Applied 20260322000100_create_users_table.sql
Applied 20260322000200_create_products_table.sql
Applied 20260322000300_add_category_to_products.sql
Pending 20260322000400_create_orders_table.sql
3 applied, 1 pending13. tina4 queue -- Queue Management
tina4 queue:work # Start processing jobs
tina4 queue:work --queue send-email # Process specific queue
tina4 queue:dead # List dead letter jobs
tina4 queue:retry 42 # Retry a dead job
tina4 queue:clear --older-than 7d # Clear old dead jobs14. tina4 build -- Production Build
tina4 buildCompiles TypeScript to JavaScript. Bundles for production. Optimizes:
Building for production...
Compiled 47 TypeScript files
Output: dist/
Size: 245KB
Ready for deployment:
node dist/app.js15. Custom CLI Commands
Create custom seed scripts as standalone TypeScript files:
// scripts/seed-products.ts
import { Database } from "tina4-nodejs/orm";
async function seedProducts() {
const db = Database.getConnection();
const products = [
{ name: "Keyboard", price: 79.99 },
{ name: "Mouse", price: 29.99 },
{ name: "Monitor", price: 399.99 }
];
for (const p of products) {
await db.execute(
"INSERT INTO products (name, price) VALUES (:name, :price)",
p
);
}
console.log(`Seeded ${products.length} products`);
}
seedProducts();Run it:
npx tsx scripts/seed-products.ts16. Exercise: Scaffold a Feature in 5 Commands
Scaffold a complete "Customer" feature from scratch using only CLI commands.
Requirements
Starting from an existing Tina4 Node.js project, run 5 commands to create:
- A Customer ORM model with name, email, phone, and company fields
- A CRUD route file with all five REST endpoints
- A migration that creates the customers table
- Run the migration to create the table
- Run the doctor to verify everything
Expected Commands
# 1. Generate the model with route and migration
tina4 generate model Customer --with-route --with-migration
# 2. Edit the model to add fields (manual step)
# Add fields to src/orm/Customer.ts
# 3. Edit the migration to add columns (manual step)
# Add columns to the migration file
# 4. Run the migration
tina4 migrate
# 5. Run the doctor to verify everything is set up
tina4 doctorSolution
Command 1: Generate everything:
tina4 generate model Customer --with-route --with-migrationCommand 2: Edit src/orm/Customer.ts:
import { BaseModel } from "tina4-nodejs/orm";
export class Customer extends BaseModel {
static tableName = "customers";
static primaryKey = "id";
id!: number;
name!: string;
email!: string;
phone!: string;
company!: string;
createdAt!: string;
}Command 3: Edit the migration file:
-- UP
CREATE TABLE customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
phone TEXT,
company TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_customers_email ON customers(email);
-- DOWN
DROP TABLE IF EXISTS customers;Command 4: Run the migration:
tina4 migrateRunning migrations...
[UP] 20260322120000_create_customers_table.sql
1 migration appliedCommand 5: Verify with doctor:
tina4 doctor [OK] src/orm/ directory exists (3 model files)
[OK] src/routes/ directory exists (4 route files)
[OK] Database connection: sqlite:///data/app.db
...Now test the API:
curl -X POST http://localhost:7148/api/customers \
-H "Content-Type: application/json" \
-d '{"name": "Alice Corp", "email": "alice@corp.com", "phone": "+1-555-0100", "company": "Alice Corp"}'{
"id": 1,
"name": "Alice Corp",
"email": "alice@corp.com",
"phone": "+1-555-0100",
"company": "Alice Corp",
"created_at": "2026-03-22 12:00:00"
}From zero to a working CRUD API. Five commands. Under two minutes.
17. Gotchas
1. tina4 Command Not Found
Problem: Running tina4 gives "command not found".
Cause: The Tina4 CLI is not installed or not in your PATH.
Fix: Install the CLI: curl -fsSL https://tina4.com/install.sh | sh. Verify with tina4 --version. If installed but not found, add the installation directory to your PATH:
echo 'export PATH="$HOME/.tina4/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc2. Wrong Language Detected
Problem: tina4 init creates a PHP project instead of Node.js.
Cause: A composer.json file exists in the directory from a previous project.
Fix: Use explicit language selection: tina4 init nodejs my-project. Or delete the conflicting language file before running init.
3. Generated Files Overwrite Existing Code
Problem: Running tina4 generate route products overwrites your custom route file.
Cause: The generate command creates files at fixed paths. If the file exists, it is overwritten.
Fix: The CLI warns you before overwriting. Check if the file exists first. If you need to regenerate, rename the existing file: mv src/routes/products.ts src/routes/products_backup.ts.
4. Migration Order Issues
Problem: A migration fails because it references a table that does not exist yet.
Cause: Migration files run in alphabetical (timestamp) order. If migration B depends on a table created by migration A, but A has a later timestamp, B runs first and fails.
Fix: Use consistent timestamps. The tina4 generate migration command uses the current timestamp. Generating migrations in order ensures correct execution order. If you need to fix ordering, rename the migration files to adjust their timestamps.
5. tina4 serve Uses Wrong Port
Problem: tina4 serve starts on port 7148 but you need port 8080.
Cause: The default port is 7148 unless overridden.
Fix: Set it in .env: TINA4_PORT=8080. Or pass it as a flag: tina4 serve --port 8080. The .env value takes precedence over the default. The command-line flag overrides everything.
6. Doctor Shows False Warnings
Problem: tina4 doctor warns about missing migrations, but your project does not use migrations.
Cause: Doctor checks for common conventions. It warns about missing migrations regardless of your approach.
Fix: These are warnings, not errors. Ignore warnings that do not apply to your project. Doctor is a guide, not a gatekeeper.
7. Model Name Must Be PascalCase
Problem: tina4 generate model order_item creates a model class named order_item which is not valid TypeScript convention.
Cause: The CLI uses the argument as-is for the class name.
Fix: Use PascalCase for model names: tina4 generate model OrderItem. The CLI converts it to snake_case for the table name (order_items).
8. Build Fails
Fix: Check TypeScript errors with npx tsc --noEmit.
9. Port Conflict
Fix: Use tina4 serve --port 8080 or set TINA4_PORT in .env.
18. Documentation
tina4 docs # Download framework-specific book chapters to .tina4-docs/
tina4 books # Download the complete Tina4 book (all languages) to tina4-book/tina4 docs detects your project language and downloads only the relevant chapters. The documentation is available in Markdown format, optimised for AI tools and local reference.
19. Test Port (Dual-Port Development)
When TINA4_DEBUG=true, Tina4 automatically starts a second HTTP server on port + 1000:
- Main port (e.g. 7148) — hot-reload enabled, for AI dev tools
- Test port (e.g. 8148) — stable, no hot-reload, for user testing
This prevents the browser from refreshing mid-test when AI tools edit files.
| Setting | Effect |
|---|---|
TINA4_NO_AI_PORT=true | Disables the test port entirely |
TINA4_NO_RELOAD=true | Disables hot-reload on the main port too |
--no-reload | CLI flag equivalent of TINA4_NO_RELOAD |