tina4-nodejs 3.0.0-rc.2

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.
Files changed (119) hide show
  1. package/BENCHMARK_REPORT.md +96 -0
  2. package/CARBONAH.md +140 -0
  3. package/CLAUDE.md +599 -0
  4. package/COMPARISON.md +194 -0
  5. package/README.md +595 -0
  6. package/package.json +59 -0
  7. package/packages/cli/src/bin.ts +110 -0
  8. package/packages/cli/src/commands/init.ts +194 -0
  9. package/packages/cli/src/commands/migrate.ts +96 -0
  10. package/packages/cli/src/commands/migrateCreate.ts +59 -0
  11. package/packages/cli/src/commands/routes.ts +61 -0
  12. package/packages/cli/src/commands/serve.ts +58 -0
  13. package/packages/cli/src/commands/test.ts +83 -0
  14. package/packages/core/gallery/auth/meta.json +1 -0
  15. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
  16. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
  17. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
  18. package/packages/core/gallery/database/meta.json +1 -0
  19. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
  20. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
  21. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
  22. package/packages/core/gallery/error-overlay/meta.json +1 -0
  23. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
  24. package/packages/core/gallery/orm/meta.json +1 -0
  25. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
  26. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
  27. package/packages/core/gallery/queue/meta.json +1 -0
  28. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
  29. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
  30. package/packages/core/gallery/rest-api/meta.json +1 -0
  31. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
  32. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
  33. package/packages/core/gallery/templates/meta.json +1 -0
  34. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
  35. package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
  36. package/packages/core/public/css/tina4.css +2463 -0
  37. package/packages/core/public/css/tina4.min.css +1 -0
  38. package/packages/core/public/favicon.ico +0 -0
  39. package/packages/core/public/images/logo.svg +5 -0
  40. package/packages/core/public/images/tina4-logo-icon.webp +0 -0
  41. package/packages/core/public/js/frond.min.js +420 -0
  42. package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
  43. package/packages/core/public/js/tina4.min.js +93 -0
  44. package/packages/core/public/swagger/index.html +90 -0
  45. package/packages/core/public/swagger/oauth2-redirect.html +63 -0
  46. package/packages/core/src/ai.ts +359 -0
  47. package/packages/core/src/api.ts +248 -0
  48. package/packages/core/src/auth.ts +287 -0
  49. package/packages/core/src/cache.ts +121 -0
  50. package/packages/core/src/constants.ts +48 -0
  51. package/packages/core/src/container.ts +90 -0
  52. package/packages/core/src/devAdmin.ts +2024 -0
  53. package/packages/core/src/devMailbox.ts +316 -0
  54. package/packages/core/src/dotenv.ts +172 -0
  55. package/packages/core/src/errorOverlay.test.ts +122 -0
  56. package/packages/core/src/errorOverlay.ts +278 -0
  57. package/packages/core/src/events.ts +112 -0
  58. package/packages/core/src/fakeData.ts +309 -0
  59. package/packages/core/src/graphql.ts +812 -0
  60. package/packages/core/src/health.ts +31 -0
  61. package/packages/core/src/htmlElement.ts +172 -0
  62. package/packages/core/src/i18n.ts +136 -0
  63. package/packages/core/src/index.ts +88 -0
  64. package/packages/core/src/logger.ts +226 -0
  65. package/packages/core/src/messenger.ts +822 -0
  66. package/packages/core/src/middleware.ts +138 -0
  67. package/packages/core/src/queue.ts +481 -0
  68. package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
  69. package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
  70. package/packages/core/src/rateLimiter.ts +107 -0
  71. package/packages/core/src/request.ts +189 -0
  72. package/packages/core/src/response.ts +146 -0
  73. package/packages/core/src/routeDiscovery.ts +87 -0
  74. package/packages/core/src/router.ts +398 -0
  75. package/packages/core/src/scss.ts +366 -0
  76. package/packages/core/src/server.ts +610 -0
  77. package/packages/core/src/service.ts +380 -0
  78. package/packages/core/src/session.ts +480 -0
  79. package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
  80. package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
  81. package/packages/core/src/static.ts +58 -0
  82. package/packages/core/src/testing.ts +233 -0
  83. package/packages/core/src/types.ts +98 -0
  84. package/packages/core/src/watcher.ts +37 -0
  85. package/packages/core/src/websocket.ts +408 -0
  86. package/packages/core/src/wsdl.ts +546 -0
  87. package/packages/core/templates/errors/302.twig +14 -0
  88. package/packages/core/templates/errors/401.twig +9 -0
  89. package/packages/core/templates/errors/403.twig +29 -0
  90. package/packages/core/templates/errors/404.twig +29 -0
  91. package/packages/core/templates/errors/500.twig +38 -0
  92. package/packages/core/templates/errors/502.twig +9 -0
  93. package/packages/core/templates/errors/503.twig +12 -0
  94. package/packages/core/templates/errors/base.twig +37 -0
  95. package/packages/frond/src/engine.ts +1475 -0
  96. package/packages/frond/src/index.ts +2 -0
  97. package/packages/orm/src/adapters/firebird.ts +455 -0
  98. package/packages/orm/src/adapters/mssql.ts +440 -0
  99. package/packages/orm/src/adapters/mysql.ts +355 -0
  100. package/packages/orm/src/adapters/postgres.ts +362 -0
  101. package/packages/orm/src/adapters/sqlite.ts +270 -0
  102. package/packages/orm/src/autoCrud.ts +231 -0
  103. package/packages/orm/src/baseModel.ts +536 -0
  104. package/packages/orm/src/database.ts +321 -0
  105. package/packages/orm/src/fakeData.ts +118 -0
  106. package/packages/orm/src/index.ts +49 -0
  107. package/packages/orm/src/migration.ts +392 -0
  108. package/packages/orm/src/model.ts +56 -0
  109. package/packages/orm/src/query.ts +113 -0
  110. package/packages/orm/src/seeder.ts +120 -0
  111. package/packages/orm/src/sqlTranslation.ts +272 -0
  112. package/packages/orm/src/types.ts +110 -0
  113. package/packages/orm/src/validation.ts +93 -0
  114. package/packages/swagger/src/generator.ts +189 -0
  115. package/packages/swagger/src/index.ts +2 -0
  116. package/packages/swagger/src/ui.ts +48 -0
  117. package/skills/tina4-developer.skill +0 -0
  118. package/skills/tina4-js.skill +0 -0
  119. package/skills/tina4-maintainer.skill +0 -0
@@ -0,0 +1,110 @@
1
+ import { initProject } from "./commands/init.js";
2
+ import { serveProject } from "./commands/serve.js";
3
+ import { runMigrations } from "./commands/migrate.js";
4
+ import { createMigration } from "./commands/migrateCreate.js";
5
+ import { listRoutes } from "./commands/routes.js";
6
+ import { runTests } from "./commands/test.js";
7
+
8
+ const args = process.argv.slice(2);
9
+ const command = args[0];
10
+
11
+ const HELP = `
12
+ tina4nodejs — This is not a framework.
13
+
14
+ Usage:
15
+ tina4nodejs init [dir] Create a new Tina4 project (default: current directory)
16
+ tina4nodejs serve Start the dev server with hot-reload
17
+ tina4nodejs migrate Run pending SQL migrations
18
+ tina4nodejs migrate:create <desc> Create a new migration file
19
+ tina4nodejs routes List all registered routes
20
+ tina4nodejs test [file] Run project tests
21
+ tina4nodejs ai Detect AI coding tools and install context
22
+ tina4nodejs help Show this help message
23
+
24
+ Options:
25
+ --port <number> Server port (default: 7148)
26
+ --all Install AI context for all tools (with ai command)
27
+ --force Overwrite existing AI context files (with ai command)
28
+ --help Show this help message
29
+ `;
30
+
31
+ async function main(): Promise<void> {
32
+ switch (command) {
33
+ case "init": {
34
+ const name = args[1] || ".";
35
+ await initProject(name);
36
+ break;
37
+ }
38
+ case "serve": {
39
+ const portIndex = args.indexOf("--port");
40
+ const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 7148;
41
+ await serveProject({ port });
42
+ break;
43
+ }
44
+ case "migrate": {
45
+ await runMigrations(args[1]);
46
+ break;
47
+ }
48
+ case "migrate:create": {
49
+ const description = args.slice(1).join(" ");
50
+ await createMigration(description || undefined);
51
+ break;
52
+ }
53
+ case "routes": {
54
+ await listRoutes();
55
+ break;
56
+ }
57
+ case "test": {
58
+ await runTests(args[1]);
59
+ break;
60
+ }
61
+ case "ai": {
62
+ const { detectAi, installAiContext, installAllAiContext, aiStatusReport } = await import("../../core/src/ai.js");
63
+ const root = args[1] || ".";
64
+ const installAll = args.includes("--all");
65
+ const force = args.includes("--force");
66
+
67
+ // Show status
68
+ console.log(aiStatusReport(root));
69
+
70
+ // Install context
71
+ if (installAll) {
72
+ const created = installAllAiContext(root, force);
73
+ if (created.length > 0) {
74
+ console.log("Installed AI context files:");
75
+ for (const f of created) {
76
+ console.log(` + ${f}`);
77
+ }
78
+ } else {
79
+ console.log("All AI context files already exist (use --force to overwrite).");
80
+ }
81
+ } else {
82
+ const created = installAiContext(root, { force });
83
+ if (created.length > 0) {
84
+ console.log("Installed AI context files:");
85
+ for (const f of created) {
86
+ console.log(` + ${f}`);
87
+ }
88
+ } else {
89
+ console.log("No new AI context files needed.");
90
+ }
91
+ }
92
+ break;
93
+ }
94
+ case "help":
95
+ case "--help":
96
+ case "-h":
97
+ case undefined:
98
+ console.log(HELP);
99
+ break;
100
+ default:
101
+ console.error(`Unknown command: ${command}`);
102
+ console.log(HELP);
103
+ process.exit(1);
104
+ }
105
+ }
106
+
107
+ main().catch((err) => {
108
+ console.error(err);
109
+ process.exit(1);
110
+ });
@@ -0,0 +1,194 @@
1
+ import { mkdirSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join, resolve, basename } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+
5
+ export async function initProject(name: string): Promise<void> {
6
+ const targetDir = name === "." ? process.cwd() : resolve(name);
7
+ const projectName = name === "." ? basename(process.cwd()) : basename(name);
8
+
9
+ if (name !== "." && existsSync(targetDir)) {
10
+ console.error(`Error: Directory "${targetDir}" already exists.`);
11
+ process.exit(1);
12
+ }
13
+
14
+ console.log(`\n Creating Tina4 project: ${projectName}\n`);
15
+
16
+ // Create directory structure
17
+ const dirs = [
18
+ "",
19
+ "src",
20
+ "src/routes",
21
+ "src/routes/api",
22
+ "src/routes/api/hello",
23
+ "src/models",
24
+ "src/templates",
25
+ "public",
26
+ "data",
27
+ ];
28
+
29
+ for (const dir of dirs) {
30
+ mkdirSync(join(targetDir, dir), { recursive: true });
31
+ }
32
+
33
+ // package.json
34
+ writeFileSync(
35
+ join(targetDir, "package.json"),
36
+ JSON.stringify(
37
+ {
38
+ name: projectName,
39
+ version: "0.0.1",
40
+ private: true,
41
+ type: "module",
42
+ scripts: {
43
+ dev: "tina4 serve",
44
+ serve: "tina4 serve",
45
+ },
46
+ dependencies: {
47
+ "tina4-nodejs": "^0.0.1",
48
+ },
49
+ devDependencies: {
50
+ typescript: "^5.7.0",
51
+ tsx: "^4.19.0",
52
+ },
53
+ },
54
+ null,
55
+ 2
56
+ ) + "\n"
57
+ );
58
+
59
+ // tsconfig.json
60
+ writeFileSync(
61
+ join(targetDir, "tsconfig.json"),
62
+ JSON.stringify(
63
+ {
64
+ compilerOptions: {
65
+ target: "ES2022",
66
+ module: "Node16",
67
+ moduleResolution: "Node16",
68
+ strict: true,
69
+ esModuleInterop: true,
70
+ skipLibCheck: true,
71
+ outDir: "./dist",
72
+ rootDir: "./src",
73
+ },
74
+ include: ["src"],
75
+ },
76
+ null,
77
+ 2
78
+ ) + "\n"
79
+ );
80
+
81
+ // .gitignore
82
+ writeFileSync(
83
+ join(targetDir, ".gitignore"),
84
+ `node_modules/
85
+ dist/
86
+ *.db
87
+ *.sqlite
88
+ .env
89
+ .DS_Store
90
+ data/
91
+ `
92
+ );
93
+
94
+ // Sample route: GET /api/hello
95
+ writeFileSync(
96
+ join(targetDir, "src/routes/api/hello/get.ts"),
97
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
98
+
99
+ export const meta = {
100
+ summary: "Hello World",
101
+ tags: ["Example"],
102
+ };
103
+
104
+ export default async function (req: Tina4Request, res: Tina4Response): Promise<void> {
105
+ res.json({ message: "Hello from Tina4!", timestamp: new Date().toISOString() });
106
+ }
107
+ `
108
+ );
109
+
110
+ // Sample model: Example
111
+ writeFileSync(
112
+ join(targetDir, "src/models/Example.ts"),
113
+ `export default class Example {
114
+ static tableName = "examples";
115
+
116
+ static fields = {
117
+ id: { type: "integer" as const, primaryKey: true, autoIncrement: true },
118
+ name: { type: "string" as const, required: true, maxLength: 255 },
119
+ description: { type: "text" as const },
120
+ active: { type: "boolean" as const, default: true },
121
+ createdAt: { type: "datetime" as const, default: "now" },
122
+ };
123
+ }
124
+ `
125
+ );
126
+
127
+ // Sample template
128
+ writeFileSync(
129
+ join(targetDir, "src/templates/welcome.html.twig"),
130
+ `<!DOCTYPE html>
131
+ <html>
132
+ <head>
133
+ <title>{{ title }}</title>
134
+ </head>
135
+ <body>
136
+ <h1>Welcome to {{ name }}</h1>
137
+ <p>This is not a framework.</p>
138
+ </body>
139
+ </html>
140
+ `
141
+ );
142
+
143
+ // Static index page
144
+ writeFileSync(
145
+ join(targetDir, "public/index.html"),
146
+ `<!DOCTYPE html>
147
+ <html>
148
+ <head>
149
+ <title>Tina4</title>
150
+ <style>
151
+ body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
152
+ h1 { font-size: 2.5em; margin-bottom: 0.2em; }
153
+ p { color: #666; font-size: 1.1em; }
154
+ code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
155
+ a { color: #2563eb; }
156
+ </style>
157
+ </head>
158
+ <body>
159
+ <h1>tina4</h1>
160
+ <p><em>This is not a framework.</em></p>
161
+ <p>Your API is running. Try:</p>
162
+ <ul>
163
+ <li><a href="/api/hello">GET /api/hello</a></li>
164
+ <li><a href="/swagger">API Documentation</a></li>
165
+ </ul>
166
+ </body>
167
+ </html>
168
+ `
169
+ );
170
+
171
+ console.log(" Installing dependencies...\n");
172
+
173
+ let npmOk = true;
174
+ try {
175
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
176
+ } catch {
177
+ npmOk = false;
178
+ console.log("\n Note: npm install failed — the package may not be published yet.");
179
+ console.log(" Your project files have been created. You can install dependencies later.\n");
180
+ }
181
+
182
+ const absPath = resolve(targetDir);
183
+ const cdStep = name === "." ? "" : ` cd ${absPath}\n`;
184
+ console.log(`
185
+ Done! Your Tina4 project is ready.
186
+
187
+ Next steps:
188
+ ${cdStep} npm install
189
+ npx tina4nodejs serve
190
+
191
+ Your API will be running at http://localhost:7148
192
+ Swagger docs at http://localhost:7148/swagger
193
+ `);
194
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * CLI command: migrate — Run pending SQL migration files.
3
+ *
4
+ * Scans the migrations/ directory for .sql files, executes them in order,
5
+ * and records each as applied via the ORM migration tracker.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
8
+ import { join, resolve } from "node:path";
9
+
10
+ export async function runMigrations(migrationDir?: string): Promise<void> {
11
+ const dir = resolve(migrationDir ?? "migrations");
12
+
13
+ if (!existsSync(dir)) {
14
+ console.log(" No migrations/ directory found. Nothing to run.");
15
+ return;
16
+ }
17
+
18
+ // Initialise the database so the adapter is available
19
+ let initDatabase: typeof import("@tina4/orm").initDatabase;
20
+ let ensureMigrationTable: typeof import("@tina4/orm").ensureMigrationTable;
21
+ let isMigrationApplied: typeof import("@tina4/orm").isMigrationApplied;
22
+ let recordMigration: typeof import("@tina4/orm").recordMigration;
23
+ let getNextBatch: typeof import("@tina4/orm").getNextBatch;
24
+ let getAdapter: typeof import("@tina4/orm").getAdapter;
25
+
26
+ try {
27
+ const orm = await import("@tina4/orm");
28
+ initDatabase = orm.initDatabase;
29
+ ensureMigrationTable = orm.ensureMigrationTable;
30
+ isMigrationApplied = orm.isMigrationApplied;
31
+ recordMigration = orm.recordMigration;
32
+ getNextBatch = orm.getNextBatch;
33
+ getAdapter = orm.getAdapter;
34
+ } catch {
35
+ console.error(" Error: @tina4/orm is required to run migrations.");
36
+ process.exit(1);
37
+ }
38
+
39
+ // Ensure database is initialised (uses DATABASE_URL or defaults to sqlite)
40
+ try {
41
+ initDatabase();
42
+ } catch {
43
+ // Adapter may already be set — ignore
44
+ }
45
+
46
+ ensureMigrationTable();
47
+
48
+ // Collect .sql files sorted alphabetically
49
+ const files = readdirSync(dir)
50
+ .filter((f) => f.endsWith(".sql"))
51
+ .sort();
52
+
53
+ if (files.length === 0) {
54
+ console.log(" No .sql migration files found.");
55
+ return;
56
+ }
57
+
58
+ const batch = getNextBatch();
59
+ let applied = 0;
60
+
61
+ for (const file of files) {
62
+ const name = file.replace(/\.sql$/, "");
63
+
64
+ if (isMigrationApplied(name)) {
65
+ continue;
66
+ }
67
+
68
+ const sql = readFileSync(join(dir, file), "utf-8").trim();
69
+ if (!sql) continue;
70
+
71
+ console.log(` Migrating: ${file}`);
72
+
73
+ const adapter = getAdapter();
74
+ // Split on semicolons and execute each statement
75
+ const statements = sql.split(";").map((s) => s.trim()).filter(Boolean);
76
+
77
+ for (const stmt of statements) {
78
+ try {
79
+ adapter.execute(stmt);
80
+ } catch (err) {
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ console.error(` Error in ${file}: ${msg}`);
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ recordMigration(name, batch);
88
+ applied++;
89
+ }
90
+
91
+ if (applied === 0) {
92
+ console.log(" Nothing to migrate — all migrations already applied.");
93
+ } else {
94
+ console.log(` Applied ${applied} migration(s) (batch ${batch}).`);
95
+ }
96
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * CLI command: migrate:create — Create a new SQL migration file.
3
+ *
4
+ * Usage:
5
+ * tina4 migrate:create "create users table"
6
+ * tina4 migrate:create add_email_to_users
7
+ *
8
+ * Creates migrations/000001_description.sql (next sequence number).
9
+ */
10
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+
13
+ export async function createMigration(description?: string): Promise<void> {
14
+ if (!description) {
15
+ console.error(" Usage: tina4 migrate:create <description>");
16
+ console.error(' Example: tina4 migrate:create "create users table"');
17
+ process.exit(1);
18
+ }
19
+
20
+ const dir = resolve("migrations");
21
+
22
+ // Ensure migrations/ exists
23
+ if (!existsSync(dir)) {
24
+ mkdirSync(dir, { recursive: true });
25
+ }
26
+
27
+ // Determine the next sequence number
28
+ const existing = existsSync(dir)
29
+ ? readdirSync(dir).filter((f) => f.endsWith(".sql")).sort()
30
+ : [];
31
+
32
+ let nextSeq = 1;
33
+ if (existing.length > 0) {
34
+ const last = existing[existing.length - 1];
35
+ const match = last.match(/^(\d+)/);
36
+ if (match) {
37
+ nextSeq = parseInt(match[1], 10) + 1;
38
+ }
39
+ }
40
+
41
+ // Sanitise description for filename
42
+ const safeName = description
43
+ .toLowerCase()
44
+ .replace(/[^a-z0-9]+/g, "_")
45
+ .replace(/^_|_$/g, "");
46
+
47
+ const seqStr = String(nextSeq).padStart(6, "0");
48
+ const fileName = `${seqStr}_${safeName}.sql`;
49
+ const filePath = join(dir, fileName);
50
+
51
+ const template = `-- Migration: ${description}
52
+ -- Created: ${new Date().toISOString()}
53
+
54
+ `;
55
+
56
+ writeFileSync(filePath, template, "utf-8");
57
+ console.log(` Created migration: ${fileName}`);
58
+ console.log(` Path: ${filePath}`);
59
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * CLI command: routes — List all registered routes.
3
+ *
4
+ * Discovers routes from src/routes/ and displays them in a table.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+
9
+ export async function listRoutes(): Promise<void> {
10
+ const routesDir = resolve("src/routes");
11
+
12
+ if (!existsSync(routesDir)) {
13
+ console.error(" No src/routes/ directory found. Are you in a Tina4 project?");
14
+ process.exit(1);
15
+ }
16
+
17
+ let discoverRoutes: typeof import("@tina4/core").discoverRoutes;
18
+
19
+ try {
20
+ const core = await import("@tina4/core");
21
+ discoverRoutes = core.discoverRoutes;
22
+ } catch {
23
+ console.error(" Error: @tina4/core is required to list routes.");
24
+ process.exit(1);
25
+ }
26
+
27
+ const routes = await discoverRoutes(routesDir);
28
+
29
+ if (routes.length === 0) {
30
+ console.log(" No routes found.");
31
+ return;
32
+ }
33
+
34
+ // Sort by path then method
35
+ routes.sort((a: { path: string; method: string }, b: { path: string; method: string }) => {
36
+ const pathCmp = a.path.localeCompare(b.path);
37
+ return pathCmp !== 0 ? pathCmp : a.method.localeCompare(b.method);
38
+ });
39
+
40
+ console.log("");
41
+ console.log(" Registered Routes:");
42
+ console.log(" " + "-".repeat(70));
43
+ console.log(
44
+ " " +
45
+ "METHOD".padEnd(10) +
46
+ "PATH".padEnd(40) +
47
+ "SUMMARY"
48
+ );
49
+ console.log(" " + "-".repeat(70));
50
+
51
+ for (const route of routes) {
52
+ const method = route.method.toUpperCase().padEnd(10);
53
+ const path = route.path.padEnd(40);
54
+ const summary = route.meta?.summary ?? "";
55
+ console.log(` ${method}${path}${summary}`);
56
+ }
57
+
58
+ console.log(" " + "-".repeat(70));
59
+ console.log(` Total: ${routes.length} route(s)`);
60
+ console.log("");
61
+ }
@@ -0,0 +1,58 @@
1
+ import { resolve } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+
4
+ export interface ServeOptions {
5
+ port?: number;
6
+ }
7
+
8
+ export async function serveProject(options: ServeOptions): Promise<void> {
9
+ const port = options.port ?? 7148;
10
+ const cwd = process.cwd();
11
+
12
+ const routesDir = resolve(cwd, "src/routes");
13
+ const modelsDir = resolve(cwd, "src/models");
14
+ const templatesDir = resolve(cwd, "src/templates");
15
+ const staticDir = resolve(cwd, "public");
16
+
17
+ if (!existsSync(routesDir) && !existsSync(modelsDir)) {
18
+ console.error(" Error: Not a Tina4 project. Run this from a project created with 'tina4 init'.");
19
+ process.exit(1);
20
+ }
21
+
22
+ const { startServer } = await import("@tina4/core");
23
+ const { watchForChanges } = await import("@tina4/core/src/watcher.js");
24
+
25
+ const server = await startServer({
26
+ port,
27
+ routesDir,
28
+ modelsDir,
29
+ templatesDir,
30
+ staticDir,
31
+ });
32
+
33
+ // Watch for file changes and reload routes
34
+ const watcher = watchForChanges([routesDir, modelsDir, templatesDir], async () => {
35
+ try {
36
+ const { discoverRoutes } = await import("@tina4/core");
37
+ const routes = await discoverRoutes(routesDir);
38
+ server.router.clear();
39
+ for (const route of routes) {
40
+ server.router.addRoute(route);
41
+ }
42
+ console.log(` Reloaded ${routes.length} route(s)`);
43
+ } catch (err) {
44
+ console.error(" Error reloading routes:", err);
45
+ }
46
+ });
47
+
48
+ // Graceful shutdown
49
+ const shutdown = () => {
50
+ console.log("\n Shutting down...");
51
+ watcher.close();
52
+ server.close();
53
+ process.exit(0);
54
+ };
55
+
56
+ process.on("SIGINT", shutdown);
57
+ process.on("SIGTERM", shutdown);
58
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * CLI command: test — Run project tests.
3
+ *
4
+ * Looks for test files and executes them with tsx.
5
+ * Supports: test/integration.ts, test/*.ts, tests/*.ts, *.test.ts patterns.
6
+ */
7
+ import { existsSync, readdirSync } from "node:fs";
8
+ import { resolve, join } from "node:path";
9
+ import { execSync } from "node:child_process";
10
+
11
+ export async function runTests(testPath?: string): Promise<void> {
12
+ const cwd = process.cwd();
13
+
14
+ // If a specific test file is provided, run it directly
15
+ if (testPath) {
16
+ const file = resolve(testPath);
17
+ if (!existsSync(file)) {
18
+ console.error(` Error: Test file not found: ${testPath}`);
19
+ process.exit(1);
20
+ }
21
+ console.log(` Running: ${testPath}\n`);
22
+ try {
23
+ execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
24
+ } catch {
25
+ process.exit(1);
26
+ }
27
+ return;
28
+ }
29
+
30
+ // Auto-discover test files
31
+ const candidates = [
32
+ "test/integration.ts",
33
+ "test",
34
+ "tests",
35
+ ];
36
+
37
+ let testFiles: string[] = [];
38
+
39
+ for (const candidate of candidates) {
40
+ const fullPath = resolve(cwd, candidate);
41
+ if (!existsSync(fullPath)) continue;
42
+
43
+ // If it's a file, run it
44
+ if (candidate.endsWith(".ts")) {
45
+ testFiles.push(fullPath);
46
+ break;
47
+ }
48
+
49
+ // If it's a directory, collect all .ts and .test.ts files
50
+ try {
51
+ const files = readdirSync(fullPath)
52
+ .filter((f) => f.endsWith(".ts"))
53
+ .map((f) => join(fullPath, f));
54
+ testFiles.push(...files);
55
+ } catch {
56
+ // skip
57
+ }
58
+ if (testFiles.length > 0) break;
59
+ }
60
+
61
+ if (testFiles.length === 0) {
62
+ console.log(" No test files found.");
63
+ console.log(" Looked in: test/integration.ts, test/*.ts, tests/*.ts");
64
+ return;
65
+ }
66
+
67
+ console.log(` Found ${testFiles.length} test file(s)\n`);
68
+
69
+ let failed = false;
70
+ for (const file of testFiles) {
71
+ const relative = file.replace(cwd + "/", "");
72
+ console.log(` Running: ${relative}`);
73
+ try {
74
+ execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
75
+ } catch {
76
+ failed = true;
77
+ }
78
+ }
79
+
80
+ if (failed) {
81
+ process.exit(1);
82
+ }
83
+ }
@@ -0,0 +1 @@
1
+ {"name": "Auth", "description": "JWT login form with token display", "try_url": "/gallery/auth"}
@@ -0,0 +1,22 @@
1
+ /** Gallery: Auth — login endpoint, returns a JWT token. */
2
+ import type { Tina4Request, Tina4Response } from "@tina4/core";
3
+
4
+ export default async function (req: Tina4Request, res: Tina4Response) {
5
+ const body = (req.body as Record<string, unknown>) ?? {};
6
+ const username = (body.username as string) ?? "";
7
+ const password = (body.password as string) ?? "";
8
+
9
+ if (username && password) {
10
+ // In a real app: import { Auth } from "@tina4/core";
11
+ // const auth = new Auth();
12
+ // const token = auth.createToken({ username, role: "user" });
13
+ // For the gallery demo, generate a simple base64 token
14
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
15
+ const payload = Buffer.from(JSON.stringify({ username, role: "user", iat: Math.floor(Date.now() / 1000) })).toString("base64url");
16
+ const signature = Buffer.from("gallery-demo-signature").toString("base64url");
17
+ const token = `${header}.${payload}.${signature}`;
18
+ return res.json({ token, message: `Welcome ${username}!` });
19
+ }
20
+
21
+ return res.json({ error: "Username and password required" }, 401);
22
+ }