tina4-nodejs 3.13.81 → 3.13.83
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 +6 -6
- package/README.md +4 -4
- package/package.json +1 -1
- package/packages/cli/src/bin.ts +163 -13
- package/packages/core/public/css/tina4.css +56 -130
- package/packages/core/public/css/tina4.min.css +1 -1
- package/packages/core/src/devAdmin.ts +9 -11
- package/packages/core/src/index.ts +4 -0
- package/packages/core/src/mqtt.ts +859 -0
- package/packages/core/src/mqttMessage.ts +104 -0
- package/packages/core/src/scss.ts +48 -1
- package/packages/core/src/server.ts +98 -9
- package/packages/core/src/testClient.ts +72 -7
- package/packages/orm/src/adapters/firebird.ts +1 -1
- package/packages/orm/src/adapters/mssql.ts +1 -1
- package/packages/orm/src/adapters/mysql.ts +1 -1
- package/packages/orm/src/adapters/postgres.ts +1 -1
- package/packages/orm/src/adapters/sqlite.ts +1 -1
- package/packages/orm/src/baseModel.ts +1 -1
- package/packages/orm/src/cachedDatabase.ts +1 -1
- package/packages/orm/src/database.ts +1 -1
- package/packages/orm/src/index.ts +1 -1
- /package/packages/orm/src/{sqlTranslation.ts → sqlTranslator.ts} +0 -0
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.83)
|
|
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.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.83 - 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
|
-
|
|
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
|
-
- `
|
|
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/
|
|
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
|
-
- **
|
|
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">
|
|
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-
|
|
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
|
|
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
|
|
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
package/packages/cli/src/bin.ts
CHANGED
|
@@ -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
|
|
88
|
-
* version — no bootstrap, no database, no migrations, no app imports.
|
|
89
|
-
* exactly what `commands --json` serialises and what the tina4 Rust
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|