tina4-nodejs 3.10.50 → 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.50",
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
 
@@ -46,6 +47,7 @@ const HELP = `
46
47
  Options:
47
48
  --port <number> Server port (default: 7148)
48
49
  --no-browser Don't open the browser on serve
50
+ --no-reload Disable file watcher / live-reload on serve
49
51
  --all Install AI context for all tools (with ai command)
50
52
  --force Overwrite existing AI context files (with ai command)
51
53
  --help Show this help message
@@ -85,11 +87,12 @@ async function main(): Promise<void> {
85
87
  const portIndex = args.indexOf("--port");
86
88
  const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 7148;
87
89
  const noBrowser = args.includes("--no-browser");
90
+ const noReload = args.includes("--no-reload");
88
91
 
89
92
  // Kill any existing process on the port
90
93
  killProcessOnPort(port);
91
94
 
92
- await serveProject({ port, noBrowser });
95
+ await serveProject({ port, noBrowser, noReload });
93
96
  break;
94
97
  }
95
98
  case "migrate": {
@@ -128,6 +131,40 @@ async function main(): Promise<void> {
128
131
  await runSeeds(args[1]);
129
132
  break;
130
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
+ }
131
168
  case "ai": {
132
169
  const { showMenu, installSelected, installAll } = await import("../../core/src/ai.js");
133
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>
@@ -4,9 +4,14 @@ import { existsSync } from "node:fs";
4
4
  export interface ServeOptions {
5
5
  port?: number;
6
6
  noBrowser?: boolean;
7
+ noReload?: boolean;
7
8
  }
8
9
 
9
10
  export async function serveProject(options: ServeOptions): Promise<void> {
11
+ if (options.noReload) {
12
+ process.env.TINA4_NO_RELOAD = "true";
13
+ }
14
+
10
15
  const port = options.port ?? 7148;
11
16
  const cwd = process.cwd();
12
17
 
@@ -33,26 +38,30 @@ export async function serveProject(options: ServeOptions): Promise<void> {
33
38
  });
34
39
 
35
40
  // Watch for file changes and reload routes
41
+ const noReload = ["true", "1", "yes"].includes((process.env.TINA4_NO_RELOAD ?? "").toLowerCase());
36
42
  const watchDirs = [routesDir, ormDir, modelsDir, templatesDir].filter((d) => existsSync(d));
37
- const watcher = watchForChanges(watchDirs, async () => {
38
- try {
39
- const { discoverRoutes } = await import("../../../core/src/index.js");
40
- // Clear routes BEFORE re-discovery to avoid stale duplicates
41
- server.router.clear();
42
- const routes = await discoverRoutes(routesDir);
43
- for (const route of routes) {
44
- server.router.addRoute(route);
43
+ let watcher: { close: () => void } | null = null;
44
+ if (!noReload) {
45
+ watcher = watchForChanges(watchDirs, async () => {
46
+ try {
47
+ const { discoverRoutes } = await import("../../../core/src/index.js");
48
+ // Clear routes BEFORE re-discovery to avoid stale duplicates
49
+ server.router.clear();
50
+ const routes = await discoverRoutes(routesDir);
51
+ for (const route of routes) {
52
+ server.router.addRoute(route);
53
+ }
54
+ console.log(` Reloaded ${routes.length} route(s)`);
55
+ } catch (err) {
56
+ console.error(" Error reloading routes:", err);
45
57
  }
46
- console.log(` Reloaded ${routes.length} route(s)`);
47
- } catch (err) {
48
- console.error(" Error reloading routes:", err);
49
- }
50
- });
58
+ });
59
+ }
51
60
 
52
61
  // Graceful shutdown
53
62
  const shutdown = () => {
54
63
  console.log("\n Shutting down...");
55
- watcher.close();
64
+ watcher?.close();
56
65
  server.close();
57
66
  process.exit(0);
58
67
  };
@@ -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
@@ -2467,7 +2481,7 @@ function renderBubbleChart(files,depGraph,scanMode){
2467
2481
  bubbles.forEach(function(b,i){if(b.f.path===src)srcIdx=i;});
2468
2482
  if(srcIdx===null)return;
2469
2483
  (depGraph[src]||[]).forEach(function(tgt){
2470
- var tgtName=tgt.split('.').pop().toLowerCase();
2484
+ var tgtName=basename(tgt);
2471
2485
  var tgtIdx=nameIdx[tgtName];
2472
2486
  if(tgtIdx!==undefined&&srcIdx!==tgtIdx)edges.push([srcIdx,tgtIdx]);
2473
2487
  });
@@ -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
@@ -673,7 +708,17 @@ ${reset}
673
708
  const requestId = Date.now().toString(36);
674
709
 
675
710
  // Wrap res.raw.end to inject dev toolbar and capture requests
676
- if (isDevMode() && !pathname.startsWith("/__dev")) {
711
+ // Skip toolbar injection on the AI port (no-reload behaviour)
712
+ const isAiPortRequest = !!(rawReq as any)._tina4AiPort;
713
+
714
+ // AI port: block /__dev_reload so AI tools never trigger a browser reload
715
+ if (isAiPortRequest && pathname === "/__dev_reload") {
716
+ res.raw.writeHead(404, { "Content-Type": "application/json" });
717
+ res.raw.end(JSON.stringify({ error: "Not available on AI port" }));
718
+ return;
719
+ }
720
+
721
+ if (isDevMode() && !pathname.startsWith("/__dev") && !isAiPortRequest) {
677
722
  const originalEnd = res.raw.end.bind(res.raw);
678
723
 
679
724
  const wrappedEnd: typeof res.raw.end = function (
@@ -857,7 +902,17 @@ ${reset}
857
902
  // Assign to module-level so handle() can dispatch without a server reference
858
903
  _dispatchFn = dispatch;
859
904
 
860
- 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);
861
916
 
862
917
  return new Promise((resolvePromise) => {
863
918
  server.listen(port, host, () => {
@@ -873,7 +928,34 @@ ${reset}
873
928
  // Determine server mode label
874
929
  const serverMode = isDebug ? "single" : (cluster.isWorker ? "cluster-worker" : "single");
875
930
 
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.
934
+ const noAiPort = isTruthy(process.env.TINA4_NO_AI_PORT ?? "");
935
+ let aiServer: ReturnType<typeof createServer> | null = null;
936
+ let testPort = port + 1000;
937
+
938
+ if (isDebug && !noAiPort) {
939
+ // Test port (port+1000): normal dispatch with full hot-reload
940
+ aiServer = createServer(async (req, res) => {
941
+ await dispatch(req, res);
942
+ });
943
+
944
+ aiServer.on("error", (err: any) => {
945
+ if (err.code === "EADDRINUSE") {
946
+ Log.warn(`Test port ${testPort} in use — skipping`);
947
+ aiServer = null;
948
+ }
949
+ });
950
+
951
+ aiServer.listen(testPort, host);
952
+ }
953
+
876
954
  // Banner goes to stdout via console.log — NOT through the framework logger
955
+ const dualPortLines = (isDebug && !noAiPort)
956
+ ? `\n Test Port: http://localhost:${testPort} (stable — no hot-reload)`
957
+ : "";
958
+
877
959
  console.log(`${color}
878
960
  ______ _ __ __
879
961
  /_ __/(_)___ ____ _/ // /
@@ -881,19 +963,24 @@ ${reset}
881
963
  / / / / / / / /_/ /__ __/
882
964
  /_/ /_/_/ /_/\\__,_/ /_/
883
965
  ${reset}
884
- Tina4 Node.js v${TINA4_VERSION} — This is not a framework
966
+ Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
885
967
 
886
968
  Server: http://${displayHost}:${port} (${serverMode})
887
969
  Swagger: http://localhost:${port}/swagger
888
970
  Dashboard: http://localhost:${port}/__dev
889
- Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})
971
+ Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})${dualPortLines}
890
972
  `);
891
973
  const noBrowser = isTruthy(process.env.TINA4_NO_BROWSER);
892
974
  if (!noBrowser) {
893
- 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);
894
980
  }
895
981
  resolvePromise({
896
982
  close: () => {
983
+ if (aiServer) aiServer.close();
897
984
  server.close();
898
985
  // Close database if ORM was initialized
899
986
  import("../../orm/src/index.js").then((orm) => orm.closeDatabase()).catch(() => {});