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 +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/cli/src/bin.ts +39 -2
- package/packages/cli/src/commands/init.ts +2 -2
- package/packages/cli/src/commands/serve.ts +23 -14
- package/packages/core/src/devAdmin.ts +16 -2
- package/packages/core/src/metrics.ts +2 -0
- package/packages/core/src/server.ts +115 -28
- package/packages/orm/src/adapters/mongodb.ts +679 -0
- package/packages/orm/src/adapters/odbc.ts +413 -0
- package/packages/orm/src/adapters/sqlite.ts +23 -3
- package/packages/orm/src/autoCrud.ts +15 -6
- package/packages/orm/src/baseModel.ts +32 -5
- package/packages/orm/src/database.ts +73 -2
- package/packages/orm/src/databaseResult.ts +26 -6
- package/packages/orm/src/index.ts +4 -0
- package/packages/orm/src/query.ts +7 -0
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 —
|
|
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">
|
|
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.
|
|
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"],
|
package/packages/cli/src/bin.ts
CHANGED
|
@@ -14,7 +14,7 @@ const args = process.argv.slice(2);
|
|
|
14
14
|
const command = args[0];
|
|
15
15
|
|
|
16
16
|
const HELP = `
|
|
17
|
-
tina4nodejs —
|
|
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>
|
|
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>
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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
|
|
53
|
-
// execFileSync imported at top of file
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
execFileSync(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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">
|
|
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
|
-
//
|
|
426
|
-
|
|
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} —
|
|
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
|
-
|
|
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
|
-
|
|
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} —
|
|
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
|
-
|
|
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(() => {});
|