tina4-nodejs 3.13.133 → 3.13.134

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 (50) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/packages/cli/dist/bin.js +3181 -3051
  5. package/packages/cli/src/commands/generate.ts +33 -22
  6. package/packages/cli/src/commands/lint.ts +77 -111
  7. package/packages/core/dist/index.js +3090 -2952
  8. package/packages/core/src/.tina4-metrics.json +15004 -0
  9. package/packages/core/src/aiClient.ts +199 -161
  10. package/packages/core/src/dispatchPipeline.ts +65 -67
  11. package/packages/core/src/docs.ts +52 -544
  12. package/packages/core/src/docsParser.ts +270 -0
  13. package/packages/core/src/docsScanner.ts +121 -0
  14. package/packages/core/src/docsSignatures.ts +165 -0
  15. package/packages/core/src/index.ts +2 -0
  16. package/packages/core/src/logger.ts +68 -82
  17. package/packages/core/src/mcp.ts +32 -60
  18. package/packages/core/src/messenger.ts +136 -157
  19. package/packages/core/src/middleware.ts +56 -60
  20. package/packages/core/src/plan.ts +78 -70
  21. package/packages/core/src/projectIndex.ts +15 -288
  22. package/packages/core/src/projectIndexExtractors.ts +126 -0
  23. package/packages/core/src/projectIndexStorage.ts +122 -0
  24. package/packages/core/src/push.ts +281 -0
  25. package/packages/core/src/server.ts +182 -183
  26. package/packages/frond/dist/index.js +607 -770
  27. package/packages/frond/src/engine.ts +670 -818
  28. package/packages/orm/dist/index.js +3100 -2965
  29. package/packages/orm/src/adapters/mongodb.ts +99 -144
  30. package/packages/orm/src/baseModel.ts +429 -515
  31. package/packages/orm/src/fakeData.ts +73 -61
  32. package/packages/orm/src/migration.ts +96 -126
  33. package/packages/orm/src/seeder.ts +6 -238
  34. package/packages/orm/src/seederTable.ts +101 -0
  35. package/packages/orm/src/seederTypes.ts +14 -0
  36. package/packages/orm/src/validation.ts +97 -80
  37. package/types/core/src/aiClient.d.ts +5 -0
  38. package/types/core/src/docsParser.d.ts +28 -0
  39. package/types/core/src/docsScanner.d.ts +1 -0
  40. package/types/core/src/docsSignatures.d.ts +11 -0
  41. package/types/core/src/index.d.ts +2 -0
  42. package/types/core/src/messenger.d.ts +8 -0
  43. package/types/core/src/projectIndexExtractors.d.ts +3 -0
  44. package/types/core/src/projectIndexStorage.d.ts +13 -0
  45. package/types/core/src/push.d.ts +45 -0
  46. package/types/frond/src/engine.d.ts +25 -0
  47. package/types/orm/src/fakeData.d.ts +3 -0
  48. package/types/orm/src/seeder.d.ts +3 -89
  49. package/types/orm/src/seederTable.d.ts +9 -0
  50. package/types/orm/src/seederTypes.d.ts +16 -0
@@ -1941,6 +1941,57 @@ function startLoopbackSiblings(
1941
1941
  );
1942
1942
  }
1943
1943
 
1944
+ function startProductionCluster(port: number, host: string): {
1945
+ close: () => void;
1946
+ router: Router;
1947
+ port: number;
1948
+ } {
1949
+ const numCPUs = os.cpus().length;
1950
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
1951
+ const isTty = isatty(1);
1952
+ const color = isTty ? "\x1b[32m" : "";
1953
+ const reset = isTty ? "\x1b[0m" : "";
1954
+ const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
1955
+ if (!isBannerSuppressed()) {
1956
+ const [swaggerLine] = bannerSurfaceLines(port, { swaggerEnabled: swaggerAdvertised(), devAdminEnabled: false });
1957
+ console.log(`${color}
1958
+ Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
1959
+
1960
+ Server: http://${displayHost}:${port} (cluster, ${numCPUs} workers)${swaggerLine}
1961
+ Debug: OFF (Log level: ${logLevel})
1962
+ ${reset}`);
1963
+ }
1964
+ for (let i = 0; i < numCPUs; i++) cluster.fork();
1965
+ cluster.on("exit", (worker, code) => {
1966
+ if (code !== 0) {
1967
+ console.log(` Worker ${worker.process.pid} exited (code ${code}), restarting...`);
1968
+ cluster.fork();
1969
+ }
1970
+ });
1971
+ return {
1972
+ close: () => {
1973
+ stopAllBackgroundTasks();
1974
+ for (const id in cluster.workers) cluster.workers[id]?.kill();
1975
+ },
1976
+ router: new Router(),
1977
+ port,
1978
+ };
1979
+ }
1980
+
1981
+ function wireLocaleGlobal(frondEngine: any, base: string): void {
1982
+ if (!frondEngine) return;
1983
+ const localeDir = resolve(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
1984
+ if (!existsSync(localeDir)) return;
1985
+ try {
1986
+ const localeFiles = readdirSync(localeDir).filter((file: string) => file.endsWith(".json"));
1987
+ if (localeFiles.length === 0 || frondEngine.globals?.t) return;
1988
+ const i18nInstance = new I18n(process.env.TINA4_LOCALE ?? "en", localeDir);
1989
+ frondEngine.addGlobal("t", (key: string, params?: Record<string, string>) => i18nInstance.t(key, params));
1990
+ } catch {
1991
+ // Locale directory unreadable — skip auto-wire.
1992
+ }
1993
+ }
1994
+
1944
1995
  export async function startServer(config?: Tina4Config): Promise<{
1945
1996
  close: () => void;
1946
1997
  router: Router;
@@ -1986,62 +2037,8 @@ export async function startServer(config?: Tina4Config): Promise<{
1986
2037
  // Cluster mode for production: fork workers based on CPU count
1987
2038
  // Only when --production is explicitly set (via TINA4_PRODUCTION env var)
1988
2039
  const isProduction = (process.env.TINA4_PRODUCTION ?? "").toLowerCase() === "true";
1989
- if (cluster.isPrimary && isProduction) {
1990
- const numCPUs = os.cpus().length;
1991
- if (numCPUs > 1) {
1992
- const displayHost = host === "0.0.0.0" ? "localhost" : host;
1993
- const isTty = isatty(1);
1994
- const color = isTty ? "\x1b[32m" : "";
1995
- const reset = isTty ? "\x1b[0m" : "";
1996
- const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
1997
-
1998
- if (!isBannerSuppressed()) {
1999
- // Only advertise a surface that is actually reachable (issue #99).
2000
- // Cluster mode is the production path: debug is OFF, so /__dev always
2001
- // 404s here and is never advertised; /swagger only when explicitly on.
2002
- // Cluster mode is the production path: debug is OFF, so /__dev never
2003
- // advertises here.
2004
- const [swaggerLine] = bannerSurfaceLines(port, {
2005
- swaggerEnabled: swaggerAdvertised(),
2006
- devAdminEnabled: false,
2007
- });
2008
- console.log(`${color}
2009
- ______ _ __ __
2010
- /_ __/(_)___ ____ _/ // /
2011
- / / / / __ \\/ __ \`/ // /_
2012
- / / / / / / / /_/ /__ __/
2013
- /_/ /_/_/ /_/\\__,_/ /_/
2014
- ${reset}
2015
- Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
2016
-
2017
- Server: http://${displayHost}:${port} (cluster, ${numCPUs} workers)${swaggerLine}
2018
- Debug: OFF (Log level: ${logLevel})
2019
- `);
2020
- }
2021
-
2022
- for (let i = 0; i < numCPUs; i++) {
2023
- cluster.fork();
2024
- }
2025
-
2026
- cluster.on("exit", (worker, code, _signal) => {
2027
- if (code !== 0) {
2028
- console.log(` Worker ${worker.process.pid} exited (code ${code}), restarting...`);
2029
- cluster.fork();
2030
- }
2031
- });
2032
-
2033
- // Return a handle that kills all workers
2034
- return {
2035
- close: () => {
2036
- stopAllBackgroundTasks();
2037
- for (const id in cluster.workers) {
2038
- cluster.workers[id]?.kill();
2039
- }
2040
- },
2041
- router: new Router(),
2042
- port,
2043
- };
2044
- }
2040
+ if (cluster.isPrimary && isProduction && os.cpus().length > 1) {
2041
+ return startProductionCluster(port, host);
2045
2042
  }
2046
2043
 
2047
2044
  const base = config?.basePath ? resolve(config.basePath) : process.cwd();
@@ -2112,20 +2109,7 @@ ${reset}
2112
2109
  }
2113
2110
 
2114
2111
  // Auto-wire i18n → template global t() when locale files exist
2115
- if (frondEngine) {
2116
- const localeDir = resolve(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
2117
- if (existsSync(localeDir)) {
2118
- try {
2119
- const localeFiles = readdirSync(localeDir).filter((f: string) => f.endsWith(".json"));
2120
- if (localeFiles.length > 0 && !frondEngine.globals?.t) {
2121
- const i18nInstance = new I18n(process.env.TINA4_LOCALE ?? "en", localeDir);
2122
- frondEngine.addGlobal("t", (key: string, params?: Record<string, string>) => i18nInstance.t(key, params));
2123
- }
2124
- } catch {
2125
- // Locale directory unreadable — skip auto-wire
2126
- }
2127
- }
2128
- }
2112
+ wireLocaleGlobal(frondEngine, base);
2129
2113
 
2130
2114
  // Built-in middleware
2131
2115
  middleware.use(cors());
@@ -2166,119 +2150,8 @@ ${reset}
2166
2150
  console.log(`\n \x1b[36mCSRF\x1b[0m protection enabled (TINA4_CSRF)`);
2167
2151
  }
2168
2152
 
2169
- // Initialize ORM if models directory exists (check src/orm/ first, then src/models/)
2170
- const hasOrmDir = existsSync(ormDir);
2171
- const hasModelsDir = existsSync(modelsDir);
2172
- if (hasOrmDir || hasModelsDir) {
2173
- try {
2174
- const orm = await import("../../orm/src/index.js");
2175
- const dbConfig = config?.database ?? {};
2176
- await orm.initDatabase({
2177
- type: dbConfig.type ?? "sqlite",
2178
- path: dbConfig.path ?? "./data/tina4.db",
2179
- });
2180
-
2181
- // Discover from src/orm/ (primary) and src/models/ (fallback), merge results
2182
- let models = hasOrmDir ? await orm.discoverModels(ormDir) : [];
2183
- if (hasModelsDir) {
2184
- const extraModels = await orm.discoverModels(modelsDir);
2185
- // Only add models not already discovered (src/orm/ takes priority)
2186
- const existingTables = new Set(models.map((m: any) => m.definition.tableName));
2187
- for (const m of extraModels) {
2188
- if (!existingTables.has(m.definition.tableName)) {
2189
- models.push(m);
2190
- }
2191
- }
2192
- }
2193
- if (models.length > 0) {
2194
- console.log(`\n Models discovered:`);
2195
- await orm.syncModels(models);
2196
- for (const { definition } of models) {
2197
- console.log(` \x1b[35m${definition.tableName}\x1b[0m (${Object.keys(definition.fields).length} fields)`);
2198
- }
2199
-
2200
- // Generate auto-CRUD routes ONLY for models that explicitly opted in via
2201
- // `static autoCrud = true` (the documented opt-in gate; default false). The
2202
- // server previously generated the 5 CRUD endpoints for every discovered model
2203
- // regardless of the flag, contradicting the documented contract. (Python's
2204
- // AutoCrud is opt-in too — via an explicit AutoCrud.register/discover call.)
2205
- const crudModels = orm.crudEligibleModels(models);
2206
- const crudRoutes = crudModels.length > 0 ? orm.generateCrudRoutes(crudModels) : [];
2207
- for (const route of crudRoutes) {
2208
- // Only add if no file-based route already handles this
2209
- const existing = router.match(route.method, route.pattern.replace(/\{(\w+)\}/g, "test").replace(/\[(\w+)\]/g, "test"));
2210
- if (!existing) {
2211
- router.addRoute(route);
2212
- }
2213
- }
2214
-
2215
- if (crudRoutes.length > 0) {
2216
- console.log(`\n Auto-CRUD endpoints:`);
2217
- for (const route of crudRoutes) {
2218
- console.log(` \x1b[33m${route.method.padEnd(7)}\x1b[0m ${route.pattern}`);
2219
- }
2220
- }
2221
- }
2222
- } catch (err) {
2223
- console.warn(`\n ORM not available (install @tina4/orm to enable):`, err);
2224
- }
2225
-
2226
- // Auto-run pending migrations on startup — AFTER initDatabase()/model sync,
2227
- // BEFORE the server listens. Non-breaking: a failure is logged and boot
2228
- // continues (the helper never throws). Gated on a migrations/ dir + the
2229
- // TINA4_AUTO_MIGRATE flag (default on) + a resolvable DB adapter.
2230
- await autoMigrateOnStartup("migrations", base);
2231
- }
2232
-
2233
- // Initialize Swagger — gated on TINA4_SWAGGER_ENABLED (default: enabled
2234
- // in debug mode, off in production). Loading the swagger module also
2235
- // pulls in route discovery for the generator, so skip the import entirely
2236
- // when disabled.
2237
- try {
2238
- const swagger = await import("../../swagger/src/index.js");
2239
- // Single source of truth for BOTH the gated routes and the bundled
2240
- // public/swagger assets (which static serving would otherwise expose).
2241
- swaggerAssetsEnabled = swagger.swaggerEnabled();
2242
- if (!swaggerAssetsEnabled) {
2243
- // Skip the rest of the swagger block when disabled.
2244
- throw new Error("__swagger_disabled__");
2245
- }
2246
-
2247
- // Collect model definitions for schema generation
2248
- let modelDefs: Array<{ tableName: string; fields: Record<string, unknown> }> = [];
2249
- try {
2250
- const orm = await import("../../orm/src/index.js");
2251
- const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync(d));
2252
- const seenTables = new Set<string>();
2253
- for (const dir of allModelDirs) {
2254
- const discovered = await orm.discoverModels(dir);
2255
- for (const m of discovered) {
2256
- if (!seenTables.has(m.definition.tableName)) {
2257
- modelDefs.push(m.definition);
2258
- seenTables.add(m.definition.tableName);
2259
- }
2260
- }
2261
- }
2262
- } catch {
2263
- // ORM not available, swagger will work without model schemas
2264
- }
2265
-
2266
- // Read router.getRoutes() LIVE on every call — never a captured snapshot
2267
- // (SWAG-NODE-BOOT-SNAPSHOT, ADR-0004). This used to close over a
2268
- // boot-time `allRoutes` array, so the spec regenerated per request but
2269
- // over a FROZEN route list: a route registered after boot (hot-reload, or
2270
- // DevAdmin.register running after this block — see the /__feedback
2271
- // exclusion above) never appeared. Python/PHP/Ruby always read the live
2272
- // route table inside their generate() call; this is that same shape.
2273
- const getSpec = () => swagger.generate(router.getRoutes(), modelDefs as any);
2274
- const swaggerRoutes = swagger.createSwaggerRoutes(getSpec);
2275
- for (const route of swaggerRoutes) {
2276
- router.addRoute(route);
2277
- }
2278
- } catch {
2279
- // Swagger not available
2280
- }
2281
-
2153
+ await initializeOrm(config, router, base, ormDir, modelsDir);
2154
+ swaggerAssetsEnabled = await configureSwagger(router, ormDir, modelsDir);
2282
2155
  // Register dev admin dashboard routes
2283
2156
  if (DevAdmin.isEnabled()) {
2284
2157
  DevAdmin.register(router);
@@ -2641,3 +2514,129 @@ ${reset}
2641
2514
  });
2642
2515
  });
2643
2516
  }
2517
+ async function initializeOrm(
2518
+ config: Tina4Config | undefined,
2519
+ router: Router,
2520
+ base: string,
2521
+ ormDir: string,
2522
+ modelsDir: string,
2523
+ ): Promise<void> {
2524
+ // Initialize ORM if models directory exists (check src/orm/ first, then src/models/)
2525
+ const hasOrmDir = existsSync(ormDir);
2526
+ const hasModelsDir = existsSync(modelsDir);
2527
+ if (hasOrmDir || hasModelsDir) {
2528
+ try {
2529
+ const orm = await import("../../orm/src/index.js");
2530
+ const dbConfig = config?.database ?? {};
2531
+ await orm.initDatabase({
2532
+ type: dbConfig.type ?? "sqlite",
2533
+ path: dbConfig.path ?? "./data/tina4.db",
2534
+ });
2535
+
2536
+ // Discover from src/orm/ (primary) and src/models/ (fallback), merge results
2537
+ let models = hasOrmDir ? await orm.discoverModels(ormDir) : [];
2538
+ if (hasModelsDir) {
2539
+ const extraModels = await orm.discoverModels(modelsDir);
2540
+ // Only add models not already discovered (src/orm/ takes priority)
2541
+ const existingTables = new Set(models.map((m: any) => m.definition.tableName));
2542
+ for (const m of extraModels) {
2543
+ if (!existingTables.has(m.definition.tableName)) {
2544
+ models.push(m);
2545
+ }
2546
+ }
2547
+ }
2548
+ if (models.length > 0) {
2549
+ console.log(`\n Models discovered:`);
2550
+ await orm.syncModels(models);
2551
+ for (const { definition } of models) {
2552
+ console.log(` \x1b[35m${definition.tableName}\x1b[0m (${Object.keys(definition.fields).length} fields)`);
2553
+ }
2554
+
2555
+ // Generate auto-CRUD routes ONLY for models that explicitly opted in via
2556
+ // `static autoCrud = true` (the documented opt-in gate; default false). The
2557
+ // server previously generated the 5 CRUD endpoints for every discovered model
2558
+ // regardless of the flag, contradicting the documented contract. (Python's
2559
+ // AutoCrud is opt-in too — via an explicit AutoCrud.register/discover call.)
2560
+ const crudModels = orm.crudEligibleModels(models);
2561
+ const crudRoutes = crudModels.length > 0 ? orm.generateCrudRoutes(crudModels) : [];
2562
+ for (const route of crudRoutes) {
2563
+ // Only add if no file-based route already handles this
2564
+ const existing = router.match(route.method, route.pattern.replace(/\{(\w+)\}/g, "test").replace(/\[(\w+)\]/g, "test"));
2565
+ if (!existing) {
2566
+ router.addRoute(route);
2567
+ }
2568
+ }
2569
+
2570
+ if (crudRoutes.length > 0) {
2571
+ console.log(`\n Auto-CRUD endpoints:`);
2572
+ for (const route of crudRoutes) {
2573
+ console.log(` \x1b[33m${route.method.padEnd(7)}\x1b[0m ${route.pattern}`);
2574
+ }
2575
+ }
2576
+ }
2577
+ } catch (err) {
2578
+ console.warn(`\n ORM not available (install @tina4/orm to enable):`, err);
2579
+ }
2580
+
2581
+ // Auto-run pending migrations on startup — AFTER initDatabase()/model sync,
2582
+ // BEFORE the server listens. Non-breaking: a failure is logged and boot
2583
+ // continues (the helper never throws). Gated on a migrations/ dir + the
2584
+ // TINA4_AUTO_MIGRATE flag (default on) + a resolvable DB adapter.
2585
+ await autoMigrateOnStartup("migrations", base);
2586
+ }
2587
+
2588
+ }
2589
+
2590
+ async function configureSwagger(router: Router, ormDir: string, modelsDir: string): Promise<boolean> {
2591
+ let enabled = false;
2592
+ // Initialize Swagger — gated on TINA4_SWAGGER_ENABLED (default: enabled
2593
+ // in debug mode, off in production). Loading the swagger module also
2594
+ // pulls in route discovery for the generator, so skip the import entirely
2595
+ // when disabled.
2596
+ try {
2597
+ const swagger = await import("../../swagger/src/index.js");
2598
+ // Single source of truth for BOTH the gated routes and the bundled
2599
+ // public/swagger assets (which static serving would otherwise expose).
2600
+ enabled = swagger.swaggerEnabled();
2601
+ if (!swaggerAssetsEnabled) {
2602
+ // Skip the rest of the swagger block when disabled.
2603
+ throw new Error("__swagger_disabled__");
2604
+ }
2605
+
2606
+ // Collect model definitions for schema generation
2607
+ let modelDefs: Array<{ tableName: string; fields: Record<string, unknown> }> = [];
2608
+ try {
2609
+ const orm = await import("../../orm/src/index.js");
2610
+ const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync(d));
2611
+ const seenTables = new Set<string>();
2612
+ for (const dir of allModelDirs) {
2613
+ const discovered = await orm.discoverModels(dir);
2614
+ for (const m of discovered) {
2615
+ if (!seenTables.has(m.definition.tableName)) {
2616
+ modelDefs.push(m.definition);
2617
+ seenTables.add(m.definition.tableName);
2618
+ }
2619
+ }
2620
+ }
2621
+ } catch {
2622
+ // ORM not available, swagger will work without model schemas
2623
+ }
2624
+
2625
+ // Read router.getRoutes() LIVE on every call — never a captured snapshot
2626
+ // (SWAG-NODE-BOOT-SNAPSHOT, ADR-0004). This used to close over a
2627
+ // boot-time `allRoutes` array, so the spec regenerated per request but
2628
+ // over a FROZEN route list: a route registered after boot (hot-reload, or
2629
+ // DevAdmin.register running after this block — see the /__feedback
2630
+ // exclusion above) never appeared. Python/PHP/Ruby always read the live
2631
+ // route table inside their generate() call; this is that same shape.
2632
+ const getSpec = () => swagger.generate(router.getRoutes(), modelDefs as any);
2633
+ const swaggerRoutes = swagger.createSwaggerRoutes(getSpec);
2634
+ for (const route of swaggerRoutes) {
2635
+ router.addRoute(route);
2636
+ }
2637
+ } catch {
2638
+ // Swagger not available
2639
+ }
2640
+
2641
+ return enabled;
2642
+ }