tina4-nodejs 3.10.55 → 3.10.60

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.
package/CLAUDE.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## What This Project Is
6
6
 
7
- Tina4 for Node.js/TypeScript v3.10.42 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.10.42 — The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
8
8
 
9
9
  The philosophy: zero ceremony, batteries included, file system as source of truth.
10
10
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <img src="https://tina4.com/logo.svg" alt="Tina4" width="200">
3
3
  </p>
4
4
  <h1 align="center">Tina4 Node.js</h1>
5
- <h3 align="center">This Is Now A 4Framework</h3>
5
+ <h3 align="center">The Intelligent Native Application 4ramework</h3>
6
6
  <p align="center">54 built-in features. Zero dependencies. One import, everything works.</p>
7
7
  <p align="center">
8
8
  <a href="https://www.npmjs.com/package/tina4-nodejs"><img src="https://img.shields.io/npm/v/tina4-nodejs?color=7b1fa2&label=npm" alt="npm"></a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.10.55",
3
+ "version": "3.10.60",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
6
6
  "keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
@@ -14,7 +14,7 @@ const args = process.argv.slice(2);
14
14
  const command = args[0];
15
15
 
16
16
  const HELP = `
17
- tina4nodejs — This is not a framework.
17
+ tina4nodejs — The Intelligent Native Application 4ramework
18
18
 
19
19
  Usage:
20
20
  tina4nodejs init [dir] Create a new Tina4 project (default: current directory)
@@ -26,6 +26,7 @@ const HELP = `
26
26
  tina4nodejs routes List all registered routes
27
27
  tina4nodejs test [file] Run project tests
28
28
  tina4nodejs seed [file] Run database seed files from src/seeds/
29
+ tina4nodejs console Open an interactive REPL with the framework loaded
29
30
  tina4nodejs ai Install AI coding assistant context files
30
31
  tina4nodejs help Show this help message
31
32
 
@@ -130,6 +131,40 @@ async function main(): Promise<void> {
130
131
  await runSeeds(args[1]);
131
132
  break;
132
133
  }
134
+ case "console": {
135
+ const repl = await import("node:repl");
136
+ const { loadEnv } = await import("@tina4/core");
137
+ const { Router } = await import("@tina4/core");
138
+ const { initDatabase, Database } = await import("@tina4/orm");
139
+ const { Log } = await import("@tina4/core");
140
+
141
+ loadEnv();
142
+
143
+ const dbUrl = process.env.DATABASE_URL;
144
+ let db: unknown = null;
145
+ if (dbUrl) {
146
+ try {
147
+ db = await initDatabase({ url: dbUrl });
148
+ } catch {
149
+ console.warn(" Warning: could not connect to database — db will be null");
150
+ }
151
+ }
152
+
153
+ console.log("\n Tina4 Node.js Console");
154
+ console.log(" Type JavaScript. Framework is loaded.");
155
+ console.log(" Available: db, Router, Database, Log");
156
+ console.log(" Exit: Ctrl+D or .exit\n");
157
+
158
+ const r = repl.start({ prompt: "tina4> " });
159
+
160
+ r.context.Router = Router;
161
+ r.context.Database = Database;
162
+ r.context.Log = Log;
163
+ r.context.db = db;
164
+
165
+ await new Promise<void>((resolve) => r.on("exit", resolve));
166
+ break;
167
+ }
133
168
  case "ai": {
134
169
  const { showMenu, installSelected, installAll } = await import("../../core/src/ai.js");
135
170
  const root = args[1] || ".";
@@ -196,7 +196,7 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
196
196
  </head>
197
197
  <body>
198
198
  <h1>Welcome to {{ name }}</h1>
199
- <p>This is not a framework.</p>
199
+ <p>The Intelligent Native Application 4ramework</p>
200
200
  </body>
201
201
  </html>
202
202
  `
@@ -219,7 +219,7 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
219
219
  </head>
220
220
  <body>
221
221
  <h1>tina4</h1>
222
- <p><em>This is not a framework.</em></p>
222
+ <p><em>The Intelligent Native Application 4ramework</em></p>
223
223
  <p>Your API is running. Try:</p>
224
224
  <ul>
225
225
  <li><a href="/api/hello">GET /api/hello</a></li>
@@ -21,7 +21,21 @@ import { quickMetrics, fullAnalysis, fileDetail } from "./metrics.js";
21
21
 
22
22
  const cpuCount = osCpus().length;
23
23
 
24
- const TINA4_VERSION = "3.10.34";
24
+ // Read version from root package.json dynamically
25
+ const TINA4_VERSION = (() => {
26
+ try {
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ // Try root package.json first, then core package.json
29
+ for (const rel of ["../../../package.json", "../../package.json"]) {
30
+ const p = resolve(__dirname, rel);
31
+ if (existsSync(p)) {
32
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
33
+ if (pkg.version) return pkg.version;
34
+ }
35
+ }
36
+ } catch {}
37
+ return "0.0.0";
38
+ })();
25
39
 
26
40
  // ---------------------------------------------------------------------------
27
41
  // Types
@@ -60,8 +60,10 @@ function hasMatchingTest(relPath: string): boolean {
60
60
  const name = relPath.split('/').pop()?.replace('.ts', '').replace('.js', '') || '';
61
61
  const patterns = [
62
62
  `test/${name}.test.ts`,
63
+ `test/${name}.test.js`,
63
64
  `${relPath.replace('.ts', '.test.ts').replace('.js', '.test.js')}`,
64
65
  `tests/${name}.test.ts`,
66
+ `spec/${name}.spec.ts`,
65
67
  ];
66
68
  return patterns.some(p => fs.existsSync(p));
67
69
  }
@@ -46,23 +46,62 @@ const TINA4_VERSION = readPackageVersion();
46
46
  const frondCache = new Map<string, InstanceType<any>>();
47
47
 
48
48
  /**
49
- * Test-bind each port in a subprocess to find one that is available.
50
- * Falls back to `start` if none of the candidates work.
49
+ * Kill whatever process is listening on *port*.
50
+ * Uses lsof on macOS/Linux and netstat + taskkill on Windows.
51
+ * Throws if the port cannot be freed.
51
52
  */
52
- function findAvailablePort(start: number, maxTries = 10): number {
53
- // execFileSync imported at top of file (ESM)
54
- for (let offset = 0; offset < maxTries; offset++) {
55
- const port = start + offset;
56
- try {
57
- execFileSync(process.execPath, ["-e", `
58
- const s = require("net").createServer();
59
- s.listen(${port}, "127.0.0.1", () => { s.close(); process.exit(0); });
60
- s.on("error", () => process.exit(1));
61
- `], { timeout: 1000 });
62
- return port;
63
- } catch { continue; }
53
+ function killPort(port: number): void {
54
+ // execFileSync is imported at the top of the file
55
+ console.log(` Port ${port} in use killing existing process...`);
56
+ try {
57
+ if (process.platform === "win32") {
58
+ const out = execFileSync("netstat", ["-ano"], { encoding: "utf-8", timeout: 5000 });
59
+ for (const line of out.split("\n")) {
60
+ if (line.includes(`:${port}`) && (line.includes("LISTENING") || line.includes("ESTABLISHED"))) {
61
+ const parts = line.trim().split(/\s+/);
62
+ const pid = parts[parts.length - 1];
63
+ if (/^\d+$/.test(pid)) {
64
+ execFileSync("taskkill", ["/PID", pid, "/F"], { timeout: 5000 });
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ } else {
70
+ const pids = execFileSync("lsof", ["-ti", `:${port}`], { encoding: "utf-8", timeout: 5000 })
71
+ .trim()
72
+ .split("\n")
73
+ .filter(Boolean);
74
+ for (const pid of pids) {
75
+ if (/^\d+$/.test(pid.trim())) {
76
+ process.kill(parseInt(pid.trim(), 10), "SIGTERM");
77
+ }
78
+ }
79
+ }
80
+ // Brief pause for the OS to reclaim the port
81
+ execFileSync(process.execPath, ["-e", "setTimeout(() => {}, 500)"], { timeout: 2000 });
82
+ console.log(` Port ${port} freed`);
83
+ } catch (err) {
84
+ throw new Error(`Could not free port ${port}: ${err}`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Check if *port* is available; if not, kill the process on it and return *port*.
90
+ * The auto-increment behaviour is intentionally removed — the server always
91
+ * claims the requested port.
92
+ */
93
+ function findAvailablePort(start: number): number {
94
+ try {
95
+ execFileSync(process.execPath, ["-e", `
96
+ const s = require("net").createServer();
97
+ s.listen(${start}, "0.0.0.0", () => { s.close(); process.exit(0); });
98
+ s.on("error", () => process.exit(1));
99
+ `], { timeout: 1000 });
100
+ return start;
101
+ } catch {
102
+ killPort(start);
103
+ return start;
64
104
  }
65
- return start;
66
105
  }
67
106
 
68
107
  /**
@@ -322,7 +361,7 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
322
361
  <div class="hero">
323
362
  <img src="/images/tina4-logo-icon.webp" class="logo" alt="Tina4">
324
363
  <h1>Tina4NodeJs</h1>
325
- <p class="tagline">This is not a framework</p>
364
+ <p class="tagline">The Intelligent Native Application 4ramework</p>
326
365
  <div class="actions">
327
366
  <a href="https://tina4.com/nodejs" class="btn" target="_blank">Website</a>
328
367
  <a href="/__dev" class="btn">Dev Admin</a>
@@ -422,12 +461,8 @@ export async function startServer(config?: Tina4Config): Promise<{
422
461
  const host = resolved.host;
423
462
  let port = resolved.port;
424
463
 
425
- // Auto-increment port if the requested one is already in use
426
- const availablePort = findAvailablePort(port);
427
- if (availablePort !== port) {
428
- console.log(` Port ${port} in use, using ${availablePort} instead`);
429
- port = availablePort;
430
- }
464
+ // Claim the requested port kill whatever is on it if needed
465
+ port = findAvailablePort(port);
431
466
 
432
467
  // Cluster mode for production: fork workers based on CPU count
433
468
  // Only when --production is explicitly set (via TINA4_PRODUCTION env var)
@@ -448,7 +483,7 @@ export async function startServer(config?: Tina4Config): Promise<{
448
483
  / / / / / / / /_/ /__ __/
449
484
  /_/ /_/_/ /_/\\__,_/ /_/
450
485
  ${reset}
451
- Tina4 Node.js v${TINA4_VERSION} — This is not a framework
486
+ Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
452
487
 
453
488
  Server: http://${displayHost}:${port} (cluster, ${numCPUs} workers)
454
489
  Swagger: http://localhost:${port}/swagger
@@ -867,7 +902,17 @@ ${reset}
867
902
  // Assign to module-level so handle() can dispatch without a server reference
868
903
  _dispatchFn = dispatch;
869
904
 
870
- const server = createServer(dispatch);
905
+ // When dual-port is active (debug mode + no TINA4_NO_AI_PORT), tag main port requests
906
+ // as AI port to suppress reload/toolbar injection. Test port (port+1000) gets full reload.
907
+ const _dualPortActive = isTruthy(process.env.TINA4_DEBUG ?? "") &&
908
+ !isTruthy(process.env.TINA4_NO_AI_PORT ?? "");
909
+ const mainPortDispatch = _dualPortActive
910
+ ? async (req: IncomingMessage, res: ServerResponse) => {
911
+ (req as any)._tina4AiPort = true;
912
+ await dispatch(req, res);
913
+ }
914
+ : dispatch;
915
+ const server = createServer(mainPortDispatch);
871
916
 
872
917
  return new Promise((resolvePromise) => {
873
918
  server.listen(port, host, () => {
@@ -883,33 +928,32 @@ ${reset}
883
928
  // Determine server mode label
884
929
  const serverMode = isDebug ? "single" : (cluster.isWorker ? "cluster-worker" : "single");
885
930
 
886
- // AI dual-port: start a second HTTP server on port+1 when in debug mode
887
- // and TINA4_NO_AI_PORT is not set. This port serves requests without
888
- // dev-reload or toolbar injection so AI coding tools get stable responses.
931
+ // AI dual-port: main port = AI dev port (no reload), port+1000 = user testing port (hot-reload)
932
+ // When TINA4_DEBUG=true and TINA4_NO_AI_PORT is not set, main server suppresses reload/toolbar
933
+ // and a second server on port+1000 provides the normal hot-reload experience.
889
934
  const noAiPort = isTruthy(process.env.TINA4_NO_AI_PORT ?? "");
890
935
  let aiServer: ReturnType<typeof createServer> | null = null;
891
- let aiPort = port + 1;
936
+ let testPort = port + 1000;
892
937
 
893
938
  if (isDebug && !noAiPort) {
939
+ // Test port (port+1000): normal dispatch with full hot-reload
894
940
  aiServer = createServer(async (req, res) => {
895
- // Tag the request so dispatch knows it came from the AI port
896
- (req as any)._tina4AiPort = true;
897
941
  await dispatch(req, res);
898
942
  });
899
943
 
900
944
  aiServer.on("error", (err: any) => {
901
945
  if (err.code === "EADDRINUSE") {
902
- Log.warn(`AI port ${aiPort} in use — skipping`);
946
+ Log.warn(`Test port ${testPort} in use — skipping`);
903
947
  aiServer = null;
904
948
  }
905
949
  });
906
950
 
907
- aiServer.listen(aiPort, host);
951
+ aiServer.listen(testPort, host);
908
952
  }
909
953
 
910
954
  // Banner goes to stdout via console.log — NOT through the framework logger
911
- const aiPortLine = (isDebug && !noAiPort)
912
- ? `\n AI Port: http://localhost:${aiPort} (no-reload)`
955
+ const dualPortLines = (isDebug && !noAiPort)
956
+ ? `\n Test Port: http://localhost:${testPort} (stable — no hot-reload)`
913
957
  : "";
914
958
 
915
959
  console.log(`${color}
@@ -919,16 +963,20 @@ ${reset}
919
963
  / / / / / / / /_/ /__ __/
920
964
  /_/ /_/_/ /_/\\__,_/ /_/
921
965
  ${reset}
922
- Tina4 Node.js v${TINA4_VERSION} — This is not a framework
966
+ Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
923
967
 
924
968
  Server: http://${displayHost}:${port} (${serverMode})
925
969
  Swagger: http://localhost:${port}/swagger
926
970
  Dashboard: http://localhost:${port}/__dev
927
- Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})${aiPortLine}
971
+ Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})${dualPortLines}
928
972
  `);
929
973
  const noBrowser = isTruthy(process.env.TINA4_NO_BROWSER);
930
974
  if (!noBrowser) {
931
- openBrowser(`http://${displayHost}:${port}`);
975
+ // Open browser on test port (hot-reload) if available, otherwise main port
976
+ const browserTarget = (isDebug && !noAiPort && aiServer)
977
+ ? `http://${displayHost}:${testPort}`
978
+ : `http://${displayHost}:${port}`;
979
+ openBrowser(browserTarget);
932
980
  }
933
981
  resolvePromise({
934
982
  close: () => {