tina4-nodejs 3.13.81 → 3.13.82

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
@@ -1,10 +1,10 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.81)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.82)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
5
5
  ## What This Project Is
6
6
 
7
- Tina4 for Node.js/TypeScript v3.13.81 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.82 - 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
 
@@ -51,7 +51,7 @@ tina4-nodejs/
51
51
  baseModel.ts # Base model class
52
52
  fakeData.ts # ORM-aware fake data (extends core, field-type heuristics)
53
53
  seeder.ts # Database seeding (seedTable, seedOrm, seedModels)
54
- sqlTranslation.ts # Cross-engine SQL translator + query cache
54
+ sqlTranslator.ts # Cross-engine SQL translator + query cache
55
55
  swagger/ # OpenAPI spec generator, Swagger UI
56
56
  frond/ # Zero-dependency Twig-compatible template engine
57
57
  test/
@@ -155,7 +155,7 @@ Database layer with auto-CRUD generation, seeding, fake data, and SQL translatio
155
155
  - `types.ts` — `FieldDefinition`, `ModelDefinition`, `DatabaseAdapter`, `QueryOptions`
156
156
  - `fakeData.ts` — ORM-aware fake data extending core (adds `forField()` with column-name heuristics)
157
157
  - `seeder.ts` — Database seeding (`seedTable` raw SQL, `seedOrm` model-based, `seedModels` FK-ordered batch). All return a `SeedSummary { seeded, failed, errors }`; per-row failures are logged + counted + skipped (`strict` re-raises). Options: `{ overrides, clear, seed, strict }`.
158
- - `sqlTranslation.ts` — Cross-engine SQL translator (`SQLTranslator`) and TTL query cache (`QueryCache`)
158
+ - `sqlTranslator.ts` — Cross-engine SQL translator (`SQLTranslator`) and TTL query cache (`QueryCache`)
159
159
  - **Instance methods:** `save(): this|false` (fluent, false on failure), `delete()`, `forceDelete()`, `restore()`, `load(sql, params?, include?): boolean`, `validate(): string[]`, `toDict(include?)`, `toAssoc(include?)`, `toObject()`, `toArray(): unknown[]`, `toList()`, `toJson(include?)`, `hasOne(class, fk)`, `hasMany(class, fk, limit?, offset?)`, `belongsTo(class, fk)`
160
160
  - **Static methods:** `find(id, include?)`, `findById(id, include?)`, `findOrFail(id)`, `create(data)`, `all(where?, params?, include?)`, `select(sql, params?)`, `selectOne(sql, params?, include?)`, `where(conditions, params?, limit?, offset?, include?)`, `count(conditions?, params?)`, `withTrashed(conditions?, params?, limit?, offset?)`, `scope(name, filterSql, params?)` (registers reusable method), `createTable()`, `query()`, `_processForeignKeys()`, `_applyFkRegistry()`
161
161
  - **Foreign key auto-wire:** Declare a field with `type: "foreignKey"` and `references: "ModelName"` to auto-wire both `belongsTo` on the declaring model and `hasMany` on the referenced model. Optional `relatedName` overrides the has-many key. Models must be registered via `BaseModel.registerModel(name, class)` for name-based resolution. Example: `user_id: { type: "foreignKey", references: "User" }` → `post.belongsTo(User, "user_id")` and `user.hasMany(Post, "user_id")` both resolve without extra wiring.
@@ -526,7 +526,7 @@ accepts `seed`/`clear`/`strict`, and returns `{ seeded, failed, errors, table }`
526
526
 
527
527
  Column-name heuristics in `forField()`: columns named `email`, `phone`, `name`, `address`, `city`, `country`, `company`, `url`, `uuid`, `ip`, `currency`, etc. get contextually appropriate fake data.
528
528
 
529
- ## Module: SQL Translation (`packages/orm/src/sqlTranslation.ts`)
529
+ ## Module: SQL Translation (`packages/orm/src/sqlTranslator.ts`)
530
530
 
531
531
  Cross-engine SQL dialect translator and in-memory query cache. All translator methods are static on `SQLTranslator`. The `QueryCache` provides TTL-based caching with LRU eviction.
532
532
 
@@ -1237,7 +1237,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
1237
1237
 
1238
1238
  ## v3 Features Summary
1239
1239
 
1240
- - **45 built-in features**, zero third-party dependencies
1240
+ - **98 built-in features**, zero third-party dependencies
1241
1241
  - **5,379 tests** passing across 154 files (build + typecheck green; 9 PostgreSQL/Valkey service-gated failures)
1242
1242
  - **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
1243
1243
  - **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
package/README.md CHANGED
@@ -3,11 +3,11 @@
3
3
  </p>
4
4
  <h1 align="center">Tina4 Node.js</h1>
5
5
  <h3 align="center">The Intelligent Native Application 4ramework</h3>
6
- <p align="center">55 built-in features. Zero dependencies. One import, everything works.</p>
6
+ <p align="center">98 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/core"><img src="https://img.shields.io/npm/v/@tina4/core?color=7b1fa2&label=npm" alt="npm"></a>
9
9
  <img src="https://img.shields.io/badge/tests-2%2C897%20passing-brightgreen" alt="Tests">
10
- <img src="https://img.shields.io/badge/features-55-blue" alt="Features">
10
+ <img src="https://img.shields.io/badge/features-98-blue" alt="Features">
11
11
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
12
12
  <a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
13
13
  </p>
@@ -93,7 +93,7 @@ Benchmarked with `wrk`: 5,000 requests, 50 concurrent, median of 3 runs:
93
93
  | Raw `node:http` | 91,110 | 0 | 1 |
94
94
  | **Tina4 Node.js** | **84,771** | 0 | 55 |
95
95
 
96
- Tina4 Node.js runs at **93% of raw Node.js speed** while providing 55 built-in features, a zero-overhead architecture.
96
+ Tina4 Node.js runs at **93% of raw Node.js speed** while providing 98 built-in features, a zero-overhead architecture.
97
97
 
98
98
  **Across all 4 Tina4 implementations:**
99
99
 
@@ -107,7 +107,7 @@ Tina4 Node.js runs at **93% of raw Node.js speed** while providing 55 built-in f
107
107
 
108
108
  ## Cross-Framework Parity
109
109
 
110
- Tina4 ships identical features across four languages: same architecture, same conventions, same 55 features:
110
+ Tina4 ships identical features across four languages: same architecture, same conventions, same 97 features:
111
111
 
112
112
  | | Python | PHP | Ruby | Node.js |
113
113
  |---|--------|-----|------|---------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.81",
3
+ "version": "3.13.82",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
6
6
  "keywords": [
@@ -11,9 +11,9 @@ import { runSeeds } from "./commands/seed.js";
11
11
  import { runMetrics } from "./commands/metrics.js";
12
12
  import { queueCommand, QUEUE_SUBCOMMAND_NAMES } from "./commands/queue.js";
13
13
  import { buildImage } from "./commands/build.js";
14
- import { execSync } from "node:child_process";
15
- import { existsSync, readFileSync } from "node:fs";
16
- import { dirname, join } from "node:path";
14
+ import { execSync, spawnSync } from "node:child_process";
15
+ import { existsSync, readFileSync, statSync } from "node:fs";
16
+ import { delimiter, dirname, join } from "node:path";
17
17
  import { fileURLToPath, pathToFileURL } from "node:url";
18
18
 
19
19
  // ── Version (cheap, side-effect-free) ───────────────────────────────
@@ -73,6 +73,8 @@ export interface CommandManifestEntry {
73
73
  summary: string;
74
74
  args?: string[];
75
75
  subcommands?: string[];
76
+ /** True when the tina4 client implements this command, not the framework. */
77
+ delegated?: boolean;
76
78
  }
77
79
 
78
80
  export interface CommandManifest {
@@ -84,14 +86,19 @@ export interface CommandManifest {
84
86
  /**
85
87
  * Build the machine-readable manifest of the CLI's command surface.
86
88
  *
87
- * Pure data: reads the module-level COMMANDS registry plus the framework
88
- * version — no bootstrap, no database, no migrations, no app imports. This is
89
- * exactly what `commands --json` serialises and what the tina4 Rust client
90
- * consumes to discover which commands this framework supports.
89
+ * Pure data: reads the module-level COMMANDS and DELEGATED registries plus the
90
+ * framework version — no bootstrap, no database, no migrations, no app imports.
91
+ * This is exactly what `commands --json` serialises and what the tina4 Rust
92
+ * client consumes to discover which commands this framework supports.
93
+ *
94
+ * Commands handed to the `tina4` client carry `delegated: true`, so the manifest
95
+ * describes the WHOLE surface the CLI accepts while still saying who implements
96
+ * each one. The client needs no change: its help renderer already drops manifest
97
+ * names that clash with its own natives.
91
98
  *
92
99
  * Shape (identical keys to the Python master):
93
100
  * { framework: "nodejs", version: "<x.y.z>",
94
- * commands: [{ name, summary, args?, subcommands? }, ...] }
101
+ * commands: [{ name, summary, args?, subcommands?, delegated? }, ...] }
95
102
  */
96
103
  export function buildCommandManifest(): CommandManifest {
97
104
  const commands: CommandManifestEntry[] = Object.entries(COMMANDS).map(([name, spec]) => {
@@ -100,6 +107,11 @@ export function buildCommandManifest(): CommandManifest {
100
107
  if (spec.subcommands && spec.subcommands.length) entry.subcommands = [...spec.subcommands];
101
108
  return entry;
102
109
  });
110
+ for (const [name, spec] of Object.entries(DELEGATED)) {
111
+ const entry: CommandManifestEntry = { name, summary: spec.summary, delegated: true };
112
+ if (spec.args && spec.args.length) entry.args = [...spec.args];
113
+ commands.push(entry);
114
+ }
103
115
  return { framework: "nodejs", version: readCliVersion(), commands };
104
116
  }
105
117
 
@@ -126,7 +138,8 @@ export function runCommands(args: string[] = []): void {
126
138
  console.log(`\n Tina4 ${manifest.framework} — ${manifest.version}\n`);
127
139
  const width = Math.max(...manifest.commands.map((c) => c.name.length));
128
140
  for (const c of manifest.commands) {
129
- console.log(` ${c.name.padEnd(width)} ${c.summary}`);
141
+ const marker = c.delegated ? ` (${CLIENT_BINARY} client)` : "";
142
+ console.log(` ${c.name.padEnd(width)} ${c.summary}${marker}`);
130
143
  if (c.subcommands && c.subcommands.length) {
131
144
  console.log(` ${" ".repeat(width)} ${c.subcommands.join(", ")}`);
132
145
  }
@@ -137,21 +150,24 @@ export function runCommands(args: string[] = []): void {
137
150
  /**
138
151
  * Print the human-readable command reference.
139
152
  *
140
- * Generated from the COMMANDS and GENERATORS registries — the SAME single
141
- * source of truth that drives dispatch (`main`) and the `commands --json`
153
+ * Generated from the COMMANDS, DELEGATED and GENERATORS registries — the SAME
154
+ * single source of truth that drives dispatch (`main`) and the `commands --json`
142
155
  * manifest — so the help text can never drift from what the CLI actually does.
143
156
  */
144
157
  function printHelp(): void {
145
158
  const commandRows: [string, string][] = Object.entries(COMMANDS).map(
146
159
  ([name, spec]) => [`${name}${spec.usage ? " " + spec.usage : ""}`, spec.summary],
147
160
  );
161
+ const delegatedRows: [string, string][] = Object.entries(DELEGATED).map(
162
+ ([name, spec]) => [`${name}${spec.usage ? " " + spec.usage : ""}`, spec.summary],
163
+ );
148
164
  const generatorRows: [string, string][] = Object.entries(GENERATORS).map(
149
165
  ([name, spec]) => [`generate ${name}${spec.usage ? " " + spec.usage : ""}`, spec.summary],
150
166
  );
151
167
 
152
168
  // Align summaries in a column; a left cell longer than the cap overflows
153
169
  // cleanly (2-space gap) rather than pushing every other summary out.
154
- const pad = Math.min(46, Math.max(...[...commandRows, ...generatorRows].map(([left]) => left.length)));
170
+ const pad = Math.min(46, Math.max(...[...commandRows, ...delegatedRows, ...generatorRows].map(([left]) => left.length)));
155
171
  const row = (left: string, summary: string): string => {
156
172
  const gap = left.length <= pad ? pad : left.length;
157
173
  return ` ${left.padEnd(gap)} ${summary}`;
@@ -166,6 +182,10 @@ function printHelp(): void {
166
182
  " Commands:",
167
183
  ...commandRows.map(([left, summary]) => row(left, summary)),
168
184
  "",
185
+ ` Delegated to the ${CLIENT_BINARY} client (same behaviour in every framework):`,
186
+ ...delegatedRows.map(([left, summary]) => row(left, summary)),
187
+ ` (these run the ${CLIENT_BINARY} client — install: curl -fsSL https://tina4.com/install.sh | sh)`,
188
+ "",
169
189
  " Generators:",
170
190
  ...generatorRows.map(([left, summary]) => row(left, summary)),
171
191
  "",
@@ -356,6 +376,130 @@ export const COMMANDS: Record<string, CommandSpec> = {
356
376
  },
357
377
  };
358
378
 
379
+ // ── Delegation to the `tina4` client ────────────────────────────────
380
+ //
381
+ // `doctor`, `setup` and `deploy` are owned by the Rust `tina4` client, not by
382
+ // any framework. `doctor` probes ALL FOUR runtimes plus package managers, ports
383
+ // and global AI-skills currency; `setup` installs language runtimes (Homebrew /
384
+ // Chocolatey, with UAC elevation on Windows) and scaffolds a project from
385
+ // nothing; `deploy` writes deployment boilerplate baked into the client binary.
386
+ // Cloning any of them into four languages would duplicate hundreds of lines per
387
+ // language for zero new capability — and four copies would immediately drift.
388
+ //
389
+ // So the framework CLI DELEGATES: it resolves `tina4` on PATH, runs it with the
390
+ // same argv, and exits with the client's exit code. All four frameworks reach
391
+ // the SAME implementation, which is a stronger parity guarantee than four ports.
392
+ //
393
+ // Delegation is ALLOW-LISTED, never blind. The client forwards ITS unknown
394
+ // commands to the framework CLI, so a framework that forwarded its unknowns back
395
+ // would ping-pong an unknown command between two processes forever — and that is
396
+ // not hypothetical here: this package publishes a `tina4` bin alias, so `tina4`
397
+ // on PATH can already resolve to THIS CLI. The closed DELEGATED set contains only
398
+ // commands the client dispatches natively, so no loop is possible by
399
+ // construction, and a real typo still gets "Unknown command".
400
+ //
401
+ // There are no handlers here: main() runs `tina4 <name> <args...>` and exits with
402
+ // its code. Keep this set closed and identical in all four frameworks. Summaries
403
+ // are the client's own wording, verbatim. Mirrors the Python master's DELEGATED.
404
+
405
+ export interface DelegatedSpec {
406
+ summary: string;
407
+ usage?: string;
408
+ args?: string[];
409
+ }
410
+
411
+ export const DELEGATED: Record<string, DelegatedSpec> = {
412
+ doctor: { summary: "Check installed languages and tools" },
413
+ setup: { summary: "Guided, menu-driven setup: install everything + scaffold a ready-to-run project" },
414
+ deploy: {
415
+ usage: "<docker|systemd|nginx|cpanel> [--force]",
416
+ args: ["target"],
417
+ summary: "Generate deployment scaffolding (Dockerfile, systemd unit, nginx block, cPanel)",
418
+ },
419
+ };
420
+
421
+ export const CLIENT_BINARY = "tina4";
422
+
423
+ // Internal process marker (same class as the client's own TINA4_SETUP_ELEVATED):
424
+ // set on the child so a client that resolves back to a framework CLI is caught
425
+ // instead of spawning forever. NOT user configuration — deliberately absent from
426
+ // the CLI's known_vars().
427
+ export const DELEGATION_GUARD_ENV = "TINA4_CLI_DELEGATED";
428
+
429
+ // 127 is the conventional "command not found" and covers both ways the client
430
+ // can be unreachable (absent from PATH, or the loop guard tripping).
431
+ export const EXIT_CLIENT_UNAVAILABLE = 127;
432
+ export const EXIT_UNKNOWN_COMMAND = 1;
433
+
434
+ const CLIENT_INSTALL_HINT =
435
+ " Install it: curl -fsSL https://tina4.com/install.sh | sh\n" +
436
+ " Windows: irm https://tina4.com/install.ps1 | iex";
437
+
438
+ /**
439
+ * Absolute path of the `tina4` client on PATH, or null if it isn't there.
440
+ *
441
+ * Scans PATH directly rather than shelling out to which/where — one less process
442
+ * and it behaves the same on every platform.
443
+ */
444
+ function findClient(): string | null {
445
+ const windows = process.platform === "win32";
446
+ const names = windows
447
+ ? [`${CLIENT_BINARY}.exe`, `${CLIENT_BINARY}.cmd`, `${CLIENT_BINARY}.bat`]
448
+ : [CLIENT_BINARY];
449
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
450
+ if (!dir) continue;
451
+ for (const name of names) {
452
+ const candidate = join(dir, name);
453
+ try {
454
+ if (statSync(candidate).isFile()) return candidate;
455
+ } catch {
456
+ // not there — keep looking
457
+ }
458
+ }
459
+ }
460
+ return null;
461
+ }
462
+
463
+ /**
464
+ * Run `tina4 <command> <args...>`, returning the client's exit code.
465
+ *
466
+ * Returns EXIT_CLIENT_UNAVAILABLE (127) with an actionable message when the
467
+ * client is not on PATH, or when the re-entry guard shows the resolved `tina4`
468
+ * came back to a framework CLI (a delegation loop).
469
+ */
470
+ export function delegateToClient(command: string, args: string[]): number {
471
+ if (process.env[DELEGATION_GUARD_ENV] === command) {
472
+ console.error(
473
+ ` Refusing to delegate '${command}' again — the 'tina4' on your PATH\n` +
474
+ ` resolved back to a framework CLI instead of the tina4 client.\n\n` +
475
+ ` Check which 'tina4' comes first on your PATH and put the client first.`,
476
+ );
477
+ return EXIT_CLIENT_UNAVAILABLE;
478
+ }
479
+
480
+ const client = findClient();
481
+ if (client === null) {
482
+ console.error(
483
+ ` '${command}' is provided by the tina4 client, which is not on your PATH.\n\n` +
484
+ `${CLIENT_INSTALL_HINT}\n\n` +
485
+ ` Then run: ${CLIENT_BINARY} ${command}`,
486
+ );
487
+ return EXIT_CLIENT_UNAVAILABLE;
488
+ }
489
+
490
+ // stdio is inherited, so the client's interactive prompts (setup) and colour
491
+ // output work exactly as if it had been invoked directly.
492
+ const result = spawnSync(client, [command, ...args], {
493
+ stdio: "inherit",
494
+ env: { ...process.env, [DELEGATION_GUARD_ENV]: command },
495
+ });
496
+ if (result.error) {
497
+ console.error(` Could not start the tina4 client at ${client}: ${result.error.message}`);
498
+ return EXIT_CLIENT_UNAVAILABLE;
499
+ }
500
+ return result.status ?? EXIT_CLIENT_UNAVAILABLE;
501
+ }
502
+
359
503
  // ── Dispatch ────────────────────────────────────────────────────────
360
504
 
361
505
  async function main(): Promise<void> {
@@ -374,9 +518,15 @@ async function main(): Promise<void> {
374
518
  return;
375
519
  }
376
520
 
521
+ if (command in DELEGATED) {
522
+ process.exit(delegateToClient(command, cmdArgs));
523
+ }
524
+
525
+ // A genuinely unknown command is an ERROR: exit non-zero so a typo in a script
526
+ // or CI step fails loudly instead of reporting success.
377
527
  console.error(`Unknown command: ${command}`);
378
528
  printHelp();
379
- process.exit(1);
529
+ process.exit(EXIT_UNKNOWN_COMMAND);
380
530
  }
381
531
 
382
532
  // Run only when invoked as the entrypoint — importing this module (e.g. in a