tina4-nodejs 3.13.120 → 3.13.122
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 +2 -2
- package/package.json +2 -1
- package/packages/cli/dist/bin.js +10744 -10661
- package/packages/cli/src/bin.ts +5 -2
- package/packages/cli/src/commands/generate.ts +117 -19
- package/packages/cli/src/commands/migrateCreate.ts +32 -37
- package/packages/core/dist/index.js +2421 -339
- package/packages/core/src/mcp.ts +62 -11
- package/packages/core/src/middleware.ts +1156 -1120
- package/packages/orm/dist/index.js +2470 -388
- package/types/cli/src/commands/generate.d.ts +16 -0
- package/types/cli/src/commands/migrateCreate.d.ts +1 -1
- package/types/core/src/middleware.d.ts +15 -0
|
@@ -1831,15 +1831,15 @@ function findOutsideQuotes(expr, needle) {
|
|
|
1831
1831
|
}
|
|
1832
1832
|
return -1;
|
|
1833
1833
|
}
|
|
1834
|
-
function splitOutsideQuotes(expr,
|
|
1835
|
-
if (!expr.includes(
|
|
1834
|
+
function splitOutsideQuotes(expr, sep7) {
|
|
1835
|
+
if (!expr.includes(sep7)) return [expr];
|
|
1836
1836
|
const parts = [];
|
|
1837
1837
|
let currentStart = 0;
|
|
1838
1838
|
let inQuote = null;
|
|
1839
1839
|
let depth = 0;
|
|
1840
1840
|
let bracketDepth = 0;
|
|
1841
1841
|
let i = 0;
|
|
1842
|
-
const sepLen =
|
|
1842
|
+
const sepLen = sep7.length;
|
|
1843
1843
|
const lastStart = expr.length - sepLen;
|
|
1844
1844
|
while (i <= lastStart) {
|
|
1845
1845
|
const ch = expr[i];
|
|
@@ -1860,7 +1860,7 @@ function splitOutsideQuotes(expr, sep6) {
|
|
|
1860
1860
|
else if (ch === ")") depth--;
|
|
1861
1861
|
else if (ch === "[") bracketDepth++;
|
|
1862
1862
|
else if (ch === "]") bracketDepth--;
|
|
1863
|
-
if (depth === 0 && bracketDepth === 0 && expr.startsWith(
|
|
1863
|
+
if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep7, i)) {
|
|
1864
1864
|
parts.push(expr.slice(currentStart, i));
|
|
1865
1865
|
i += sepLen;
|
|
1866
1866
|
currentStart = i;
|
|
@@ -2636,8 +2636,8 @@ var init_engine = __esm({
|
|
|
2636
2636
|
},
|
|
2637
2637
|
first: (v) => Array.isArray(v) ? v[0] ?? null : null,
|
|
2638
2638
|
last: (v) => Array.isArray(v) ? v[v.length - 1] ?? null : null,
|
|
2639
|
-
join: (v,
|
|
2640
|
-
split: (v,
|
|
2639
|
+
join: (v, sep7) => Array.isArray(v) ? v.map(String).join(sep7 !== void 0 ? String(sep7) : ", ") : String(v),
|
|
2640
|
+
split: (v, sep7) => String(v).split(sep7 !== void 0 ? String(sep7) : " "),
|
|
2641
2641
|
replace: (v, from, to) => {
|
|
2642
2642
|
const s = String(v);
|
|
2643
2643
|
if (from !== void 0 && typeof from === "object" && from !== null && !Array.isArray(from)) {
|
|
@@ -5338,6 +5338,9 @@ var init_middleware = __esm({
|
|
|
5338
5338
|
`max-age=${hsts}; includeSubDomains`
|
|
5339
5339
|
);
|
|
5340
5340
|
}
|
|
5341
|
+
if (process.env.TINA4_CSP === void 0) {
|
|
5342
|
+
_SecurityHeadersMiddleware.warnCspDefaultOnce();
|
|
5343
|
+
}
|
|
5341
5344
|
res.header(
|
|
5342
5345
|
"Content-Security-Policy",
|
|
5343
5346
|
process.env.TINA4_CSP ?? "default-src 'self'"
|
|
@@ -5353,6 +5356,30 @@ var init_middleware = __esm({
|
|
|
5353
5356
|
);
|
|
5354
5357
|
return [req2, res];
|
|
5355
5358
|
}
|
|
5359
|
+
/** Warn-once ledger for the default-CSP heads-up (per process). */
|
|
5360
|
+
static cspDefaultWarned = false;
|
|
5361
|
+
/**
|
|
5362
|
+
* Warn once per process that the default CSP is in force (TINA4_CSP unset).
|
|
5363
|
+
*
|
|
5364
|
+
* Secure-by-default keeps `default-src 'self'` (SECHDR-DEC-01), but that
|
|
5365
|
+
* default is invisible: it blocks runtime-injected inline styles, cross-origin
|
|
5366
|
+
* fonts/scripts/CDNs, `data:` URIs, and cross-origin WebSocket/XHR (a separate
|
|
5367
|
+
* API or LiveKit host) — and the failure surfaces only in the browser at
|
|
5368
|
+
* runtime, long after a deploy has gone green. So the framework says so once,
|
|
5369
|
+
* naming the escape hatch. It NEVER fails the boot or a request — logging a
|
|
5370
|
+
* heads-up must not be the reason the server or a request dies. Fires only when
|
|
5371
|
+
* TINA4_CSP is ABSENT; setting it (even to empty) is an explicit opt-in.
|
|
5372
|
+
*/
|
|
5373
|
+
static warnCspDefaultOnce() {
|
|
5374
|
+
if (_SecurityHeadersMiddleware.cspDefaultWarned) return;
|
|
5375
|
+
_SecurityHeadersMiddleware.cspDefaultWarned = true;
|
|
5376
|
+
const message = `TINA4_CSP is not set, so Tina4 is serving the default Content-Security-Policy "default-src 'self'" on every response. That default blocks runtime-injected inline styles, cross-origin fonts/scripts/CDNs, data: URIs, and cross-origin WebSocket/XHR (e.g. a separate API or LiveKit host). If your app uses any of these, set TINA4_CSP to a policy that allows them (see https://tina4.com); to silence this notice without changing behaviour, set TINA4_CSP="default-src 'self'".`;
|
|
5377
|
+
try {
|
|
5378
|
+
Log.warning(message);
|
|
5379
|
+
} catch {
|
|
5380
|
+
console.warn(message);
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5356
5383
|
/**
|
|
5357
5384
|
* True when the client request is HTTPS. Proxy-aware and byte-parity with
|
|
5358
5385
|
* Python (request.is_secure_scheme), PHP (Request::isSecureScheme) and Ruby
|
|
@@ -7753,7 +7780,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
|
|
|
7753
7780
|
const elapsedMs = () => performance.now() - startedAt;
|
|
7754
7781
|
if (budgetMs === null) return attempt();
|
|
7755
7782
|
const started = attempt();
|
|
7756
|
-
return new Promise((
|
|
7783
|
+
return new Promise((resolve21, reject) => {
|
|
7757
7784
|
let expired = false;
|
|
7758
7785
|
const timer = setTimeout(() => {
|
|
7759
7786
|
expired = true;
|
|
@@ -7763,7 +7790,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
|
|
|
7763
7790
|
(arrived) => {
|
|
7764
7791
|
clearTimeout(timer);
|
|
7765
7792
|
if (expired) abandon?.(arrived);
|
|
7766
|
-
else
|
|
7793
|
+
else resolve21(arrived);
|
|
7767
7794
|
},
|
|
7768
7795
|
(failure) => {
|
|
7769
7796
|
clearTimeout(timer);
|
|
@@ -8352,10 +8379,10 @@ var init_mysql = __esm({
|
|
|
8352
8379
|
...timeoutOption
|
|
8353
8380
|
});
|
|
8354
8381
|
}
|
|
8355
|
-
return new Promise((
|
|
8382
|
+
return new Promise((resolve21, reject) => {
|
|
8356
8383
|
this.connection.connect((err) => {
|
|
8357
8384
|
if (err) reject(err);
|
|
8358
|
-
else
|
|
8385
|
+
else resolve21();
|
|
8359
8386
|
});
|
|
8360
8387
|
});
|
|
8361
8388
|
},
|
|
@@ -8377,10 +8404,10 @@ var init_mysql = __esm({
|
|
|
8377
8404
|
}
|
|
8378
8405
|
}
|
|
8379
8406
|
queryPromise(sql, params) {
|
|
8380
|
-
return new Promise((
|
|
8407
|
+
return new Promise((resolve21, reject) => {
|
|
8381
8408
|
this.connection.query(sql, params ?? [], (err, results) => {
|
|
8382
8409
|
if (err) reject(err);
|
|
8383
|
-
else
|
|
8410
|
+
else resolve21(results);
|
|
8384
8411
|
});
|
|
8385
8412
|
});
|
|
8386
8413
|
}
|
|
@@ -8766,11 +8793,11 @@ var init_mssql = __esm({
|
|
|
8766
8793
|
};
|
|
8767
8794
|
}
|
|
8768
8795
|
await withConnectTimeout(
|
|
8769
|
-
() => new Promise((
|
|
8796
|
+
() => new Promise((resolve21, reject) => {
|
|
8770
8797
|
this.connection = new Connection(tediousConfig);
|
|
8771
8798
|
this.connection.on("connect", (err) => {
|
|
8772
8799
|
if (err) reject(err);
|
|
8773
|
-
else
|
|
8800
|
+
else resolve21();
|
|
8774
8801
|
});
|
|
8775
8802
|
this.connection.connect();
|
|
8776
8803
|
}),
|
|
@@ -8815,11 +8842,11 @@ var init_mssql = __esm({
|
|
|
8815
8842
|
const tediousModule = requireTedious();
|
|
8816
8843
|
const Request = tediousModule.Request;
|
|
8817
8844
|
const TYPES = tediousModule.TYPES;
|
|
8818
|
-
return new Promise((
|
|
8845
|
+
return new Promise((resolve21, reject) => {
|
|
8819
8846
|
const rows = [];
|
|
8820
8847
|
const request = new Request(sql, (err, rowCount) => {
|
|
8821
8848
|
if (err) reject(err);
|
|
8822
|
-
else
|
|
8849
|
+
else resolve21({ rows, rowCount });
|
|
8823
8850
|
});
|
|
8824
8851
|
if (params) {
|
|
8825
8852
|
params.forEach((p, i) => {
|
|
@@ -9021,8 +9048,8 @@ var init_mssql = __esm({
|
|
|
9021
9048
|
throw new Error("Use startTransactionAsync() for MSSQL.");
|
|
9022
9049
|
}
|
|
9023
9050
|
async startTransactionAsync() {
|
|
9024
|
-
await new Promise((
|
|
9025
|
-
this.connection.beginTransaction((err) => err ? reject(err) :
|
|
9051
|
+
await new Promise((resolve21, reject) => {
|
|
9052
|
+
this.connection.beginTransaction((err) => err ? reject(err) : resolve21());
|
|
9026
9053
|
});
|
|
9027
9054
|
this._inTransaction = true;
|
|
9028
9055
|
}
|
|
@@ -9030,8 +9057,8 @@ var init_mssql = __esm({
|
|
|
9030
9057
|
throw new Error("Use commitAsync() for MSSQL.");
|
|
9031
9058
|
}
|
|
9032
9059
|
async commitAsync() {
|
|
9033
|
-
await new Promise((
|
|
9034
|
-
this.connection.commitTransaction((err) => err ? reject(err) :
|
|
9060
|
+
await new Promise((resolve21, reject) => {
|
|
9061
|
+
this.connection.commitTransaction((err) => err ? reject(err) : resolve21());
|
|
9035
9062
|
});
|
|
9036
9063
|
this._inTransaction = false;
|
|
9037
9064
|
}
|
|
@@ -9039,8 +9066,8 @@ var init_mssql = __esm({
|
|
|
9039
9066
|
throw new Error("Use rollbackAsync() for MSSQL.");
|
|
9040
9067
|
}
|
|
9041
9068
|
async rollbackAsync() {
|
|
9042
|
-
await new Promise((
|
|
9043
|
-
this.connection.rollbackTransaction((err) => err ? reject(err) :
|
|
9069
|
+
await new Promise((resolve21, reject) => {
|
|
9070
|
+
this.connection.rollbackTransaction((err) => err ? reject(err) : resolve21());
|
|
9044
9071
|
});
|
|
9045
9072
|
this._inTransaction = false;
|
|
9046
9073
|
}
|
|
@@ -9365,8 +9392,8 @@ var init_firebird = __esm({
|
|
|
9365
9392
|
}
|
|
9366
9393
|
attachOnce(config) {
|
|
9367
9394
|
const fb = requireFirebird();
|
|
9368
|
-
return new Promise((
|
|
9369
|
-
fb.attach(config, (err, db) => err ? reject(err) :
|
|
9395
|
+
return new Promise((resolve21, reject) => {
|
|
9396
|
+
fb.attach(config, (err, db) => err ? reject(err) : resolve21(db));
|
|
9370
9397
|
});
|
|
9371
9398
|
}
|
|
9372
9399
|
/**
|
|
@@ -9385,7 +9412,7 @@ var init_firebird = __esm({
|
|
|
9385
9412
|
} catch (err) {
|
|
9386
9413
|
lastError = err;
|
|
9387
9414
|
if (attempt < attempts - 1) {
|
|
9388
|
-
await new Promise((
|
|
9415
|
+
await new Promise((resolve21) => setTimeout(resolve21, 100 * (attempt + 1)));
|
|
9389
9416
|
}
|
|
9390
9417
|
}
|
|
9391
9418
|
}
|
|
@@ -9468,19 +9495,19 @@ var init_firebird = __esm({
|
|
|
9468
9495
|
}
|
|
9469
9496
|
queryPromise(sql, params) {
|
|
9470
9497
|
const translated = this.translateSql(sql);
|
|
9471
|
-
return this.withReconnect(() => new Promise((
|
|
9498
|
+
return this.withReconnect(() => new Promise((resolve21, reject) => {
|
|
9472
9499
|
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
9473
9500
|
if (err) reject(err);
|
|
9474
|
-
else
|
|
9501
|
+
else resolve21(result ?? []);
|
|
9475
9502
|
});
|
|
9476
9503
|
}));
|
|
9477
9504
|
}
|
|
9478
9505
|
executePromise(sql, params) {
|
|
9479
9506
|
const translated = this.translateSql(sql);
|
|
9480
|
-
return this.withReconnect(() => new Promise((
|
|
9507
|
+
return this.withReconnect(() => new Promise((resolve21, reject) => {
|
|
9481
9508
|
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
9482
9509
|
if (err) reject(err);
|
|
9483
|
-
else
|
|
9510
|
+
else resolve21();
|
|
9484
9511
|
});
|
|
9485
9512
|
}));
|
|
9486
9513
|
}
|
|
@@ -9519,13 +9546,13 @@ var init_firebird = __esm({
|
|
|
9519
9546
|
* and no bytes round-tripped (FB-BLOB-SRP-UNVERIFIED).
|
|
9520
9547
|
*/
|
|
9521
9548
|
readBlob(blobFn) {
|
|
9522
|
-
return new Promise((
|
|
9549
|
+
return new Promise((resolve21, reject) => {
|
|
9523
9550
|
blobFn((err, _name, emitter) => {
|
|
9524
9551
|
if (err) return reject(err);
|
|
9525
|
-
if (!emitter) return
|
|
9552
|
+
if (!emitter) return resolve21(null);
|
|
9526
9553
|
const chunks = [];
|
|
9527
9554
|
emitter.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
9528
|
-
emitter.on("end", () =>
|
|
9555
|
+
emitter.on("end", () => resolve21(Buffer.concat(chunks)));
|
|
9529
9556
|
emitter.on("error", (streamErr) => reject(streamErr));
|
|
9530
9557
|
});
|
|
9531
9558
|
});
|
|
@@ -9661,12 +9688,12 @@ var init_firebird = __esm({
|
|
|
9661
9688
|
}
|
|
9662
9689
|
async startTransactionAsync() {
|
|
9663
9690
|
this.ensureConnected();
|
|
9664
|
-
await new Promise((
|
|
9691
|
+
await new Promise((resolve21, reject) => {
|
|
9665
9692
|
this.db.transaction(0, (err, transaction) => {
|
|
9666
9693
|
if (err) reject(err);
|
|
9667
9694
|
else {
|
|
9668
9695
|
this.transaction = transaction;
|
|
9669
|
-
|
|
9696
|
+
resolve21();
|
|
9670
9697
|
}
|
|
9671
9698
|
});
|
|
9672
9699
|
});
|
|
@@ -9676,12 +9703,12 @@ var init_firebird = __esm({
|
|
|
9676
9703
|
}
|
|
9677
9704
|
async commitAsync() {
|
|
9678
9705
|
if (!this.transaction) throw new Error("No active transaction to commit.");
|
|
9679
|
-
await new Promise((
|
|
9706
|
+
await new Promise((resolve21, reject) => {
|
|
9680
9707
|
this.transaction.commit((err) => {
|
|
9681
9708
|
if (err) reject(err);
|
|
9682
9709
|
else {
|
|
9683
9710
|
this.transaction = null;
|
|
9684
|
-
|
|
9711
|
+
resolve21();
|
|
9685
9712
|
}
|
|
9686
9713
|
});
|
|
9687
9714
|
});
|
|
@@ -9691,12 +9718,12 @@ var init_firebird = __esm({
|
|
|
9691
9718
|
}
|
|
9692
9719
|
async rollbackAsync() {
|
|
9693
9720
|
if (!this.transaction) throw new Error("No active transaction to rollback.");
|
|
9694
|
-
await new Promise((
|
|
9721
|
+
await new Promise((resolve21, reject) => {
|
|
9695
9722
|
this.transaction.rollback((err) => {
|
|
9696
9723
|
if (err) reject(err);
|
|
9697
9724
|
else {
|
|
9698
9725
|
this.transaction = null;
|
|
9699
|
-
|
|
9726
|
+
resolve21();
|
|
9700
9727
|
}
|
|
9701
9728
|
});
|
|
9702
9729
|
});
|
|
@@ -12845,7 +12872,7 @@ async function createMigration(description, options) {
|
|
|
12845
12872
|
}
|
|
12846
12873
|
const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
12847
12874
|
const now = /* @__PURE__ */ new Date();
|
|
12848
|
-
const
|
|
12875
|
+
const timestamp2 = [
|
|
12849
12876
|
now.getFullYear(),
|
|
12850
12877
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
12851
12878
|
String(now.getDate()).padStart(2, "0"),
|
|
@@ -12853,8 +12880,8 @@ async function createMigration(description, options) {
|
|
|
12853
12880
|
String(now.getMinutes()).padStart(2, "0"),
|
|
12854
12881
|
String(now.getSeconds()).padStart(2, "0")
|
|
12855
12882
|
].join("");
|
|
12856
|
-
const upFileName = `${
|
|
12857
|
-
const downFileName = `${
|
|
12883
|
+
const upFileName = `${timestamp2}_${safeName}.sql`;
|
|
12884
|
+
const downFileName = `${timestamp2}_${safeName}.down.sql`;
|
|
12858
12885
|
const upPath = join7(dir, upFileName);
|
|
12859
12886
|
const downPath = join7(dir, downFileName);
|
|
12860
12887
|
const upTemplate = `-- Migration: ${description}
|
|
@@ -12877,7 +12904,7 @@ async function createClassMigration(description, options) {
|
|
|
12877
12904
|
const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
12878
12905
|
const className = description.replace(/[^a-zA-Z0-9 ]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
12879
12906
|
const now = /* @__PURE__ */ new Date();
|
|
12880
|
-
const
|
|
12907
|
+
const timestamp2 = [
|
|
12881
12908
|
now.getFullYear(),
|
|
12882
12909
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
12883
12910
|
String(now.getDate()).padStart(2, "0"),
|
|
@@ -12885,7 +12912,7 @@ async function createClassMigration(description, options) {
|
|
|
12885
12912
|
String(now.getMinutes()).padStart(2, "0"),
|
|
12886
12913
|
String(now.getSeconds()).padStart(2, "0")
|
|
12887
12914
|
].join("");
|
|
12888
|
-
const fileName = `${
|
|
12915
|
+
const fileName = `${timestamp2}_${safeName}.ts`;
|
|
12889
12916
|
const filePath = join7(dir, fileName);
|
|
12890
12917
|
const content = `// Migration: ${description}
|
|
12891
12918
|
// Created: ${now.toISOString()}
|
|
@@ -18802,7 +18829,7 @@ ${s}\r
|
|
|
18802
18829
|
connect() {
|
|
18803
18830
|
if (this.connected) return Promise.resolve();
|
|
18804
18831
|
if (this.connecting) return this.connecting;
|
|
18805
|
-
this.connecting = new Promise((
|
|
18832
|
+
this.connecting = new Promise((resolve21, reject) => {
|
|
18806
18833
|
const sock = net.createConnection({ host: this.host, port: this.port });
|
|
18807
18834
|
sock.setNoDelay(true);
|
|
18808
18835
|
const onError = (err) => {
|
|
@@ -18839,7 +18866,7 @@ ${s}\r
|
|
|
18839
18866
|
sock.on("error", (e) => {
|
|
18840
18867
|
this.brokenError = e;
|
|
18841
18868
|
});
|
|
18842
|
-
|
|
18869
|
+
resolve21();
|
|
18843
18870
|
} catch (e) {
|
|
18844
18871
|
onError(e);
|
|
18845
18872
|
}
|
|
@@ -18902,12 +18929,12 @@ ${s}\r
|
|
|
18902
18929
|
}
|
|
18903
18930
|
/** Send one command and await its reply (assumes socket is up). */
|
|
18904
18931
|
raw(args) {
|
|
18905
|
-
return new Promise((
|
|
18932
|
+
return new Promise((resolve21, reject) => {
|
|
18906
18933
|
if (!this.sock || this.sock.destroyed) {
|
|
18907
18934
|
reject(this.brokenError ?? new Error("redis socket not connected"));
|
|
18908
18935
|
return;
|
|
18909
18936
|
}
|
|
18910
|
-
this.waiters.push({ resolve:
|
|
18937
|
+
this.waiters.push({ resolve: resolve21, reject });
|
|
18911
18938
|
this.sock.write(_RespClient.encode(args));
|
|
18912
18939
|
});
|
|
18913
18940
|
}
|
|
@@ -19249,7 +19276,7 @@ ${s}\r
|
|
|
19249
19276
|
connect() {
|
|
19250
19277
|
if (this.connected) return Promise.resolve();
|
|
19251
19278
|
if (this.connecting) return this.connecting;
|
|
19252
|
-
this.connecting = new Promise((
|
|
19279
|
+
this.connecting = new Promise((resolve21, reject) => {
|
|
19253
19280
|
const sock = net.createConnection({ host: this.host, port: this.port });
|
|
19254
19281
|
sock.setNoDelay(true);
|
|
19255
19282
|
sock.once("error", (err) => {
|
|
@@ -19270,7 +19297,7 @@ ${s}\r
|
|
|
19270
19297
|
p.resolve(this.buffer.toString("utf-8"));
|
|
19271
19298
|
}
|
|
19272
19299
|
});
|
|
19273
|
-
|
|
19300
|
+
resolve21();
|
|
19274
19301
|
});
|
|
19275
19302
|
});
|
|
19276
19303
|
return this.connecting;
|
|
@@ -19303,13 +19330,13 @@ ${s}\r
|
|
|
19303
19330
|
async send(payload, terminator) {
|
|
19304
19331
|
await this.connect();
|
|
19305
19332
|
if (!this.sock || this.sock.destroyed) return "";
|
|
19306
|
-
return new Promise((
|
|
19333
|
+
return new Promise((resolve21) => {
|
|
19307
19334
|
this.buffer = Buffer.alloc(0);
|
|
19308
|
-
this.pending = { terminator, resolve:
|
|
19335
|
+
this.pending = { terminator, resolve: resolve21 };
|
|
19309
19336
|
const timer = setTimeout(() => {
|
|
19310
|
-
if (this.pending && this.pending.resolve ===
|
|
19337
|
+
if (this.pending && this.pending.resolve === resolve21) {
|
|
19311
19338
|
this.pending = null;
|
|
19312
|
-
|
|
19339
|
+
resolve21(this.buffer.toString("utf-8"));
|
|
19313
19340
|
}
|
|
19314
19341
|
}, 4e3);
|
|
19315
19342
|
if (timer.unref) timer.unref();
|
|
@@ -21075,7 +21102,7 @@ async function parseBody(req2) {
|
|
|
21075
21102
|
}
|
|
21076
21103
|
const contentType = req2.headers["content-type"] ?? "";
|
|
21077
21104
|
const chunks = [];
|
|
21078
|
-
await new Promise((
|
|
21105
|
+
await new Promise((resolve21, reject) => {
|
|
21079
21106
|
let received = 0;
|
|
21080
21107
|
let refused = false;
|
|
21081
21108
|
req2.on("data", (chunk) => {
|
|
@@ -21090,7 +21117,7 @@ async function parseBody(req2) {
|
|
|
21090
21117
|
chunks.push(chunk);
|
|
21091
21118
|
});
|
|
21092
21119
|
req2.on("end", () => {
|
|
21093
|
-
if (!refused)
|
|
21120
|
+
if (!refused) resolve21();
|
|
21094
21121
|
});
|
|
21095
21122
|
req2.on("error", reject);
|
|
21096
21123
|
});
|
|
@@ -24323,6 +24350,2038 @@ var init_version = __esm({
|
|
|
24323
24350
|
}
|
|
24324
24351
|
});
|
|
24325
24352
|
|
|
24353
|
+
// ../cli/src/commands/generate.ts
|
|
24354
|
+
var generate_exports = {};
|
|
24355
|
+
__export(generate_exports, {
|
|
24356
|
+
DEFAULT_FIELDS: () => DEFAULT_FIELDS,
|
|
24357
|
+
GENERATORS: () => GENERATORS,
|
|
24358
|
+
RESOLUTION_ENVELOPE_VERSION: () => RESOLUTION_ENVELOPE_VERSION,
|
|
24359
|
+
SQL_RESERVED_TABLE_NAMES: () => SQL_RESERVED_TABLE_NAMES,
|
|
24360
|
+
aiFill: () => aiFill,
|
|
24361
|
+
currentResolution: () => currentResolution,
|
|
24362
|
+
extend: () => extend,
|
|
24363
|
+
fieldsOrDefault: () => fieldsOrDefault,
|
|
24364
|
+
generate: () => generate,
|
|
24365
|
+
generateMigration: () => generateMigration,
|
|
24366
|
+
generateProgrammatic: () => generateProgrammatic,
|
|
24367
|
+
parseCliArgs: () => parseCliArgs,
|
|
24368
|
+
parseEvery: () => parseEvery,
|
|
24369
|
+
parseFields: () => parseFields,
|
|
24370
|
+
pluralizeReserved: () => pluralizeReserved,
|
|
24371
|
+
toPascal: () => toPascal,
|
|
24372
|
+
toSnake: () => toSnake,
|
|
24373
|
+
toTableName: () => toTableName
|
|
24374
|
+
});
|
|
24375
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync9 } from "node:fs";
|
|
24376
|
+
import { join as join20, relative as relative2, resolve as resolve10, sep as sep4 } from "node:path";
|
|
24377
|
+
function ensureDir(dir) {
|
|
24378
|
+
if (__resolution.dryRun) return;
|
|
24379
|
+
if (!existsSync15(dir)) {
|
|
24380
|
+
mkdirSync12(dir, { recursive: true });
|
|
24381
|
+
}
|
|
24382
|
+
}
|
|
24383
|
+
function writeFileSafe(path8, content) {
|
|
24384
|
+
captureEditHints(path8, content);
|
|
24385
|
+
if (__resolution.dryRun) {
|
|
24386
|
+
return;
|
|
24387
|
+
}
|
|
24388
|
+
if (existsSync15(path8)) {
|
|
24389
|
+
if (!__resolution.jsonMode) console.log(` File already exists: ${path8}`);
|
|
24390
|
+
return;
|
|
24391
|
+
}
|
|
24392
|
+
writeFileSync9(path8, content, "utf-8");
|
|
24393
|
+
__resolution.actionsTaken.push(`wrote ${path8}`);
|
|
24394
|
+
if (!__resolution.jsonMode) console.log(` Created ${path8}`);
|
|
24395
|
+
}
|
|
24396
|
+
function toSnake(name) {
|
|
24397
|
+
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
24398
|
+
}
|
|
24399
|
+
function pluralizeReserved(name) {
|
|
24400
|
+
if (name.endsWith("y") && !/[aeiouy]y$/i.test(name)) return name.slice(0, -1) + "ies";
|
|
24401
|
+
if (/(s|x|z|ch|sh)$/.test(name)) return name + "es";
|
|
24402
|
+
return name + "s";
|
|
24403
|
+
}
|
|
24404
|
+
function toTableName(name) {
|
|
24405
|
+
const raw = toSnake(name);
|
|
24406
|
+
if (SQL_RESERVED_TABLE_NAMES.has(raw)) {
|
|
24407
|
+
const safe = pluralizeReserved(raw);
|
|
24408
|
+
recordTransformation({
|
|
24409
|
+
kind: "reserved_word_pluralize",
|
|
24410
|
+
from: raw,
|
|
24411
|
+
to: safe,
|
|
24412
|
+
reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
|
|
24413
|
+
override: `--table ${raw} --quote (requires quoted-identifier mode, not yet implemented)`
|
|
24414
|
+
});
|
|
24415
|
+
return safe;
|
|
24416
|
+
}
|
|
24417
|
+
return raw;
|
|
24418
|
+
}
|
|
24419
|
+
function resetResolution(target, input, opts) {
|
|
24420
|
+
__resolution.target = target;
|
|
24421
|
+
__resolution.input = input;
|
|
24422
|
+
__resolution.body = { transformations: [] };
|
|
24423
|
+
__resolution.actionsTaken = [];
|
|
24424
|
+
__resolution.dryRun = opts.dryRun;
|
|
24425
|
+
__resolution.jsonMode = opts.jsonMode;
|
|
24426
|
+
}
|
|
24427
|
+
function recordTransformation(t) {
|
|
24428
|
+
__resolution.body.transformations.push(t);
|
|
24429
|
+
}
|
|
24430
|
+
function currentResolution() {
|
|
24431
|
+
const body = {
|
|
24432
|
+
...__resolution.body,
|
|
24433
|
+
transformations: [...__resolution.body.transformations]
|
|
24434
|
+
};
|
|
24435
|
+
if (__resolution.body.edit_hints) {
|
|
24436
|
+
body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
|
|
24437
|
+
}
|
|
24438
|
+
if (__resolution.body.next) {
|
|
24439
|
+
body.next = [...__resolution.body.next];
|
|
24440
|
+
}
|
|
24441
|
+
if (__resolution.body.test_paths) {
|
|
24442
|
+
body.test_paths = [...__resolution.body.test_paths];
|
|
24443
|
+
}
|
|
24444
|
+
if (__resolution.body.routes) {
|
|
24445
|
+
body.routes = [...__resolution.body.routes];
|
|
24446
|
+
}
|
|
24447
|
+
return {
|
|
24448
|
+
command: "generate",
|
|
24449
|
+
target: __resolution.target,
|
|
24450
|
+
input: { ...__resolution.input },
|
|
24451
|
+
resolution: body,
|
|
24452
|
+
actions_taken: [...__resolution.actionsTaken],
|
|
24453
|
+
dry_run: __resolution.dryRun
|
|
24454
|
+
};
|
|
24455
|
+
}
|
|
24456
|
+
function setResolutionField(key, value) {
|
|
24457
|
+
__resolution.body[key] = value;
|
|
24458
|
+
}
|
|
24459
|
+
function pushRoute(routePattern) {
|
|
24460
|
+
if (!__resolution.body.routes) __resolution.body.routes = [];
|
|
24461
|
+
__resolution.body.routes.push(routePattern);
|
|
24462
|
+
}
|
|
24463
|
+
function pushTestPath(path8) {
|
|
24464
|
+
if (!__resolution.body.test_paths) __resolution.body.test_paths = [];
|
|
24465
|
+
__resolution.body.test_paths.push(path8);
|
|
24466
|
+
}
|
|
24467
|
+
function pushEditHint(hint) {
|
|
24468
|
+
if (!__resolution.body.edit_hints) __resolution.body.edit_hints = [];
|
|
24469
|
+
__resolution.body.edit_hints.push(hint);
|
|
24470
|
+
}
|
|
24471
|
+
function setNextSteps(steps) {
|
|
24472
|
+
if (steps.length === 0) return;
|
|
24473
|
+
__resolution.body.next = [...steps];
|
|
24474
|
+
}
|
|
24475
|
+
function toRelPath(absPath) {
|
|
24476
|
+
const cwd = process.cwd();
|
|
24477
|
+
const rel = relative2(cwd, absPath);
|
|
24478
|
+
if (!rel) return absPath;
|
|
24479
|
+
return sep4 === "/" ? rel : rel.split(sep4).join("/");
|
|
24480
|
+
}
|
|
24481
|
+
function captureEditHints(absPath, content) {
|
|
24482
|
+
if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
|
|
24483
|
+
const relPath = toRelPath(absPath);
|
|
24484
|
+
const lines = content.split("\n");
|
|
24485
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24486
|
+
const match = TINA4_EDIT_MARKER.exec(lines[i]);
|
|
24487
|
+
if (match) {
|
|
24488
|
+
pushEditHint({ file: relPath, line: i + 1, label: match[1].trim() });
|
|
24489
|
+
}
|
|
24490
|
+
}
|
|
24491
|
+
}
|
|
24492
|
+
function printResolution() {
|
|
24493
|
+
if (__resolution.jsonMode) {
|
|
24494
|
+
process.stdout.write(JSON.stringify(currentResolution(), null, 2) + "\n");
|
|
24495
|
+
return;
|
|
24496
|
+
}
|
|
24497
|
+
const b = __resolution.body;
|
|
24498
|
+
const lines = [];
|
|
24499
|
+
lines.push("");
|
|
24500
|
+
lines.push(`Generated ${__resolution.target} ${__resolution.input.name}`);
|
|
24501
|
+
if (b.class_name || b.file_path) {
|
|
24502
|
+
const where = b.file_path ? ` (in ${b.file_path})` : "";
|
|
24503
|
+
lines.push(` class ${b.class_name ?? __resolution.input.name}${where}`);
|
|
24504
|
+
}
|
|
24505
|
+
if (b.table_name) {
|
|
24506
|
+
const t = b.transformations.find((x) => x.kind === "reserved_word_pluralize");
|
|
24507
|
+
const note = t ? ` (auto-pluralized: '${t.from}' is a SQL reserved word)` : "";
|
|
24508
|
+
lines.push(` table ${b.table_name}${note}`);
|
|
24509
|
+
}
|
|
24510
|
+
if (b.routes && b.routes.length) {
|
|
24511
|
+
lines.push(` routes ${b.routes.join(", ")}`);
|
|
24512
|
+
}
|
|
24513
|
+
if (b.migration_path) {
|
|
24514
|
+
lines.push(` migration ${b.migration_path}`);
|
|
24515
|
+
}
|
|
24516
|
+
const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
|
|
24517
|
+
if (reserved && reserved.from && reserved.override) {
|
|
24518
|
+
lines.push("");
|
|
24519
|
+
lines.push(` To keep the raw name '${reserved.from}' as the table:`);
|
|
24520
|
+
lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
|
|
24521
|
+
}
|
|
24522
|
+
if (b.test_paths && b.test_paths.length > 0) {
|
|
24523
|
+
lines.push("");
|
|
24524
|
+
lines.push(" Tests:");
|
|
24525
|
+
for (const testPath of b.test_paths) lines.push(` ${testPath}`);
|
|
24526
|
+
}
|
|
24527
|
+
if (b.edit_hints && b.edit_hints.length > 0) {
|
|
24528
|
+
lines.push("");
|
|
24529
|
+
lines.push(" Edit these lines:");
|
|
24530
|
+
for (const hint of b.edit_hints) {
|
|
24531
|
+
lines.push(` ${hint.file}:${hint.line} ${hint.label}`);
|
|
24532
|
+
}
|
|
24533
|
+
}
|
|
24534
|
+
if (b.next && b.next.length > 0) {
|
|
24535
|
+
lines.push("");
|
|
24536
|
+
lines.push(" Next:");
|
|
24537
|
+
for (const step of b.next) lines.push(` ${step}`);
|
|
24538
|
+
}
|
|
24539
|
+
lines.push("");
|
|
24540
|
+
process.stderr.write(lines.join("\n"));
|
|
24541
|
+
}
|
|
24542
|
+
function toPlural(name) {
|
|
24543
|
+
const lower = name.toLowerCase();
|
|
24544
|
+
if (lower.endsWith("s")) return lower;
|
|
24545
|
+
if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
|
|
24546
|
+
return lower + "s";
|
|
24547
|
+
}
|
|
24548
|
+
function toCamel(name) {
|
|
24549
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
24550
|
+
}
|
|
24551
|
+
function toPascal(name) {
|
|
24552
|
+
return name.split(/[^0-9a-zA-Z]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
24553
|
+
}
|
|
24554
|
+
function parseFields(fieldsStr) {
|
|
24555
|
+
if (!fieldsStr || !fieldsStr.trim()) return [];
|
|
24556
|
+
const result = [];
|
|
24557
|
+
for (const part of fieldsStr.split(",")) {
|
|
24558
|
+
const trimmed = part.trim();
|
|
24559
|
+
if (trimmed.includes(":")) {
|
|
24560
|
+
const [fname, ftype] = trimmed.split(":", 2);
|
|
24561
|
+
if (fname.trim()) result.push([fname.trim(), ftype.trim().toLowerCase()]);
|
|
24562
|
+
} else if (trimmed) {
|
|
24563
|
+
result.push([trimmed, "string"]);
|
|
24564
|
+
}
|
|
24565
|
+
}
|
|
24566
|
+
return result;
|
|
24567
|
+
}
|
|
24568
|
+
function fieldsOrDefault(fieldsStr) {
|
|
24569
|
+
const parsed = parseFields(fieldsStr);
|
|
24570
|
+
return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
24571
|
+
}
|
|
24572
|
+
function parseCliArgs(args) {
|
|
24573
|
+
const booleanFlags = /* @__PURE__ */ new Set([
|
|
24574
|
+
"no-browser",
|
|
24575
|
+
"no-reload",
|
|
24576
|
+
"production",
|
|
24577
|
+
"managed",
|
|
24578
|
+
"all",
|
|
24579
|
+
"clear",
|
|
24580
|
+
"public",
|
|
24581
|
+
"no-migration",
|
|
24582
|
+
// Suppress the co-emitted migration test (used by the migrate:create
|
|
24583
|
+
// delegation — a plain migrate:create is "just a migration, no test",
|
|
24584
|
+
// matching its pre-3.13.121 UX now that it routes through generate migration).
|
|
24585
|
+
"no-test",
|
|
24586
|
+
// Resolution transparency (Feature B, 3.13.117): both accept NO value.
|
|
24587
|
+
"json",
|
|
24588
|
+
"dry-run"
|
|
24589
|
+
]);
|
|
24590
|
+
const flags = {};
|
|
24591
|
+
const positional = [];
|
|
24592
|
+
let i = 0;
|
|
24593
|
+
while (i < args.length) {
|
|
24594
|
+
if (args[i].startsWith("--")) {
|
|
24595
|
+
const key = args[i].slice(2);
|
|
24596
|
+
if (booleanFlags.has(key)) {
|
|
24597
|
+
flags[key] = true;
|
|
24598
|
+
i += 1;
|
|
24599
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
24600
|
+
flags[key] = args[i + 1];
|
|
24601
|
+
i += 2;
|
|
24602
|
+
} else {
|
|
24603
|
+
flags[key] = true;
|
|
24604
|
+
i += 1;
|
|
24605
|
+
}
|
|
24606
|
+
} else {
|
|
24607
|
+
positional.push(args[i]);
|
|
24608
|
+
i += 1;
|
|
24609
|
+
}
|
|
24610
|
+
}
|
|
24611
|
+
return { flags, positional };
|
|
24612
|
+
}
|
|
24613
|
+
function parseEvery(every) {
|
|
24614
|
+
if (!every || every === true) return 60;
|
|
24615
|
+
const s = String(every).trim().toLowerCase();
|
|
24616
|
+
const units = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
24617
|
+
const unit = s.slice(-1);
|
|
24618
|
+
if (unit in units) {
|
|
24619
|
+
const n2 = parseFloat(s.slice(0, -1));
|
|
24620
|
+
return Number.isFinite(n2) ? Math.max(1, Math.round(n2 * units[unit])) : 60;
|
|
24621
|
+
}
|
|
24622
|
+
const n = parseFloat(s);
|
|
24623
|
+
return Number.isFinite(n) ? Math.max(1, Math.round(n)) : 60;
|
|
24624
|
+
}
|
|
24625
|
+
function aiFill(fn, spec, indent = " ") {
|
|
24626
|
+
const rule = (label) => "\u2500".repeat(Math.max(4, 46 - label.length));
|
|
24627
|
+
const lines = [`${indent}// \u2500\u2500\u2500 AI-FILL: ${fn} ${rule(fn)}`];
|
|
24628
|
+
lines.push(`${indent}// Intent: ${spec.intent}`);
|
|
24629
|
+
if (spec.given) lines.push(`${indent}// Given: ${spec.given}`);
|
|
24630
|
+
lines.push(`${indent}// Use: ${spec.use}`);
|
|
24631
|
+
if (spec.ret) lines.push(`${indent}// Return: ${spec.ret}`);
|
|
24632
|
+
lines.push(`${indent}// Ground: ${spec.ground}`);
|
|
24633
|
+
lines.push(`${indent}throw new Error(${JSON.stringify(spec.raise)}); // remove when implemented`);
|
|
24634
|
+
lines.push(`${indent}// ${"\u2500".repeat(52)}`);
|
|
24635
|
+
return lines.join("\n") + "\n";
|
|
24636
|
+
}
|
|
24637
|
+
function extend(note, hint = "", indent = " ") {
|
|
24638
|
+
let out = `${indent}// \u2500\u2500\u2500 EXTEND: ${note} ${"\u2500".repeat(Math.max(4, 46 - note.length))}
|
|
24639
|
+
`;
|
|
24640
|
+
if (hint) out += `${indent}// ${hint}
|
|
24641
|
+
`;
|
|
24642
|
+
return out;
|
|
24643
|
+
}
|
|
24644
|
+
function timestamp() {
|
|
24645
|
+
const now = /* @__PURE__ */ new Date();
|
|
24646
|
+
return now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, "0") + String(now.getDate()).padStart(2, "0") + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
|
|
24647
|
+
}
|
|
24648
|
+
function isoNow() {
|
|
24649
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
24650
|
+
}
|
|
24651
|
+
async function generate(what, name, extraArgs = []) {
|
|
24652
|
+
if (!what) {
|
|
24653
|
+
console.error(" Usage: tina4nodejs generate <what> <name> [options]");
|
|
24654
|
+
console.error(` Generators: ${GENERATOR_LIST}`);
|
|
24655
|
+
console.error(' Options: --fields "name:string,price:float" --model ModelName');
|
|
24656
|
+
console.error(" --public open a route's writes (default: secure)");
|
|
24657
|
+
console.error(' --every 5m | --cron "\u2026" service schedule');
|
|
24658
|
+
console.error(" --json emit machine-readable resolution envelope on stdout");
|
|
24659
|
+
console.error(" --dry-run report resolution without writing any files");
|
|
24660
|
+
process.exit(1);
|
|
24661
|
+
}
|
|
24662
|
+
const noNameGenerators = /* @__PURE__ */ new Set(["auth"]);
|
|
24663
|
+
if (noNameGenerators.has(what) && name.startsWith("--")) {
|
|
24664
|
+
extraArgs = [name, ...extraArgs];
|
|
24665
|
+
name = "";
|
|
24666
|
+
}
|
|
24667
|
+
if (!noNameGenerators.has(what) && !name) {
|
|
24668
|
+
console.error(` Usage: tina4nodejs generate ${what} <name> [options]`);
|
|
24669
|
+
process.exit(1);
|
|
24670
|
+
}
|
|
24671
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
24672
|
+
const jsonMode = Boolean(flags.json);
|
|
24673
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
24674
|
+
resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode });
|
|
24675
|
+
const spec = GENERATORS[what];
|
|
24676
|
+
if (spec) {
|
|
24677
|
+
spec.handler(name, flags);
|
|
24678
|
+
} else {
|
|
24679
|
+
console.error(` Unknown generator: ${what}`);
|
|
24680
|
+
console.error(` Available: ${GENERATOR_LIST}`);
|
|
24681
|
+
process.exit(1);
|
|
24682
|
+
}
|
|
24683
|
+
const nextFn = NEXT_STEPS[what];
|
|
24684
|
+
if (nextFn) {
|
|
24685
|
+
const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
|
|
24686
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
24687
|
+
}
|
|
24688
|
+
printResolution();
|
|
24689
|
+
}
|
|
24690
|
+
async function generateProgrammatic(what, name, extraArgs = []) {
|
|
24691
|
+
const spec = GENERATORS[what];
|
|
24692
|
+
if (!spec) throw new Error(`Unknown generator: ${what} (available: ${GENERATOR_LIST})`);
|
|
24693
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
24694
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
24695
|
+
resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode: true });
|
|
24696
|
+
spec.handler(name, flags);
|
|
24697
|
+
const nextFn = NEXT_STEPS[what];
|
|
24698
|
+
if (nextFn) {
|
|
24699
|
+
const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
|
|
24700
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
24701
|
+
}
|
|
24702
|
+
return currentResolution();
|
|
24703
|
+
}
|
|
24704
|
+
function generateModel(name, flags, emitTest = true) {
|
|
24705
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
24706
|
+
const table2 = toTableName(name);
|
|
24707
|
+
const dir = resolve10("src/models");
|
|
24708
|
+
ensureDir(dir);
|
|
24709
|
+
const path8 = join20(dir, `${name}.ts`);
|
|
24710
|
+
setResolutionField("class_name", name);
|
|
24711
|
+
setResolutionField("table_name", table2);
|
|
24712
|
+
setResolutionField("file_path", `src/models/${name}.ts`);
|
|
24713
|
+
pushTestPath(`tests/${table2}_model.test.ts`);
|
|
24714
|
+
const fieldLines = [
|
|
24715
|
+
` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`,
|
|
24716
|
+
` // tina4:edit add or change fields for this model (string,int,float,bool,text,datetime)`
|
|
24717
|
+
];
|
|
24718
|
+
for (const [fname, ftype] of fields) {
|
|
24719
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
24720
|
+
fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
|
|
24721
|
+
}
|
|
24722
|
+
fieldLines.push(` created_at: { type: "datetime" as const },`);
|
|
24723
|
+
const content = `import { BaseModel } from "tina4-nodejs/orm";
|
|
24724
|
+
|
|
24725
|
+
export default class ${name} extends BaseModel {
|
|
24726
|
+
static tableName = "${table2}";
|
|
24727
|
+
static fields = {
|
|
24728
|
+
${fieldLines.join("\n")}
|
|
24729
|
+
};
|
|
24730
|
+
}
|
|
24731
|
+
`;
|
|
24732
|
+
writeFileSafe(path8, content);
|
|
24733
|
+
if (!flags["no-migration"]) {
|
|
24734
|
+
generateMigration(`create_${table2}`, flags, fields, table2, false);
|
|
24735
|
+
}
|
|
24736
|
+
if (emitTest) emitModelTest(name, table2, fields);
|
|
24737
|
+
}
|
|
24738
|
+
function secureOptOut(isPublic) {
|
|
24739
|
+
return isPublic ? `export const secure = false;
|
|
24740
|
+
|
|
24741
|
+
` : "";
|
|
24742
|
+
}
|
|
24743
|
+
function generateRoute(name, flags, emitTest = true) {
|
|
24744
|
+
const routePath = name.replace(/^\//, "");
|
|
24745
|
+
const singular = routePath.endsWith("s") ? routePath.slice(0, -1) : routePath;
|
|
24746
|
+
const model = flags.model;
|
|
24747
|
+
const isPublic = Boolean(flags.public);
|
|
24748
|
+
const base = resolve10("src/routes/api", routePath);
|
|
24749
|
+
const idDir = join20(base, "[id]");
|
|
24750
|
+
ensureDir(base);
|
|
24751
|
+
ensureDir(idDir);
|
|
24752
|
+
pushRoute(`/api/${routePath}`);
|
|
24753
|
+
pushRoute(`/api/${routePath}/{id}`);
|
|
24754
|
+
if (__resolution.target === "route") {
|
|
24755
|
+
setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
|
|
24756
|
+
}
|
|
24757
|
+
const table2 = model ? toTableName(model) : "";
|
|
24758
|
+
const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
|
|
24759
|
+
` : "";
|
|
24760
|
+
const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
|
|
24761
|
+
` : "";
|
|
24762
|
+
const writeDoc = isPublic ? "Public (--public): no token required." : "Secure by default: requires a Bearer token (use --public to open).";
|
|
24763
|
+
if (model) {
|
|
24764
|
+
writeFileSafe(
|
|
24765
|
+
join20(base, "get.ts"),
|
|
24766
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24767
|
+
${modelImportBase}
|
|
24768
|
+
export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
|
|
24769
|
+
|
|
24770
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24771
|
+
// tina4:edit tune pagination defaults or add filter/sort parsing here
|
|
24772
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
24773
|
+
const limit = parseInt(req.query.limit as string) || 20;
|
|
24774
|
+
const offset = (page - 1) * limit;
|
|
24775
|
+
const rows = await ${model}.select("SELECT * FROM ${table2} LIMIT ? OFFSET ?", [limit, offset]);
|
|
24776
|
+
res.json({ data: rows.map((r) => r.toObject()), page, limit });
|
|
24777
|
+
}
|
|
24778
|
+
`
|
|
24779
|
+
);
|
|
24780
|
+
} else {
|
|
24781
|
+
writeFileSafe(
|
|
24782
|
+
join20(base, "get.ts"),
|
|
24783
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24784
|
+
|
|
24785
|
+
export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
|
|
24786
|
+
|
|
24787
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24788
|
+
${aiFill(`list_${routePath}`, {
|
|
24789
|
+
intent: `return the ${routePath} collection (add pagination if it grows)`,
|
|
24790
|
+
given: "req.query -> filters/paging",
|
|
24791
|
+
use: `Model.select("SELECT \u2026 LIMIT ? OFFSET ?", [limit, offset]) then r.toObject()`,
|
|
24792
|
+
ret: "res.json({ data: rows })",
|
|
24793
|
+
ground: `tina4_context("list ORM records with pagination", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24794
|
+
raise: `${routePath} list not implemented`
|
|
24795
|
+
})}}
|
|
24796
|
+
`
|
|
24797
|
+
);
|
|
24798
|
+
}
|
|
24799
|
+
if (model) {
|
|
24800
|
+
writeFileSafe(
|
|
24801
|
+
join20(base, "post.ts"),
|
|
24802
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24803
|
+
${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
|
|
24804
|
+
|
|
24805
|
+
// ${writeDoc}
|
|
24806
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24807
|
+
// tina4:edit validate the body before persist (Validator or hand-checks)
|
|
24808
|
+
${extend(
|
|
24809
|
+
"validate / business rules before persist",
|
|
24810
|
+
`e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`
|
|
24811
|
+
)} const item = new ${model}(req.body as Record<string, unknown>);
|
|
24812
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
24813
|
+
// write is reported to the client as a 201 carrying unsaved data.
|
|
24814
|
+
if ((await item.save()) === false) {
|
|
24815
|
+
res.json({ error: "Could not create ${singular}" }, 400);
|
|
24816
|
+
return;
|
|
24817
|
+
}
|
|
24818
|
+
res.json({ data: item.toObject() }, 201);
|
|
24819
|
+
}
|
|
24820
|
+
`
|
|
24821
|
+
);
|
|
24822
|
+
} else {
|
|
24823
|
+
writeFileSafe(
|
|
24824
|
+
join20(base, "post.ts"),
|
|
24825
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24826
|
+
|
|
24827
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
|
|
24828
|
+
|
|
24829
|
+
// ${writeDoc}
|
|
24830
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24831
|
+
// tina4:edit fill the create handler (see AI-FILL fill-spec below)
|
|
24832
|
+
${aiFill(`create_${singular}`, {
|
|
24833
|
+
intent: `validate the body and persist a new ${singular}`,
|
|
24834
|
+
given: "req.body -> the posted fields",
|
|
24835
|
+
use: "new Model(req.body).save() then item.toObject() (import your model)",
|
|
24836
|
+
ret: "res.json({ data: item }, 201)",
|
|
24837
|
+
ground: `tina4_context("create ORM record and return 201", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24838
|
+
raise: `create ${singular} not implemented`
|
|
24839
|
+
})}}
|
|
24840
|
+
`
|
|
24841
|
+
);
|
|
24842
|
+
}
|
|
24843
|
+
if (model) {
|
|
24844
|
+
writeFileSafe(
|
|
24845
|
+
join20(idDir, "get.ts"),
|
|
24846
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24847
|
+
${modelImportId}
|
|
24848
|
+
export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
|
|
24849
|
+
|
|
24850
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24851
|
+
const { id } = req.params;
|
|
24852
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
24853
|
+
if (!item) {
|
|
24854
|
+
res.json({ error: "Not found" }, 404);
|
|
24855
|
+
return;
|
|
24856
|
+
}
|
|
24857
|
+
res.json({ data: item.toObject() });
|
|
24858
|
+
}
|
|
24859
|
+
`
|
|
24860
|
+
);
|
|
24861
|
+
} else {
|
|
24862
|
+
writeFileSafe(
|
|
24863
|
+
join20(idDir, "get.ts"),
|
|
24864
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24865
|
+
|
|
24866
|
+
export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
|
|
24867
|
+
|
|
24868
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24869
|
+
${aiFill(`get_${singular}`, {
|
|
24870
|
+
intent: `fetch one ${singular} by id`,
|
|
24871
|
+
given: "req.params.id -> the record id",
|
|
24872
|
+
use: `Model.selectOne("SELECT \u2026 WHERE id = ?", [req.params.id])`,
|
|
24873
|
+
ret: "res.json({ data: item }) or res.json({ error: 'Not found' }, 404)",
|
|
24874
|
+
ground: `tina4_context("find ORM record by id", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24875
|
+
raise: `get ${singular} not implemented`
|
|
24876
|
+
})}}
|
|
24877
|
+
`
|
|
24878
|
+
);
|
|
24879
|
+
}
|
|
24880
|
+
if (model) {
|
|
24881
|
+
writeFileSafe(
|
|
24882
|
+
join20(idDir, "put.ts"),
|
|
24883
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24884
|
+
${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
|
|
24885
|
+
|
|
24886
|
+
// ${writeDoc}
|
|
24887
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24888
|
+
const { id } = req.params;
|
|
24889
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
24890
|
+
if (!item) {
|
|
24891
|
+
res.json({ error: "Not found" }, 404);
|
|
24892
|
+
return;
|
|
24893
|
+
}
|
|
24894
|
+
// tina4:edit guard which fields may be updated and who may update this row
|
|
24895
|
+
${extend(
|
|
24896
|
+
"guard which fields / who may update",
|
|
24897
|
+
`e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`
|
|
24898
|
+
)} Object.assign(item, req.body as Record<string, unknown>);
|
|
24899
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
24900
|
+
// write is reported to the client as a 200 carrying unsaved data.
|
|
24901
|
+
if ((await item.save()) === false) {
|
|
24902
|
+
res.json({ error: "Could not update ${singular}" }, 400);
|
|
24903
|
+
return;
|
|
24904
|
+
}
|
|
24905
|
+
res.json({ data: item.toObject() });
|
|
24906
|
+
}
|
|
24907
|
+
`
|
|
24908
|
+
);
|
|
24909
|
+
} else {
|
|
24910
|
+
writeFileSafe(
|
|
24911
|
+
join20(idDir, "put.ts"),
|
|
24912
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24913
|
+
|
|
24914
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
|
|
24915
|
+
|
|
24916
|
+
// ${writeDoc}
|
|
24917
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24918
|
+
// tina4:edit fill the update handler (see AI-FILL fill-spec below)
|
|
24919
|
+
${aiFill(`update_${singular}`, {
|
|
24920
|
+
intent: `load, mutate and save an existing ${singular}`,
|
|
24921
|
+
given: "req.params.id -> id; req.body -> changed fields",
|
|
24922
|
+
use: "Model.selectOne(\u2026) then Object.assign(item, req.body) then item.save()",
|
|
24923
|
+
ret: "res.json({ data: item }) or 404",
|
|
24924
|
+
ground: `tina4_context("update ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24925
|
+
raise: `update ${singular} not implemented`
|
|
24926
|
+
})}}
|
|
24927
|
+
`
|
|
24928
|
+
);
|
|
24929
|
+
}
|
|
24930
|
+
if (model) {
|
|
24931
|
+
writeFileSafe(
|
|
24932
|
+
join20(idDir, "delete.ts"),
|
|
24933
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24934
|
+
${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
|
|
24935
|
+
|
|
24936
|
+
// ${writeDoc}
|
|
24937
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24938
|
+
const { id } = req.params;
|
|
24939
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
24940
|
+
if (!item) {
|
|
24941
|
+
res.json({ error: "Not found" }, 404);
|
|
24942
|
+
return;
|
|
24943
|
+
}
|
|
24944
|
+
await item.delete();
|
|
24945
|
+
res.json({ message: "deleted", id });
|
|
24946
|
+
}
|
|
24947
|
+
`
|
|
24948
|
+
);
|
|
24949
|
+
} else {
|
|
24950
|
+
writeFileSafe(
|
|
24951
|
+
join20(idDir, "delete.ts"),
|
|
24952
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24953
|
+
|
|
24954
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
|
|
24955
|
+
|
|
24956
|
+
// ${writeDoc}
|
|
24957
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24958
|
+
${aiFill(`delete_${singular}`, {
|
|
24959
|
+
intent: `delete a ${singular} by id`,
|
|
24960
|
+
given: "req.params.id -> id",
|
|
24961
|
+
use: "Model.selectOne(\u2026) then item.delete()",
|
|
24962
|
+
ret: "res.json({ message: 'deleted', id }) or 404",
|
|
24963
|
+
ground: `tina4_context("delete ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24964
|
+
raise: `delete ${singular} not implemented`
|
|
24965
|
+
})}}
|
|
24966
|
+
`
|
|
24967
|
+
);
|
|
24968
|
+
}
|
|
24969
|
+
if (emitTest) {
|
|
24970
|
+
if (model) {
|
|
24971
|
+
generateTest(routePath, { model, "secure-writes": true, public: isPublic });
|
|
24972
|
+
} else {
|
|
24973
|
+
emitRouteStubTest(routePath);
|
|
24974
|
+
}
|
|
24975
|
+
}
|
|
24976
|
+
}
|
|
24977
|
+
function generateCrud(name, flags) {
|
|
24978
|
+
const table2 = toTableName(name);
|
|
24979
|
+
const routeName = toPlural(table2);
|
|
24980
|
+
const isPublic = Boolean(flags.public);
|
|
24981
|
+
if (!__resolution.jsonMode) console.log(`
|
|
24982
|
+
Generating CRUD for ${name}...
|
|
24983
|
+
`);
|
|
24984
|
+
generateModel(name, flags, false);
|
|
24985
|
+
generateRoute(routeName, { ...flags, model: name }, false);
|
|
24986
|
+
generateForm(name, flags);
|
|
24987
|
+
generateView(name, flags);
|
|
24988
|
+
generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
|
|
24989
|
+
if (!__resolution.jsonMode) {
|
|
24990
|
+
console.log(`
|
|
24991
|
+
CRUD generation complete for ${name}.`);
|
|
24992
|
+
console.log(" Run: tina4nodejs migrate");
|
|
24993
|
+
console.log(" Visit: /swagger to see the API docs");
|
|
24994
|
+
}
|
|
24995
|
+
}
|
|
24996
|
+
function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
|
|
24997
|
+
const ts = timestamp();
|
|
24998
|
+
const dir = resolve10("migrations");
|
|
24999
|
+
ensureDir(dir);
|
|
25000
|
+
let table2;
|
|
25001
|
+
if (tableOverride) {
|
|
25002
|
+
table2 = tableOverride;
|
|
25003
|
+
} else {
|
|
25004
|
+
const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
|
|
25005
|
+
table2 = toTableName(raw);
|
|
25006
|
+
}
|
|
25007
|
+
if (__resolution.target === "migration") {
|
|
25008
|
+
setResolutionField("table_name", table2);
|
|
25009
|
+
}
|
|
25010
|
+
const fields = fieldsOverride || parseFields(flags.fields || "");
|
|
25011
|
+
const isCreate = name.startsWith("create_") || fieldsOverride !== void 0;
|
|
25012
|
+
const fileName = `${ts}_${name}.sql`;
|
|
25013
|
+
const path8 = join20(dir, fileName);
|
|
25014
|
+
setResolutionField("migration_path", `migrations/${fileName}`);
|
|
25015
|
+
if (__resolution.target === "migration") {
|
|
25016
|
+
setResolutionField("file_path", `migrations/${fileName}`);
|
|
25017
|
+
pushTestPath(`tests/${table2}_migration.test.ts`);
|
|
25018
|
+
}
|
|
25019
|
+
let upSql;
|
|
25020
|
+
let downSql;
|
|
25021
|
+
if (isCreate) {
|
|
25022
|
+
const colLines = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
|
|
25023
|
+
for (const [fname, ftype] of fields) {
|
|
25024
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
25025
|
+
const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
|
|
25026
|
+
colLines.push(` ${fname} ${info.sql}${defaultClause}`);
|
|
25027
|
+
}
|
|
25028
|
+
colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
|
|
25029
|
+
upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
|
|
25030
|
+
-- tina4:edit add columns beyond id + created_at
|
|
25031
|
+
${colLines.join(",\n")}
|
|
25032
|
+
);`;
|
|
25033
|
+
downSql = `-- tina4:edit mirror the CREATE's added columns in the rollback
|
|
25034
|
+
DROP TABLE IF EXISTS ${table2};`;
|
|
25035
|
+
} else {
|
|
25036
|
+
upSql = `-- tina4:edit write your UP migration SQL here
|
|
25037
|
+
-- Example: ALTER TABLE ${table2} ADD COLUMN new_col TEXT DEFAULT '';`;
|
|
25038
|
+
downSql = `-- tina4:edit write your DOWN rollback SQL here
|
|
25039
|
+
-- Example: ALTER TABLE ${table2} DROP COLUMN new_col;`;
|
|
25040
|
+
}
|
|
25041
|
+
const now = isoNow();
|
|
25042
|
+
const content = `-- Migration: ${name}
|
|
25043
|
+
-- Created: ${now}
|
|
25044
|
+
|
|
25045
|
+
-- UP
|
|
25046
|
+
${upSql}
|
|
25047
|
+
|
|
25048
|
+
-- DOWN
|
|
25049
|
+
${downSql}
|
|
25050
|
+
`;
|
|
25051
|
+
writeFileSafe(path8, content);
|
|
25052
|
+
const downPath = join20(dir, `${ts}_${name}.down.sql`);
|
|
25053
|
+
const downContent = `-- Rollback: ${name}
|
|
25054
|
+
-- Created: ${now}
|
|
25055
|
+
|
|
25056
|
+
${downSql}
|
|
25057
|
+
`;
|
|
25058
|
+
writeFileSafe(downPath, downContent);
|
|
25059
|
+
if (emitTest && isCreate) emitMigrationTest(name, table2);
|
|
25060
|
+
}
|
|
25061
|
+
function generateMiddleware(name, _flags) {
|
|
25062
|
+
const snake = toSnake(name);
|
|
25063
|
+
const dir = resolve10("src/middleware");
|
|
25064
|
+
ensureDir(dir);
|
|
25065
|
+
const path8 = join20(dir, `${snake}.ts`);
|
|
25066
|
+
if (__resolution.target === "middleware") {
|
|
25067
|
+
setResolutionField("class_name", name);
|
|
25068
|
+
setResolutionField("file_path", `src/middleware/${snake}.ts`);
|
|
25069
|
+
pushTestPath(`tests/${snake}.test.ts`);
|
|
25070
|
+
}
|
|
25071
|
+
const content = `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25072
|
+
|
|
25073
|
+
/**
|
|
25074
|
+
* ${name} middleware \u2014 runs before and after route handlers.
|
|
25075
|
+
*
|
|
25076
|
+
* Usage:
|
|
25077
|
+
* import { before${name}, after${name} } from "../middleware/${snake}.js";
|
|
25078
|
+
*/
|
|
25079
|
+
|
|
25080
|
+
export async function before${name}(
|
|
25081
|
+
req: Tina4Request,
|
|
25082
|
+
res: Tina4Response,
|
|
25083
|
+
next: () => Promise<void>,
|
|
25084
|
+
): Promise<void> {
|
|
25085
|
+
// tina4:edit replace the Authorization check with the real pre-request rule
|
|
25086
|
+
const auth = req.headers["authorization"];
|
|
25087
|
+
if (!auth) {
|
|
25088
|
+
res.json({ error: "Unauthorized" }, 401);
|
|
25089
|
+
return;
|
|
25090
|
+
}
|
|
25091
|
+
await next();
|
|
25092
|
+
}
|
|
25093
|
+
|
|
25094
|
+
export async function after${name}(
|
|
25095
|
+
req: Tina4Request,
|
|
25096
|
+
res: Tina4Response,
|
|
25097
|
+
next: () => Promise<void>,
|
|
25098
|
+
): Promise<void> {
|
|
25099
|
+
// tina4:edit add post-processing (logging, header injection, telemetry)
|
|
25100
|
+
await next();
|
|
25101
|
+
}
|
|
25102
|
+
`;
|
|
25103
|
+
writeFileSafe(path8, content);
|
|
25104
|
+
emitMiddlewareTest(name, snake);
|
|
25105
|
+
}
|
|
25106
|
+
function generateTest(name, flags) {
|
|
25107
|
+
const snake = toSnake(name);
|
|
25108
|
+
const singular = snake.endsWith("s") ? snake.slice(0, -1) : snake;
|
|
25109
|
+
const model = flags.model;
|
|
25110
|
+
const dir = resolve10("tests");
|
|
25111
|
+
ensureDir(dir);
|
|
25112
|
+
const path8 = join20(dir, `${snake}.test.ts`);
|
|
25113
|
+
if (model && flags["secure-writes"]) {
|
|
25114
|
+
const isPublic = Boolean(flags.public);
|
|
25115
|
+
const posture = isPublic ? "open (--public)" : "gated";
|
|
25116
|
+
const writeCase = isPublic ? ` // --public opened the write: an anonymous POST creates -> 201.
|
|
25117
|
+
assert("anonymous POST is public -> 201",
|
|
25118
|
+
(await client.post("/api/${snake}", { json: { name: "test" } })).status === 201);` : ` // Secure by default: a tokenless POST is rejected with 401.
|
|
25119
|
+
assert("anonymous POST is gated -> 401",
|
|
25120
|
+
(await client.post("/api/${snake}", { json: { name: "test" } })).status === 401);
|
|
25121
|
+
// A valid Bearer token passes the gate and creates -> 201.
|
|
25122
|
+
const token = getToken({ userId: 1 });
|
|
25123
|
+
assert("authenticated POST creates -> 201",
|
|
25124
|
+
(await client.post("/api/${snake}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status === 201);`;
|
|
25125
|
+
const content2 = `/**
|
|
25126
|
+
* ${name} CRUD \u2014 reads public, writes ${posture} (secure by default).
|
|
25127
|
+
*
|
|
25128
|
+
* Real end-to-end via TestClient: no mocks \u2014 real Router, real auth gate, real
|
|
25129
|
+
* JWT, real SQLite DB + table. Run with: npx tsx tests/${snake}.test.ts
|
|
25130
|
+
*/
|
|
25131
|
+
import { dirname, resolve } from "node:path";
|
|
25132
|
+
import { fileURLToPath } from "node:url";
|
|
25133
|
+
import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
|
|
25134
|
+
import { initDatabase } from "tina4-nodejs/orm";
|
|
25135
|
+
import ${model} from "../src/models/${model}.js";
|
|
25136
|
+
|
|
25137
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
25138
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
25139
|
+
|
|
25140
|
+
let pass = 0;
|
|
25141
|
+
let fail = 0;
|
|
25142
|
+
function assert(label: string, ok: boolean): void {
|
|
25143
|
+
if (ok) { pass++; console.log(\` PASS \${label}\`); }
|
|
25144
|
+
else { fail++; console.log(\` FAIL \${label}\`); }
|
|
25145
|
+
}
|
|
25146
|
+
|
|
25147
|
+
await initDatabase({ url: "sqlite:///data/test_${snake}.db" });
|
|
25148
|
+
await ${model}.createTable();
|
|
25149
|
+
|
|
25150
|
+
const router = new Router();
|
|
25151
|
+
for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
|
|
25152
|
+
const client = new TestClient(router);
|
|
25153
|
+
|
|
25154
|
+
// Reads are public.
|
|
25155
|
+
assert("GET list is public -> 200", (await client.get("/api/${snake}")).status === 200);
|
|
25156
|
+
${writeCase}
|
|
25157
|
+
|
|
25158
|
+
console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
|
|
25159
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
25160
|
+
`;
|
|
25161
|
+
writeFileSafe(path8, content2);
|
|
25162
|
+
return;
|
|
25163
|
+
}
|
|
25164
|
+
let content;
|
|
25165
|
+
if (model) {
|
|
25166
|
+
content = `import { tests, assertTrue } from "tina4-nodejs";
|
|
25167
|
+
|
|
25168
|
+
/**
|
|
25169
|
+
* Tests for ${name} CRUD operations.
|
|
25170
|
+
*/
|
|
25171
|
+
|
|
25172
|
+
const list${model}s = tests(
|
|
25173
|
+
assertTrue([]),
|
|
25174
|
+
)(function list${model}s() {
|
|
25175
|
+
// tina4:edit assert against a real GET /api/${toSnake(name)} response (rows, count)
|
|
25176
|
+
return true;
|
|
25177
|
+
});
|
|
25178
|
+
|
|
25179
|
+
const get${model} = tests(
|
|
25180
|
+
assertTrue([]),
|
|
25181
|
+
)(function get${model}() {
|
|
25182
|
+
// tina4:edit assert against GET /api/${toSnake(name)}/{id} for one seeded row
|
|
25183
|
+
return true;
|
|
25184
|
+
});
|
|
25185
|
+
|
|
25186
|
+
const create${model} = tests(
|
|
25187
|
+
assertTrue([]),
|
|
25188
|
+
)(function create${model}() {
|
|
25189
|
+
// tina4:edit POST a valid + an invalid body, assert 201 vs 400
|
|
25190
|
+
return true;
|
|
25191
|
+
});
|
|
25192
|
+
|
|
25193
|
+
const update${model} = tests(
|
|
25194
|
+
assertTrue([]),
|
|
25195
|
+
)(function update${model}() {
|
|
25196
|
+
// tina4:edit PUT changed fields, assert the row was persisted
|
|
25197
|
+
return true;
|
|
25198
|
+
});
|
|
25199
|
+
|
|
25200
|
+
const delete${model} = tests(
|
|
25201
|
+
assertTrue([]),
|
|
25202
|
+
)(function delete${model}() {
|
|
25203
|
+
// tina4:edit DELETE the id, assert 200 then GET returns 404
|
|
25204
|
+
return true;
|
|
25205
|
+
});
|
|
25206
|
+
|
|
25207
|
+
void [list${model}s, get${model}, create${model}, update${model}, delete${model}];
|
|
25208
|
+
`;
|
|
25209
|
+
} else {
|
|
25210
|
+
const titleName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
25211
|
+
content = `import { tests, assertTrue } from "tina4-nodejs";
|
|
25212
|
+
|
|
25213
|
+
/**
|
|
25214
|
+
* Tests for ${name}.
|
|
25215
|
+
*/
|
|
25216
|
+
|
|
25217
|
+
const test${titleName} = tests(
|
|
25218
|
+
assertTrue([]),
|
|
25219
|
+
)(function test${titleName}() {
|
|
25220
|
+
// tina4:edit assert against the real behaviour under test (no mocks)
|
|
25221
|
+
return true;
|
|
25222
|
+
});
|
|
25223
|
+
|
|
25224
|
+
void test${titleName};
|
|
25225
|
+
`;
|
|
25226
|
+
}
|
|
25227
|
+
writeFileSafe(path8, content);
|
|
25228
|
+
}
|
|
25229
|
+
function generateForm(name, flags) {
|
|
25230
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
25231
|
+
const table2 = toTableName(name);
|
|
25232
|
+
const routeName = toPlural(table2);
|
|
25233
|
+
const inputTypes = {
|
|
25234
|
+
string: "text",
|
|
25235
|
+
str: "text",
|
|
25236
|
+
text: "textarea",
|
|
25237
|
+
int: "number",
|
|
25238
|
+
integer: "number",
|
|
25239
|
+
float: "number",
|
|
25240
|
+
numeric: "number",
|
|
25241
|
+
decimal: "number",
|
|
25242
|
+
bool: "checkbox",
|
|
25243
|
+
boolean: "checkbox",
|
|
25244
|
+
datetime: "datetime-local",
|
|
25245
|
+
blob: "file"
|
|
25246
|
+
};
|
|
25247
|
+
const dir = resolve10("src/templates/forms");
|
|
25248
|
+
ensureDir(dir);
|
|
25249
|
+
const path8 = join20(dir, `${table2}.twig`);
|
|
25250
|
+
let fieldHtml = "";
|
|
25251
|
+
for (const [fname, ftype] of fields) {
|
|
25252
|
+
const itype = inputTypes[ftype] || "text";
|
|
25253
|
+
const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
25254
|
+
const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
|
|
25255
|
+
if (itype === "textarea") {
|
|
25256
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
25257
|
+
<label for="${fname}">${label}</label>
|
|
25258
|
+
<textarea id="${fname}" name="${fname}" class="form-control" rows="4" placeholder="${label}">{{ item.${fname} }}</textarea>
|
|
25259
|
+
</div>
|
|
25260
|
+
`;
|
|
25261
|
+
} else if (itype === "checkbox") {
|
|
25262
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
25263
|
+
<label>
|
|
25264
|
+
<input type="checkbox" id="${fname}" name="${fname}" value="1" {% if item.${fname} %}checked{% endif %}>
|
|
25265
|
+
${label}
|
|
25266
|
+
</label>
|
|
25267
|
+
</div>
|
|
25268
|
+
`;
|
|
25269
|
+
} else {
|
|
25270
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
25271
|
+
<label for="${fname}">${label}</label>
|
|
25272
|
+
<input type="${itype}" id="${fname}" name="${fname}" class="form-control"${step} value="{{ item.${fname} }}" placeholder="${label}">
|
|
25273
|
+
</div>
|
|
25274
|
+
`;
|
|
25275
|
+
}
|
|
25276
|
+
}
|
|
25277
|
+
const content = `{% extends "base.twig" %}
|
|
25278
|
+
{% block title %}${name} {% if item.id %}Edit{% else %}Create{% endif %}{% endblock %}
|
|
25279
|
+
{% block content %}
|
|
25280
|
+
<div class="container mt-4">
|
|
25281
|
+
<h1>{% if item.id %}Edit ${name}{% else %}Create ${name}{% endif %}</h1>
|
|
25282
|
+
{# tina4:edit restyle the form beyond the scaffolded defaults #}
|
|
25283
|
+
<form method="post" action="/api/${routeName}{% if item.id %}/{{ item.id }}{% endif %}">
|
|
25284
|
+
{{ form_token() }}
|
|
25285
|
+
` + fieldHtml + ` <button type="submit" class="btn btn-primary">
|
|
25286
|
+
{% if item.id %}Update{% else %}Create{% endif %}
|
|
25287
|
+
</button>
|
|
25288
|
+
<a href="/api/${routeName}" class="btn btn-secondary">Cancel</a>
|
|
25289
|
+
</form>
|
|
25290
|
+
</div>
|
|
25291
|
+
{% endblock %}
|
|
25292
|
+
`;
|
|
25293
|
+
writeFileSafe(path8, content);
|
|
25294
|
+
}
|
|
25295
|
+
function generateView(name, flags) {
|
|
25296
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
25297
|
+
const table2 = toTableName(name);
|
|
25298
|
+
const routeName = toPlural(table2);
|
|
25299
|
+
const cols = fields.map(([f]) => f);
|
|
25300
|
+
const dir = resolve10("src/templates/pages");
|
|
25301
|
+
ensureDir(dir);
|
|
25302
|
+
const listPath = join20(dir, `${routeName}.twig`);
|
|
25303
|
+
const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
|
|
25304
|
+
const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
|
|
25305
|
+
const listContent = `{% extends "base.twig" %}
|
|
25306
|
+
{% block title %}${name}s{% endblock %}
|
|
25307
|
+
{% block content %}
|
|
25308
|
+
<div class="container mt-4">
|
|
25309
|
+
{# tina4:edit add sort / filter / pagination controls to the list #}
|
|
25310
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
25311
|
+
<h1>${name}s</h1>
|
|
25312
|
+
<a href="/${routeName}/create" class="btn btn-primary">Add ${name}</a>
|
|
25313
|
+
</div>
|
|
25314
|
+
<table class="table">
|
|
25315
|
+
<thead>
|
|
25316
|
+
<tr>
|
|
25317
|
+
<th>ID</th>
|
|
25318
|
+
${th}
|
|
25319
|
+
<th>Actions</th>
|
|
25320
|
+
</tr>
|
|
25321
|
+
</thead>
|
|
25322
|
+
<tbody>
|
|
25323
|
+
{% for item in items %}
|
|
25324
|
+
<tr>
|
|
25325
|
+
<td>{{ item.id }}</td>
|
|
25326
|
+
${td}
|
|
25327
|
+
<td>
|
|
25328
|
+
<a href="/${routeName}/{{ item.id }}" class="btn btn-sm btn-primary">View</a>
|
|
25329
|
+
<a href="/${routeName}/{{ item.id }}/edit" class="btn btn-sm btn-secondary">Edit</a>
|
|
25330
|
+
</td>
|
|
25331
|
+
</tr>
|
|
25332
|
+
{% endfor %}
|
|
25333
|
+
</tbody>
|
|
25334
|
+
</table>
|
|
25335
|
+
</div>
|
|
25336
|
+
{% endblock %}
|
|
25337
|
+
`;
|
|
25338
|
+
writeFileSafe(listPath, listContent);
|
|
25339
|
+
const detailPath = join20(dir, `${table2}.twig`);
|
|
25340
|
+
const detailFields = cols.map((c) => ` <div class="mb-3"><strong>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}:</strong> {{ item.${c} }}</div>`).join("\n");
|
|
25341
|
+
const detailContent = `{% extends "base.twig" %}
|
|
25342
|
+
{% block title %}${name} Detail{% endblock %}
|
|
25343
|
+
{% block content %}
|
|
25344
|
+
<div class="container mt-4">
|
|
25345
|
+
{# tina4:edit extend the detail view with related records or actions #}
|
|
25346
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
25347
|
+
<h1>${name} #{{ item.id }}</h1>
|
|
25348
|
+
<div>
|
|
25349
|
+
<a href="/${routeName}/{{ item.id }}/edit" class="btn btn-secondary">Edit</a>
|
|
25350
|
+
<a href="/${routeName}" class="btn btn-outline-secondary">Back</a>
|
|
25351
|
+
</div>
|
|
25352
|
+
</div>
|
|
25353
|
+
${detailFields}
|
|
25354
|
+
</div>
|
|
25355
|
+
{% endblock %}
|
|
25356
|
+
`;
|
|
25357
|
+
writeFileSafe(detailPath, detailContent);
|
|
25358
|
+
}
|
|
25359
|
+
function generateAuth(_flags) {
|
|
25360
|
+
if (!__resolution.jsonMode) console.log("\n Generating authentication scaffolding...\n");
|
|
25361
|
+
generateModel("User", { fields: "email:string,password:string,role:string" }, false);
|
|
25362
|
+
const registerDir = resolve10("src/routes/api/auth/register");
|
|
25363
|
+
const loginDir = resolve10("src/routes/api/auth/login");
|
|
25364
|
+
const meDir = resolve10("src/routes/api/auth/me");
|
|
25365
|
+
ensureDir(registerDir);
|
|
25366
|
+
ensureDir(loginDir);
|
|
25367
|
+
ensureDir(meDir);
|
|
25368
|
+
writeFileSafe(
|
|
25369
|
+
join20(registerDir, "post.ts"),
|
|
25370
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25371
|
+
import { hashPassword } from "tina4-nodejs";
|
|
25372
|
+
import User from "../../../../models/User.js";
|
|
25373
|
+
|
|
25374
|
+
// Public: registration mints an account for a user who has no token yet.
|
|
25375
|
+
export const secure = false;
|
|
25376
|
+
|
|
25377
|
+
export const meta = { summary: "Register a new user", tags: ["auth"] };
|
|
25378
|
+
|
|
25379
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
25380
|
+
// tina4:edit add password-strength / email-format / captcha rules before mint
|
|
25381
|
+
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
25382
|
+
|
|
25383
|
+
if (!email || !password) {
|
|
25384
|
+
res.json({ error: "Email and password required" }, 400);
|
|
25385
|
+
return;
|
|
25386
|
+
}
|
|
25387
|
+
|
|
25388
|
+
const existing = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
25389
|
+
if (existing) {
|
|
25390
|
+
res.json({ error: "Email already registered" }, 409);
|
|
25391
|
+
return;
|
|
25392
|
+
}
|
|
25393
|
+
|
|
25394
|
+
const user = new User({ email, password: hashPassword(password), role: "user" });
|
|
25395
|
+
await user.save();
|
|
25396
|
+
res.json({ message: "Registered", id: user.toObject().id }, 201);
|
|
25397
|
+
}
|
|
25398
|
+
`
|
|
25399
|
+
);
|
|
25400
|
+
writeFileSafe(
|
|
25401
|
+
join20(loginDir, "post.ts"),
|
|
25402
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25403
|
+
import { checkPassword, getToken } from "tina4-nodejs";
|
|
25404
|
+
import User from "../../../../models/User.js";
|
|
25405
|
+
|
|
25406
|
+
// Public: login authenticates by password and mints the token.
|
|
25407
|
+
export const secure = false;
|
|
25408
|
+
|
|
25409
|
+
export const meta = { summary: "Login and receive JWT token", tags: ["auth"] };
|
|
25410
|
+
|
|
25411
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
25412
|
+
// tina4:edit add rate-limit / lock-after-N-failures / 2FA before password check
|
|
25413
|
+
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
25414
|
+
|
|
25415
|
+
if (!email || !password) {
|
|
25416
|
+
res.json({ error: "Email and password required" }, 400);
|
|
25417
|
+
return;
|
|
25418
|
+
}
|
|
25419
|
+
|
|
25420
|
+
const user = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
25421
|
+
if (!user || !checkPassword(password, user.toObject().password as string)) {
|
|
25422
|
+
res.json({ error: "Invalid credentials" }, 401);
|
|
25423
|
+
return;
|
|
25424
|
+
}
|
|
25425
|
+
|
|
25426
|
+
const data = user.toObject();
|
|
25427
|
+
// tina4:edit set token TTL (getToken(payload, secret, expiresInMinutes)) and add scopes if needed
|
|
25428
|
+
const token = getToken({ userId: data.id, email: data.email, role: data.role });
|
|
25429
|
+
res.json({ token });
|
|
25430
|
+
}
|
|
25431
|
+
`
|
|
25432
|
+
);
|
|
25433
|
+
writeFileSafe(
|
|
25434
|
+
join20(meDir, "get.ts"),
|
|
25435
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25436
|
+
import { authenticateRequest } from "tina4-nodejs";
|
|
25437
|
+
|
|
25438
|
+
export const meta = { summary: "Get current authenticated user", tags: ["auth"] };
|
|
25439
|
+
|
|
25440
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
25441
|
+
const payload = authenticateRequest(req.headers as Record<string, string | string[] | undefined>);
|
|
25442
|
+
if (!payload) {
|
|
25443
|
+
res.json({ error: "Unauthorized" }, 401);
|
|
25444
|
+
return;
|
|
25445
|
+
}
|
|
25446
|
+
res.json({ user: payload });
|
|
25447
|
+
}
|
|
25448
|
+
`
|
|
25449
|
+
);
|
|
25450
|
+
const formsDir = resolve10("src/templates/forms");
|
|
25451
|
+
ensureDir(formsDir);
|
|
25452
|
+
writeFileSafe(
|
|
25453
|
+
join20(formsDir, "login.twig"),
|
|
25454
|
+
`{% extends "base.twig" %}
|
|
25455
|
+
{% block title %}Login{% endblock %}
|
|
25456
|
+
{% block content %}
|
|
25457
|
+
<div class="container mt-4" style="max-width:400px">
|
|
25458
|
+
<h1>Login</h1>
|
|
25459
|
+
<form method="post" action="/api/auth/login">
|
|
25460
|
+
{{ form_token() }}
|
|
25461
|
+
<div class="form-group mb-3">
|
|
25462
|
+
<label for="email">Email</label>
|
|
25463
|
+
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
|
25464
|
+
</div>
|
|
25465
|
+
<div class="form-group mb-3">
|
|
25466
|
+
<label for="password">Password</label>
|
|
25467
|
+
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
|
|
25468
|
+
</div>
|
|
25469
|
+
<button type="submit" class="btn btn-primary w-100">Login</button>
|
|
25470
|
+
<p class="mt-3 text-center"><a href="/register">Create an account</a></p>
|
|
25471
|
+
</form>
|
|
25472
|
+
</div>
|
|
25473
|
+
{% endblock %}
|
|
25474
|
+
`
|
|
25475
|
+
);
|
|
25476
|
+
writeFileSafe(
|
|
25477
|
+
join20(formsDir, "register.twig"),
|
|
25478
|
+
`{% extends "base.twig" %}
|
|
25479
|
+
{% block title %}Register{% endblock %}
|
|
25480
|
+
{% block content %}
|
|
25481
|
+
<div class="container mt-4" style="max-width:400px">
|
|
25482
|
+
<h1>Register</h1>
|
|
25483
|
+
<form method="post" action="/api/auth/register">
|
|
25484
|
+
{{ form_token() }}
|
|
25485
|
+
<div class="form-group mb-3">
|
|
25486
|
+
<label for="email">Email</label>
|
|
25487
|
+
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
|
25488
|
+
</div>
|
|
25489
|
+
<div class="form-group mb-3">
|
|
25490
|
+
<label for="password">Password</label>
|
|
25491
|
+
<input type="password" id="password" name="password" class="form-control" placeholder="Password" minlength="8" required>
|
|
25492
|
+
</div>
|
|
25493
|
+
<button type="submit" class="btn btn-primary w-100">Register</button>
|
|
25494
|
+
<p class="mt-3 text-center"><a href="/login">Already have an account?</a></p>
|
|
25495
|
+
</form>
|
|
25496
|
+
</div>
|
|
25497
|
+
{% endblock %}
|
|
25498
|
+
`
|
|
25499
|
+
);
|
|
25500
|
+
emitAuthTest();
|
|
25501
|
+
if (!__resolution.jsonMode) {
|
|
25502
|
+
console.log("\n Authentication scaffolding complete.");
|
|
25503
|
+
console.log(" Run: tina4nodejs migrate");
|
|
25504
|
+
console.log(" POST /api/auth/register \u2014 create account (public)");
|
|
25505
|
+
console.log(" POST /api/auth/login \u2014 get JWT token (public)");
|
|
25506
|
+
console.log(" GET /api/auth/me \u2014 get profile (requires token)");
|
|
25507
|
+
}
|
|
25508
|
+
}
|
|
25509
|
+
function generateService(name, flags) {
|
|
25510
|
+
const snake = toSnake(name);
|
|
25511
|
+
const camel = toCamel(toPascal(name)) || snake;
|
|
25512
|
+
const cron = flags.cron;
|
|
25513
|
+
const dir = resolve10("src/services");
|
|
25514
|
+
ensureDir(dir);
|
|
25515
|
+
const path8 = join20(dir, `${snake}.ts`);
|
|
25516
|
+
let scheduleField;
|
|
25517
|
+
let note;
|
|
25518
|
+
if (cron && cron !== true) {
|
|
25519
|
+
scheduleField = ` timing: ${JSON.stringify(String(cron))},`;
|
|
25520
|
+
note = `cron '${cron}'`;
|
|
25521
|
+
} else {
|
|
25522
|
+
const seconds = parseEvery(flags.every);
|
|
25523
|
+
scheduleField = ` interval: ${seconds},`;
|
|
25524
|
+
note = `every ${seconds}s`;
|
|
25525
|
+
}
|
|
25526
|
+
const body = aiFill(`${camel}Task`, {
|
|
25527
|
+
intent: "do the scheduled work for this service",
|
|
25528
|
+
given: "context -> ServiceContext (.name, .running, .lastRun)",
|
|
25529
|
+
use: "your ORM / Api / Messenger code (re-run on schedule)",
|
|
25530
|
+
ground: `tina4_context("background service scheduled task", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
25531
|
+
raise: `service ${snake} not implemented`
|
|
25532
|
+
});
|
|
25533
|
+
const content = `import type { ServiceContext } from "tina4-nodejs";
|
|
25534
|
+
|
|
25535
|
+
/**
|
|
25536
|
+
* ${name} background service \u2014 runs ${note} via ServiceRunner.
|
|
25537
|
+
*
|
|
25538
|
+
* Wire a runner once (e.g. in app.ts) to actually run it \u2014 \`tina4nodejs serve\`
|
|
25539
|
+
* does NOT auto-start services:
|
|
25540
|
+
*
|
|
25541
|
+
* import { ServiceRunner } from "tina4-nodejs";
|
|
25542
|
+
* await ServiceRunner.discover("src/services"); // registers this default export
|
|
25543
|
+
* ServiceRunner.start();
|
|
25544
|
+
*/
|
|
25545
|
+
|
|
25546
|
+
export async function ${camel}Task(context: ServiceContext): Promise<void> {
|
|
25547
|
+
// tina4:edit replace the AI-FILL stub below with the scheduled work
|
|
25548
|
+
${body}}
|
|
25549
|
+
|
|
25550
|
+
// Discovered by ServiceRunner.discover("src/services") \u2014 it reads name/handler
|
|
25551
|
+
// (+ interval or timing) off this default export.
|
|
25552
|
+
export default {
|
|
25553
|
+
name: "${snake}",
|
|
25554
|
+
handler: ${camel}Task,
|
|
25555
|
+
${scheduleField}
|
|
25556
|
+
};
|
|
25557
|
+
`;
|
|
25558
|
+
writeFileSafe(path8, content);
|
|
25559
|
+
emitServiceTest(name, snake, camel);
|
|
25560
|
+
}
|
|
25561
|
+
function generateQueue(name, _flags) {
|
|
25562
|
+
const topic = name.replace(/^\//, "");
|
|
25563
|
+
const slug = toSnake(topic.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
|
|
25564
|
+
const pascal = toPascal(topic) || "Topic";
|
|
25565
|
+
const dir = resolve10("src/services");
|
|
25566
|
+
ensureDir(dir);
|
|
25567
|
+
const path8 = join20(dir, `${slug}_consumer.ts`);
|
|
25568
|
+
const body = aiFill(`handle${pascal}`, {
|
|
25569
|
+
intent: `process ONE ${topic} job payload`,
|
|
25570
|
+
given: "payload -> the produced job data (job.payload)",
|
|
25571
|
+
use: "your ORM / Messenger code; return to ack (job.complete), throw to nack (job.fail)",
|
|
25572
|
+
ground: `tina4_context("process a queue job", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
25573
|
+
raise: `queue ${topic} handler not implemented`
|
|
25574
|
+
});
|
|
25575
|
+
const content = `import { Queue } from "tina4-nodejs";
|
|
25576
|
+
import type { ServiceContext } from "tina4-nodejs";
|
|
25577
|
+
|
|
25578
|
+
/**
|
|
25579
|
+
* ${topic} queue \u2014 producer + consumer worker.
|
|
25580
|
+
*
|
|
25581
|
+
* Produce from anywhere: publish${pascal}({ ... })
|
|
25582
|
+
* The consumer is a long-running worker wired as a ServiceRunner daemon:
|
|
25583
|
+
* await ServiceRunner.discover("src/services"); ServiceRunner.start();
|
|
25584
|
+
*/
|
|
25585
|
+
|
|
25586
|
+
/** Enqueue a ${topic} job for the worker below to process. Returns the job id. */
|
|
25587
|
+
export function publish${pascal}(payload: Record<string, unknown>): string {
|
|
25588
|
+
return new Queue({ topic: "${topic}" }).produce("${topic}", payload);
|
|
25589
|
+
}
|
|
25590
|
+
|
|
25591
|
+
/** Process ONE ${topic} job payload. */
|
|
25592
|
+
export async function handle${pascal}(payload: unknown): Promise<void> {
|
|
25593
|
+
// tina4:edit implement the per-job handler; return to ack, throw to nack
|
|
25594
|
+
${body}}
|
|
25595
|
+
|
|
25596
|
+
/** Long-running ${topic} worker \u2014 consume() yields jobs; ack/nack each. */
|
|
25597
|
+
export async function consume${pascal}(_context?: ServiceContext): Promise<void> {
|
|
25598
|
+
const queue = new Queue({ topic: "${topic}" });
|
|
25599
|
+
for await (const job of queue.consume("${topic}")) {
|
|
25600
|
+
const one = Array.isArray(job) ? job[0] : job;
|
|
25601
|
+
try {
|
|
25602
|
+
await handle${pascal}(one.payload);
|
|
25603
|
+
one.complete(); // ack \u2014 remove from the queue
|
|
25604
|
+
} catch (err) {
|
|
25605
|
+
one.fail(String(err)); // nack \u2014 retry / dead-letter
|
|
25606
|
+
}
|
|
25607
|
+
}
|
|
25608
|
+
}
|
|
25609
|
+
|
|
25610
|
+
// Discovered by ServiceRunner.discover("src/services"); daemon:true because
|
|
25611
|
+
// consume${pascal} owns its own loop. The topic + per-job handle keys let
|
|
25612
|
+
// \`tina4nodejs queue work ${topic}\` drive this consumer directly (own the poll
|
|
25613
|
+
// loop / bounded --once drain) without wiring a ServiceRunner.
|
|
25614
|
+
export default {
|
|
25615
|
+
name: "${topic}-consumer",
|
|
25616
|
+
topic: "${topic}",
|
|
25617
|
+
handler: consume${pascal},
|
|
25618
|
+
handle: handle${pascal},
|
|
25619
|
+
daemon: true,
|
|
25620
|
+
};
|
|
25621
|
+
`;
|
|
25622
|
+
writeFileSafe(path8, content);
|
|
25623
|
+
emitQueueTest(topic, slug, pascal);
|
|
25624
|
+
}
|
|
25625
|
+
function generateValidator(name, _flags) {
|
|
25626
|
+
const dir = resolve10("src/validators");
|
|
25627
|
+
ensureDir(dir);
|
|
25628
|
+
const path8 = join20(dir, `${toSnake(name)}.ts`);
|
|
25629
|
+
const rules = extend(
|
|
25630
|
+
"add / adjust the validation rules for this payload",
|
|
25631
|
+
`e.g. .email("email").minLength("name", 2).integer("age"); ground: tina4_context("validate request body with Validator", "nodejs")`
|
|
25632
|
+
);
|
|
25633
|
+
const content = `import { Validator } from "tina4-nodejs";
|
|
25634
|
+
|
|
25635
|
+
/**
|
|
25636
|
+
* Validate a ${name} payload. Returns a Validator (chainable rules).
|
|
25637
|
+
*
|
|
25638
|
+
* Usage in a route:
|
|
25639
|
+
* const v = validate${toPascal(name)}(req.body as Record<string, unknown>);
|
|
25640
|
+
* if (!v.isValid()) return res.json({ error: v.errors()[0]?.message }, 400);
|
|
25641
|
+
*/
|
|
25642
|
+
export function validate${toPascal(name)}(data: Record<string, unknown>): Validator {
|
|
25643
|
+
const validator = new Validator(data);
|
|
25644
|
+
// tina4:edit add rules for this payload (.email/.minLength/.integer/.inList/.pattern)
|
|
25645
|
+
${rules} validator.required("name"); // starter rule (matches the model's default field)
|
|
25646
|
+
return validator;
|
|
25647
|
+
}
|
|
25648
|
+
`;
|
|
25649
|
+
writeFileSafe(path8, content);
|
|
25650
|
+
emitValidatorTest(name, toSnake(name), toPascal(name));
|
|
25651
|
+
}
|
|
25652
|
+
function generateSeeder(name, _flags) {
|
|
25653
|
+
const table2 = toTableName(name);
|
|
25654
|
+
const dir = resolve10("src/seeds");
|
|
25655
|
+
ensureDir(dir);
|
|
25656
|
+
const path8 = join20(dir, `${table2}_seeder.ts`);
|
|
25657
|
+
const overrides = extend(
|
|
25658
|
+
"override fields that need a specific shape (seedOrm auto-fills the rest)",
|
|
25659
|
+
`e.g. return { email: (f) => f.email(), status: "active" }; ground: tina4_context("seed ORM model with FakeData", "nodejs")`
|
|
25660
|
+
);
|
|
25661
|
+
const content = `import { pathToFileURL } from "node:url";
|
|
25662
|
+
import { FakeData, seedOrm, initDatabase } from "tina4-nodejs/orm";
|
|
25663
|
+
import ${name} from "../models/${name}.js";
|
|
25664
|
+
|
|
25665
|
+
/**
|
|
25666
|
+
* Seeder for ${name} \u2014 run with: tina4nodejs seed
|
|
25667
|
+
*
|
|
25668
|
+
* seedOrm auto-fills every field by type/name; override the ones that need a
|
|
25669
|
+
* specific shape below. Each callable receives a FakeData instance.
|
|
25670
|
+
*/
|
|
25671
|
+
export function fieldOverrides(fake: FakeData): Record<string, unknown> {
|
|
25672
|
+
// tina4:edit override any fields that need a specific shape (seedOrm auto-fills the rest)
|
|
25673
|
+
${overrides} void fake; // available for overrides above
|
|
25674
|
+
return {};
|
|
25675
|
+
}
|
|
25676
|
+
|
|
25677
|
+
/** Seed rows. Invoked when this file is run directly by \`tina4nodejs seed\`. */
|
|
25678
|
+
export async function run(): Promise<void> {
|
|
25679
|
+
await initDatabase({ url: process.env.TINA4_DATABASE_URL ?? "sqlite:///data/app.db" });
|
|
25680
|
+
const summary = await seedOrm(${name} as never, 20, fieldOverrides(new FakeData()));
|
|
25681
|
+
console.log(\`Seeded \${summary.seeded} ${name} row(s), \${summary.failed} failed\`);
|
|
25682
|
+
}
|
|
25683
|
+
|
|
25684
|
+
// Only seed when executed as a script (\`tina4nodejs seed\` runs it via tsx) \u2014
|
|
25685
|
+
// importing this module (e.g. in a test) must NOT trigger seeding.
|
|
25686
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
25687
|
+
await run();
|
|
25688
|
+
}
|
|
25689
|
+
`;
|
|
25690
|
+
writeFileSafe(path8, content);
|
|
25691
|
+
emitSeederTest(name, table2);
|
|
25692
|
+
}
|
|
25693
|
+
function generateWebsocket(name, _flags) {
|
|
25694
|
+
const raw = name.trim();
|
|
25695
|
+
const wsPath = raw.startsWith("/") ? raw : "/ws/" + raw.replace(/^\/+/, "");
|
|
25696
|
+
let slug = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
|
|
25697
|
+
const base = slug.startsWith("ws_") ? slug.slice(3) : slug;
|
|
25698
|
+
const handlerName = `${toCamel(toPascal(base))}Ws`;
|
|
25699
|
+
const dir = resolve10("src/routes");
|
|
25700
|
+
ensureDir(dir);
|
|
25701
|
+
const path8 = join20(dir, `ws_${base}.ts`);
|
|
25702
|
+
const body = aiFill(handlerName, {
|
|
25703
|
+
intent: `handle an inbound "message" frame on ${wsPath}`,
|
|
25704
|
+
given: "data -> the message payload (string); connection -> WebSocketConnection",
|
|
25705
|
+
use: "connection.broadcast(data) or connection.sendJson({ ... })",
|
|
25706
|
+
ground: `tina4_context("websocket broadcast message", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
25707
|
+
raise: `websocket ${wsPath} not implemented`
|
|
25708
|
+
});
|
|
25709
|
+
const content = `import { websocket } from "tina4-nodejs";
|
|
25710
|
+
import type { WebSocketConnection } from "tina4-nodejs";
|
|
25711
|
+
|
|
25712
|
+
/**
|
|
25713
|
+
* ${wsPath} WebSocket route.
|
|
25714
|
+
*
|
|
25715
|
+
* Registered on import by websocket(). Node has NO file-based WS
|
|
25716
|
+
* auto-discovery, so IMPORT this module once from app.ts to activate it (add
|
|
25717
|
+
* \`.secure()\` to require a JWT on the upgrade):
|
|
25718
|
+
*
|
|
25719
|
+
* import "./src/routes/ws_${base}.js";
|
|
25720
|
+
*
|
|
25721
|
+
* The server invokes the handler as (connection, event, data) for each event:
|
|
25722
|
+
* "open" (connect), "message" (inbound frame), "close" (disconnect).
|
|
25723
|
+
*/
|
|
25724
|
+
export async function ${handlerName}(
|
|
25725
|
+
connection: WebSocketConnection,
|
|
25726
|
+
event: "open" | "message" | "close",
|
|
25727
|
+
data: string,
|
|
25728
|
+
): Promise<void> {
|
|
25729
|
+
if (event === "open") {
|
|
25730
|
+
// tina4:edit customize the welcome frame (or drop it)
|
|
25731
|
+
connection.sendJson({ type: "welcome" });
|
|
25732
|
+
return;
|
|
25733
|
+
}
|
|
25734
|
+
if (event === "close") {
|
|
25735
|
+
return;
|
|
25736
|
+
}
|
|
25737
|
+
// event === "message"
|
|
25738
|
+
// tina4:edit handle the inbound "message" frame (broadcast, echo, route, etc.)
|
|
25739
|
+
${body}}
|
|
25740
|
+
|
|
25741
|
+
websocket("${wsPath}", ${handlerName});
|
|
25742
|
+
`;
|
|
25743
|
+
writeFileSafe(path8, content);
|
|
25744
|
+
emitWebsocketTest(wsPath, base, handlerName);
|
|
25745
|
+
}
|
|
25746
|
+
function generateListener(name, _flags) {
|
|
25747
|
+
const event = name.trim();
|
|
25748
|
+
const slug = toSnake(event.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
|
|
25749
|
+
const handlerName = `on${toPascal(slug)}`;
|
|
25750
|
+
const dir = resolve10("src/listeners");
|
|
25751
|
+
ensureDir(dir);
|
|
25752
|
+
const path8 = join20(dir, `${slug}.ts`);
|
|
25753
|
+
const body = aiFill(handlerName, {
|
|
25754
|
+
intent: `react to the '${event}' event`,
|
|
25755
|
+
given: `args -> whatever Events.emit("${event}", ...args) passed`,
|
|
25756
|
+
use: "your app code \u2014 Messenger().send(...), an ORM write, or Events.emit(...) a follow-up",
|
|
25757
|
+
ground: `tina4_context("event listener reaction", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
25758
|
+
raise: `listener ${event} not implemented`
|
|
25759
|
+
});
|
|
25760
|
+
const content = `import { Events } from "tina4-nodejs";
|
|
25761
|
+
|
|
25762
|
+
/**
|
|
25763
|
+
* Listener for the '${event}' event.
|
|
25764
|
+
*
|
|
25765
|
+
* Registered on import by Events.on(). Node has NO src/listeners/
|
|
25766
|
+
* auto-discovery, so IMPORT this module once from app.ts to activate it:
|
|
25767
|
+
*
|
|
25768
|
+
* import "./src/listeners/${slug}.js";
|
|
25769
|
+
*
|
|
25770
|
+
* Fires when something calls Events.emit("${event}", ...args).
|
|
25771
|
+
*/
|
|
25772
|
+
export function ${handlerName}(...args: unknown[]): void {
|
|
25773
|
+
// tina4:edit implement the reaction to '${event}' (email, ORM write, follow-up emit)
|
|
25774
|
+
${body}}
|
|
25775
|
+
|
|
25776
|
+
Events.on("${event}", ${handlerName});
|
|
25777
|
+
`;
|
|
25778
|
+
writeFileSafe(path8, content);
|
|
25779
|
+
emitListenerTest(event, slug);
|
|
25780
|
+
}
|
|
25781
|
+
function writeTest(testName, content) {
|
|
25782
|
+
const dir = resolve10("tests");
|
|
25783
|
+
ensureDir(dir);
|
|
25784
|
+
writeFileSafe(join20(dir, `${testName}.test.ts`), content);
|
|
25785
|
+
}
|
|
25786
|
+
function standaloneTest(doc, body) {
|
|
25787
|
+
return `${doc}
|
|
25788
|
+
|
|
25789
|
+
let pass = 0;
|
|
25790
|
+
let fail = 0;
|
|
25791
|
+
function assert(label: string, ok: boolean): void {
|
|
25792
|
+
if (ok) { pass++; console.log(\` PASS \${label}\`); }
|
|
25793
|
+
else { fail++; console.log(\` FAIL \${label}\`); }
|
|
25794
|
+
}
|
|
25795
|
+
async function assertThrows(label: string, fn: () => unknown | Promise<unknown>): Promise<void> {
|
|
25796
|
+
try { await fn(); assert(label, false); }
|
|
25797
|
+
catch { assert(label, true); }
|
|
25798
|
+
}
|
|
25799
|
+
|
|
25800
|
+
${body}
|
|
25801
|
+
|
|
25802
|
+
console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
|
|
25803
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
25804
|
+
`;
|
|
25805
|
+
}
|
|
25806
|
+
function sampleLiteral(fieldType) {
|
|
25807
|
+
switch ((fieldType || "string").toLowerCase()) {
|
|
25808
|
+
case "int":
|
|
25809
|
+
case "integer":
|
|
25810
|
+
return "1";
|
|
25811
|
+
case "float":
|
|
25812
|
+
case "number":
|
|
25813
|
+
case "numeric":
|
|
25814
|
+
case "decimal":
|
|
25815
|
+
return "1.5";
|
|
25816
|
+
case "bool":
|
|
25817
|
+
case "boolean":
|
|
25818
|
+
return "true";
|
|
25819
|
+
case "datetime":
|
|
25820
|
+
return '"2020-01-01 00:00:00"';
|
|
25821
|
+
case "blob":
|
|
25822
|
+
return '"x"';
|
|
25823
|
+
default:
|
|
25824
|
+
return '"sample"';
|
|
25825
|
+
}
|
|
25826
|
+
}
|
|
25827
|
+
function emitModelTest(model, table2, fields) {
|
|
25828
|
+
const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
25829
|
+
const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
|
|
25830
|
+
const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
|
|
25831
|
+
const valueAssert = stringField ? `
|
|
25832
|
+
assert("string field round-trips", fetched !== null && (fetched.toObject() as Record<string, unknown>).${stringField} === "sample");` : "";
|
|
25833
|
+
const doc = `/**
|
|
25834
|
+
* Real ORM roundtrip for ${model} \u2014 no mocks, real SQLite.
|
|
25835
|
+
*
|
|
25836
|
+
* Generated with src/models/${model}.ts by \`tina4nodejs generate model
|
|
25837
|
+
* ${model}\`. The model scaffold is working code, so this passes on generation:
|
|
25838
|
+
* binds a real in-memory SQLite DB, creates the table, saves a row, reads it
|
|
25839
|
+
* back. Run with: npx tsx tests/${table2}_model.test.ts
|
|
25840
|
+
*/
|
|
25841
|
+
import ${model} from "../src/models/${model}.js";
|
|
25842
|
+
import { initDatabase } from "tina4-nodejs/orm";`;
|
|
25843
|
+
const body = `await initDatabase({ url: "sqlite:///:memory:" });
|
|
25844
|
+
await ${model}.createTable();
|
|
25845
|
+
|
|
25846
|
+
const row = new ${model}({ ${payload} });
|
|
25847
|
+
const saved = await row.save();
|
|
25848
|
+
assert("create() persists and returns the row", saved !== false && Boolean(row.toObject().id));
|
|
25849
|
+
|
|
25850
|
+
const id = row.toObject().id;
|
|
25851
|
+
const fetched = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
25852
|
+
assert("row reads back by id", fetched !== null);
|
|
25853
|
+
assert("read-back id matches", fetched !== null && (fetched.toObject() as Record<string, unknown>).id === id);${valueAssert}
|
|
25854
|
+
|
|
25855
|
+
const missing = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [999999]);
|
|
25856
|
+
assert("find missing returns null", missing === null);`;
|
|
25857
|
+
writeTest(`${table2}_model`, standaloneTest(doc, body));
|
|
25858
|
+
}
|
|
25859
|
+
function emitRouteStubTest(route) {
|
|
25860
|
+
const doc = `/**
|
|
25861
|
+
* Routing test for ${route} \u2014 no mocks, real Router + route discovery.
|
|
25862
|
+
*
|
|
25863
|
+
* Generated with src/routes/api/${route}/ by \`tina4nodejs generate route
|
|
25864
|
+
* ${route}\` (no --model). The handlers are AI-FILL stubs that throw until you
|
|
25865
|
+
* implement them, so this tests what IS live on generation: all five routes
|
|
25866
|
+
* register on the REAL Router, and the list handler fails loud until filled.
|
|
25867
|
+
* Run with: npx tsx tests/${route}.test.ts
|
|
25868
|
+
*/
|
|
25869
|
+
import { dirname, resolve } from "node:path";
|
|
25870
|
+
import { fileURLToPath } from "node:url";
|
|
25871
|
+
import { Router, discoverRoutes } from "tina4-nodejs";
|
|
25872
|
+
import listHandler from "../src/routes/api/${route}/get.js";`;
|
|
25873
|
+
const body = `const here = dirname(fileURLToPath(import.meta.url));
|
|
25874
|
+
const router = new Router();
|
|
25875
|
+
const defs = await discoverRoutes(resolve(here, "../src/routes"));
|
|
25876
|
+
for (const def of defs) router.addRoute(def);
|
|
25877
|
+
|
|
25878
|
+
const sigs = defs.map((d) => \`\${d.method} \${d.pattern}\`);
|
|
25879
|
+
for (const sig of ["GET /api/${route}", "POST /api/${route}", "GET /api/${route}/{id}", "PUT /api/${route}/{id}", "DELETE /api/${route}/{id}"]) {
|
|
25880
|
+
assert(\`route registered: \${sig}\`, sigs.includes(sig));
|
|
25881
|
+
}
|
|
25882
|
+
|
|
25883
|
+
// The scaffolded list handler is a loud AI-FILL stub \u2014 it throws until filled.
|
|
25884
|
+
await assertThrows("list handler is a live stub (throws until filled)",
|
|
25885
|
+
() => listHandler({} as never, {} as never));`;
|
|
25886
|
+
writeTest(route, standaloneTest(doc, body));
|
|
25887
|
+
}
|
|
25888
|
+
function emitMiddlewareTest(name, snake) {
|
|
25889
|
+
const doc = `/**
|
|
25890
|
+
* Real dispatch test for the ${name} middleware \u2014 no mocks.
|
|
25891
|
+
*
|
|
25892
|
+
* Generated with src/middleware/${snake}.ts by \`tina4nodejs generate middleware
|
|
25893
|
+
* ${name}\`. Drives the scaffolded before/after functions through the REAL
|
|
25894
|
+
* MiddlewareChain with a real Tina4Request/Response (built from real node http
|
|
25895
|
+
* objects) \u2014 the same continuation dispatch the live server runs.
|
|
25896
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
25897
|
+
*/
|
|
25898
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
25899
|
+
import { Socket } from "node:net";
|
|
25900
|
+
import { MiddlewareChain, createRequest, createResponse } from "tina4-nodejs";
|
|
25901
|
+
import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25902
|
+
import { before${name}, after${name} } from "../src/middleware/${snake}.js";`;
|
|
25903
|
+
const body = `function realPair(headers: Record<string, string>): { req: Tina4Request; res: Tina4Response; raw: ServerResponse } {
|
|
25904
|
+
const socket = new Socket();
|
|
25905
|
+
const rawReq = new IncomingMessage(socket);
|
|
25906
|
+
rawReq.method = "GET";
|
|
25907
|
+
rawReq.url = "/";
|
|
25908
|
+
rawReq.headers = { ...headers, host: "localhost" };
|
|
25909
|
+
rawReq.push(null);
|
|
25910
|
+
const rawRes = new ServerResponse(rawReq);
|
|
25911
|
+
rawRes.write = (() => true) as typeof rawRes.write;
|
|
25912
|
+
rawRes.end = (function (this: ServerResponse) { return this; }) as typeof rawRes.end;
|
|
25913
|
+
return { req: createRequest(rawReq), res: createResponse(rawRes), raw: rawRes };
|
|
25914
|
+
}
|
|
25915
|
+
|
|
25916
|
+
// before(): blocks an unauthenticated request (401, does not call next()).
|
|
25917
|
+
{
|
|
25918
|
+
const chain = new MiddlewareChain();
|
|
25919
|
+
chain.use(before${name});
|
|
25920
|
+
let reached = false;
|
|
25921
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
25922
|
+
const { req, res, raw } = realPair({});
|
|
25923
|
+
await chain.run(req, res);
|
|
25924
|
+
assert("before blocks unauthenticated (401, chain short-circuits)", raw.statusCode === 401 && reached === false);
|
|
25925
|
+
}
|
|
25926
|
+
|
|
25927
|
+
// before(): lets an authenticated request through to the next middleware.
|
|
25928
|
+
{
|
|
25929
|
+
const chain = new MiddlewareChain();
|
|
25930
|
+
chain.use(before${name});
|
|
25931
|
+
let reached = false;
|
|
25932
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
25933
|
+
const { req, res } = realPair({ authorization: "Bearer test" });
|
|
25934
|
+
await chain.run(req, res);
|
|
25935
|
+
assert("before passes an authenticated request through", reached === true);
|
|
25936
|
+
}
|
|
25937
|
+
|
|
25938
|
+
// after(): always continues the chain.
|
|
25939
|
+
{
|
|
25940
|
+
const chain = new MiddlewareChain();
|
|
25941
|
+
chain.use(after${name});
|
|
25942
|
+
let reached = false;
|
|
25943
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
25944
|
+
const { req, res } = realPair({});
|
|
25945
|
+
await chain.run(req, res);
|
|
25946
|
+
assert("after runs and continues the chain", reached === true);
|
|
25947
|
+
}`;
|
|
25948
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
25949
|
+
}
|
|
25950
|
+
function emitServiceTest(name, snake, camel) {
|
|
25951
|
+
const doc = `/**
|
|
25952
|
+
* Real ServiceRunner test for the ${name} service \u2014 no mocks.
|
|
25953
|
+
*
|
|
25954
|
+
* Generated with src/services/${snake}.ts by \`tina4nodejs generate service
|
|
25955
|
+
* ${name}\`. Registers the scaffold on a REAL ServiceRunner and confirms the
|
|
25956
|
+
* descriptor; the task body is an AI-FILL stub that throws until filled.
|
|
25957
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
25958
|
+
*/
|
|
25959
|
+
import { ServiceRunner } from "tina4-nodejs";
|
|
25960
|
+
import service, { ${camel}Task } from "../src/services/${snake}.js";`;
|
|
25961
|
+
const body = `assert("descriptor has a name + callable handler",
|
|
25962
|
+
service.name === "${snake}" && typeof service.handler === "function");
|
|
25963
|
+
|
|
25964
|
+
// Register on a REAL ServiceRunner and confirm it is listed.
|
|
25965
|
+
ServiceRunner.register(service.name, service.handler, { interval: (service as { interval?: number }).interval });
|
|
25966
|
+
assert("registers on a real ServiceRunner", ServiceRunner.list().some((s) => s.name === "${snake}"));
|
|
25967
|
+
ServiceRunner.remove("${snake}");
|
|
25968
|
+
|
|
25969
|
+
// The scaffolded task body is an AI-FILL stub \u2014 it throws until filled.
|
|
25970
|
+
await assertThrows("task is a live stub (throws until filled)", () => ${camel}Task({} as never));`;
|
|
25971
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
25972
|
+
}
|
|
25973
|
+
function emitQueueTest(topic, slug, pascal) {
|
|
25974
|
+
const doc = `/**
|
|
25975
|
+
* Real file-backed Queue test for the ${topic} worker \u2014 no mocks.
|
|
25976
|
+
*
|
|
25977
|
+
* Generated with src/services/${slug}_consumer.ts by \`tina4nodejs generate
|
|
25978
|
+
* queue ${topic}\`. Pushes a REAL job onto the real file-backed Queue and
|
|
25979
|
+
* asserts it is enqueued, and that the consumer is wired as a daemon. The
|
|
25980
|
+
* per-job handle is an AI-FILL stub that throws until filled.
|
|
25981
|
+
* Run with: npx tsx tests/${slug}.test.ts
|
|
25982
|
+
*/
|
|
25983
|
+
import { Queue } from "tina4-nodejs";
|
|
25984
|
+
import worker, { publish${pascal}, handle${pascal} } from "../src/services/${slug}_consumer.js";`;
|
|
25985
|
+
const body = `const jobId = publish${pascal}({ hello: "world" });
|
|
25986
|
+
assert("publish enqueues a real job (returns an id)", typeof jobId === "string" && jobId.length > 0);
|
|
25987
|
+
assert("the job is really on the queue", new Queue({ topic: "${topic}" }).size() >= 1);
|
|
25988
|
+
|
|
25989
|
+
assert("consumer default export is a daemon", worker.daemon === true);
|
|
25990
|
+
assert("consumer handler is wired", typeof worker.handler === "function");
|
|
25991
|
+
|
|
25992
|
+
// The per-job handler is an AI-FILL stub \u2014 it throws until filled.
|
|
25993
|
+
await assertThrows("handle is a live stub (throws until filled)", () => handle${pascal}({}));`;
|
|
25994
|
+
writeTest(slug, standaloneTest(doc, body));
|
|
25995
|
+
}
|
|
25996
|
+
function emitValidatorTest(name, snake, pascal) {
|
|
25997
|
+
const doc = `/**
|
|
25998
|
+
* Real validation test for validate${pascal} \u2014 no mocks.
|
|
25999
|
+
*
|
|
26000
|
+
* Generated with src/validators/${snake}.ts by \`tina4nodejs generate validator
|
|
26001
|
+
* ${name}\`. The scaffold ships a starter rule (required "name"), so this passes
|
|
26002
|
+
* on generation \u2014 adjust the rules for your payload and update these cases.
|
|
26003
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
26004
|
+
*/
|
|
26005
|
+
import { validate${pascal} } from "../src/validators/${snake}.js";`;
|
|
26006
|
+
const body = `assert("valid input passes", validate${pascal}({ name: "Ada" }).isValid());
|
|
26007
|
+
|
|
26008
|
+
const bad = validate${pascal}({});
|
|
26009
|
+
assert("invalid input fails", bad.isValid() === false);
|
|
26010
|
+
assert("invalid input reports errors", bad.errors().length > 0);`;
|
|
26011
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
26012
|
+
}
|
|
26013
|
+
function emitSeederTest(model, table2) {
|
|
26014
|
+
const doc = `/**
|
|
26015
|
+
* Real seeding test for the ${model} seeder \u2014 no mocks, real SQLite.
|
|
26016
|
+
*
|
|
26017
|
+
* Generated with src/seeds/${table2}_seeder.ts by \`tina4nodejs generate seeder
|
|
26018
|
+
* ${model}\`. Binds a real SQLite DB, creates the table, runs the scaffolded
|
|
26019
|
+
* seeder (auto-fills every field via FakeData) and asserts rows were created.
|
|
26020
|
+
* Run with: npx tsx tests/${table2}_seeder.test.ts
|
|
26021
|
+
*/
|
|
26022
|
+
import { initDatabase, FakeData } from "tina4-nodejs/orm";
|
|
26023
|
+
import ${model} from "../src/models/${model}.js";
|
|
26024
|
+
import { fieldOverrides, run } from "../src/seeds/${table2}_seeder.js";`;
|
|
26025
|
+
const body = `process.env.TINA4_DATABASE_URL = "sqlite:///test_${table2}_seeder.db";
|
|
26026
|
+
await initDatabase({ url: process.env.TINA4_DATABASE_URL });
|
|
26027
|
+
await ${model}.createTable();
|
|
26028
|
+
|
|
26029
|
+
assert("fieldOverrides returns an object", typeof fieldOverrides(new FakeData()) === "object");
|
|
26030
|
+
|
|
26031
|
+
await run(); // run() re-binds the same DB URL and seeds via seedOrm
|
|
26032
|
+
const rows = await ${model}.all();
|
|
26033
|
+
assert("run() seeds real rows", rows.length >= 1);`;
|
|
26034
|
+
writeTest(`${table2}_seeder`, standaloneTest(doc, body));
|
|
26035
|
+
}
|
|
26036
|
+
function emitWebsocketTest(wsPath, base, handler) {
|
|
26037
|
+
const doc = `/**
|
|
26038
|
+
* Real handler test for the ${wsPath} WebSocket route \u2014 no mocks.
|
|
26039
|
+
*
|
|
26040
|
+
* Generated with src/routes/ws_${base}.ts by \`tina4nodejs generate websocket
|
|
26041
|
+
* ...\`. Confirms the handler registers on the REAL Router (importing runs the
|
|
26042
|
+
* module-level websocket() call) and drives the real handler for the "close"
|
|
26043
|
+
* event (no socket needed). The "message" branch is an AI-FILL stub that throws
|
|
26044
|
+
* until filled. Run with: npx tsx tests/ws_${base}.test.ts
|
|
26045
|
+
*/
|
|
26046
|
+
import { Router } from "tina4-nodejs";
|
|
26047
|
+
import { ${handler} } from "../src/routes/ws_${base}.js"; // importing registers via websocket()`;
|
|
26048
|
+
const body = `assert("handler registered on the real router",
|
|
26049
|
+
Router.getWebSocketRoutes().some((r) => r.pattern === "${wsPath}"));
|
|
26050
|
+
|
|
26051
|
+
// The "close" branch returns cleanly without a live connection.
|
|
26052
|
+
const closed = await ${handler}(null as never, "close", "");
|
|
26053
|
+
assert("close event handled cleanly", closed === undefined);
|
|
26054
|
+
|
|
26055
|
+
// The "message" branch is an AI-FILL stub \u2014 it throws until filled.
|
|
26056
|
+
await assertThrows("message branch is a live stub (throws until filled)",
|
|
26057
|
+
() => ${handler}(null as never, "message", "hi"));`;
|
|
26058
|
+
writeTest(`ws_${base}`, standaloneTest(doc, body));
|
|
26059
|
+
}
|
|
26060
|
+
function emitListenerTest(event, slug) {
|
|
26061
|
+
const doc = `/**
|
|
26062
|
+
* Real event-bus test for the '${event}' listener \u2014 no mocks.
|
|
26063
|
+
*
|
|
26064
|
+
* Generated with src/listeners/${slug}.ts by \`tina4nodejs generate listener
|
|
26065
|
+
* ${event}\`. Confirms the listener binds on the REAL event bus (importing runs
|
|
26066
|
+
* the module-level Events.on) and that emitting the event reaches it. The
|
|
26067
|
+
* reaction body is an AI-FILL stub, so a strict emit re-raises here (proving it
|
|
26068
|
+
* ran). Run with: npx tsx tests/${slug}.test.ts
|
|
26069
|
+
*/
|
|
26070
|
+
import { Events } from "tina4-nodejs";
|
|
26071
|
+
import "../src/listeners/${slug}.js"; // importing registers the listener via Events.on()`;
|
|
26072
|
+
const body = `assert("listener registered on the real event bus", Events.listeners("${event}").length >= 1);
|
|
26073
|
+
|
|
26074
|
+
// strict emit re-raises the stub error, proving the listener actually ran.
|
|
26075
|
+
await assertThrows("emitting the event reaches the (stub) listener",
|
|
26076
|
+
() => Events.emit("${event}", { strict: true }, { id: 1 }));`;
|
|
26077
|
+
writeTest(slug, standaloneTest(doc, body));
|
|
26078
|
+
}
|
|
26079
|
+
function emitAuthTest() {
|
|
26080
|
+
const doc = `/**
|
|
26081
|
+
* Real auth test \u2014 register / login / me via the real TestClient.
|
|
26082
|
+
*
|
|
26083
|
+
* Generated with the auth scaffold by \`tina4nodejs generate auth\`. No mocks:
|
|
26084
|
+
* real Router + route discovery, real Auth (PBKDF2 + JWT), real SQLite. register
|
|
26085
|
+
* + login are public; the token from login authenticates GET /api/auth/me.
|
|
26086
|
+
* Run with: npx tsx tests/auth.test.ts
|
|
26087
|
+
*/
|
|
26088
|
+
import { dirname, resolve } from "node:path";
|
|
26089
|
+
import { fileURLToPath } from "node:url";
|
|
26090
|
+
import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
|
|
26091
|
+
import { initDatabase } from "tina4-nodejs/orm";
|
|
26092
|
+
import User from "../src/models/User.js";
|
|
26093
|
+
|
|
26094
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
26095
|
+
delete process.env.TINA4_API_KEY;
|
|
26096
|
+
const here = dirname(fileURLToPath(import.meta.url));`;
|
|
26097
|
+
const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
|
|
26098
|
+
await User.createTable();
|
|
26099
|
+
for (const existing of await User.all()) await existing.delete(); // start from an empty table
|
|
26100
|
+
|
|
26101
|
+
const router = new Router();
|
|
26102
|
+
for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
|
|
26103
|
+
const client = new TestClient(router);
|
|
26104
|
+
|
|
26105
|
+
const registered = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
|
|
26106
|
+
assert("register a new user -> 201", registered.status === 201);
|
|
26107
|
+
|
|
26108
|
+
const duplicate = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
|
|
26109
|
+
assert("duplicate register -> 409", duplicate.status === 409);
|
|
26110
|
+
|
|
26111
|
+
const login = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "secret12" } });
|
|
26112
|
+
assert("login -> 200", login.status === 200);
|
|
26113
|
+
const token = (login.json() as { token?: string }).token;
|
|
26114
|
+
assert("login returns a token", typeof token === "string" && token.length > 0);
|
|
26115
|
+
|
|
26116
|
+
const me = await client.get("/api/auth/me", { headers: { authorization: \`Bearer \${token}\` } });
|
|
26117
|
+
assert("authenticated GET /api/auth/me -> 200 (token accepted)", me.status === 200);
|
|
26118
|
+
assert("me returns the authenticated user's email",
|
|
26119
|
+
((me.json() as { user?: { email?: string } }).user?.email) === "a@b.c");
|
|
26120
|
+
|
|
26121
|
+
const anon = await client.get("/api/auth/me");
|
|
26122
|
+
assert("anonymous GET /api/auth/me -> 401", anon.status === 401);
|
|
26123
|
+
|
|
26124
|
+
const bad = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "WRONG" } });
|
|
26125
|
+
assert("wrong password -> 401", bad.status === 401);`;
|
|
26126
|
+
writeTest("auth", standaloneTest(doc, body));
|
|
26127
|
+
}
|
|
26128
|
+
function emitMigrationTest(migrationName, table2) {
|
|
26129
|
+
const doc = `/**
|
|
26130
|
+
* Real migration test for ${migrationName} \u2014 no mocks, real SQLite.
|
|
26131
|
+
*
|
|
26132
|
+
* Generated with the migration by \`tina4nodejs generate migration
|
|
26133
|
+
* ${migrationName}\`. Applies the generated UP SQL against a fresh real
|
|
26134
|
+
* in-memory SQLite database and asserts the table exists, then applies the DOWN
|
|
26135
|
+
* SQL and asserts it is gone \u2014 the raw SQL the migration runner executes.
|
|
26136
|
+
* Run with: npx tsx tests/${table2}_migration.test.ts
|
|
26137
|
+
*/
|
|
26138
|
+
import { dirname, join, resolve } from "node:path";
|
|
26139
|
+
import { fileURLToPath } from "node:url";
|
|
26140
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
26141
|
+
import { SQLiteAdapter } from "tina4-nodejs/orm";`;
|
|
26142
|
+
const body = `const here = dirname(fileURLToPath(import.meta.url));
|
|
26143
|
+
const migrationsDir = resolve(here, "../migrations");
|
|
26144
|
+
|
|
26145
|
+
const upFile = readdirSync(migrationsDir).find((f) => f.endsWith("_${migrationName}.sql") && !f.endsWith(".down.sql"));
|
|
26146
|
+
assert("generated UP migration file exists", Boolean(upFile));
|
|
26147
|
+
const downFile = upFile!.replace(/\\.sql$/, ".down.sql");
|
|
26148
|
+
|
|
26149
|
+
function statements(sql: string): string[] {
|
|
26150
|
+
const noComments = sql.split("\\n").filter((l) => !l.trim().startsWith("--")).join("\\n");
|
|
26151
|
+
return noComments.split(";").map((s) => s.trim()).filter(Boolean);
|
|
26152
|
+
}
|
|
26153
|
+
|
|
26154
|
+
const upText = readFileSync(join(migrationsDir, upFile!), "utf-8");
|
|
26155
|
+
const upSql = upText.split("-- UP")[1].split("-- DOWN")[0];
|
|
26156
|
+
const downSql = readFileSync(join(migrationsDir, downFile), "utf-8");
|
|
26157
|
+
|
|
26158
|
+
const db = new SQLiteAdapter(":memory:");
|
|
26159
|
+
for (const stmt of statements(upSql)) db.execute(stmt);
|
|
26160
|
+
assert("UP creates the ${table2} table", db.tableExists("${table2}"));
|
|
26161
|
+
|
|
26162
|
+
for (const stmt of statements(downSql)) db.execute(stmt);
|
|
26163
|
+
assert("DOWN drops the ${table2} table", db.tableExists("${table2}") === false);`;
|
|
26164
|
+
writeTest(`${table2}_migration`, standaloneTest(doc, body));
|
|
26165
|
+
}
|
|
26166
|
+
var FIELD_TYPE_MAP, SQL_RESERVED_TABLE_NAMES, RESOLUTION_ENVELOPE_VERSION, __resolution, TINA4_EDIT_MARKER, DEFAULT_FIELDS, GENERATORS, GENERATOR_LIST, NEXT_STEPS;
|
|
26167
|
+
var init_generate = __esm({
|
|
26168
|
+
"../cli/src/commands/generate.ts"() {
|
|
26169
|
+
"use strict";
|
|
26170
|
+
FIELD_TYPE_MAP = {
|
|
26171
|
+
string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
26172
|
+
str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
26173
|
+
int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
26174
|
+
integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
26175
|
+
float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
26176
|
+
number: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
26177
|
+
numeric: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
26178
|
+
decimal: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
26179
|
+
bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
26180
|
+
boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
26181
|
+
text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
26182
|
+
datetime: { orm: '"datetime"', sql: "TEXT", defaultVal: "NULL" },
|
|
26183
|
+
blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
|
|
26184
|
+
};
|
|
26185
|
+
SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
|
|
26186
|
+
"order",
|
|
26187
|
+
"group",
|
|
26188
|
+
"user",
|
|
26189
|
+
"table",
|
|
26190
|
+
"select",
|
|
26191
|
+
"from",
|
|
26192
|
+
"where",
|
|
26193
|
+
"index",
|
|
26194
|
+
"key",
|
|
26195
|
+
"values",
|
|
26196
|
+
"column",
|
|
26197
|
+
"constraint",
|
|
26198
|
+
"check",
|
|
26199
|
+
"default",
|
|
26200
|
+
"primary",
|
|
26201
|
+
"foreign",
|
|
26202
|
+
"references",
|
|
26203
|
+
"unique",
|
|
26204
|
+
"join",
|
|
26205
|
+
"union",
|
|
26206
|
+
"having",
|
|
26207
|
+
"limit",
|
|
26208
|
+
"offset",
|
|
26209
|
+
"desc",
|
|
26210
|
+
"asc",
|
|
26211
|
+
"case",
|
|
26212
|
+
"when",
|
|
26213
|
+
"then",
|
|
26214
|
+
"else",
|
|
26215
|
+
"end",
|
|
26216
|
+
"and",
|
|
26217
|
+
"or",
|
|
26218
|
+
"not",
|
|
26219
|
+
"null",
|
|
26220
|
+
"insert",
|
|
26221
|
+
"update",
|
|
26222
|
+
"delete",
|
|
26223
|
+
"create",
|
|
26224
|
+
"drop",
|
|
26225
|
+
"alter",
|
|
26226
|
+
"grant",
|
|
26227
|
+
"revoke",
|
|
26228
|
+
"commit",
|
|
26229
|
+
"rollback",
|
|
26230
|
+
"view",
|
|
26231
|
+
"trigger",
|
|
26232
|
+
"procedure",
|
|
26233
|
+
"function",
|
|
26234
|
+
"database",
|
|
26235
|
+
"schema",
|
|
26236
|
+
"session",
|
|
26237
|
+
"set",
|
|
26238
|
+
"into",
|
|
26239
|
+
"as",
|
|
26240
|
+
"on",
|
|
26241
|
+
"by",
|
|
26242
|
+
"inner",
|
|
26243
|
+
"outer",
|
|
26244
|
+
"left",
|
|
26245
|
+
"right",
|
|
26246
|
+
"full",
|
|
26247
|
+
"natural",
|
|
26248
|
+
"using",
|
|
26249
|
+
"with",
|
|
26250
|
+
"distinct",
|
|
26251
|
+
"between",
|
|
26252
|
+
"exists",
|
|
26253
|
+
"like",
|
|
26254
|
+
"in",
|
|
26255
|
+
"is",
|
|
26256
|
+
"all",
|
|
26257
|
+
"any",
|
|
26258
|
+
"cross",
|
|
26259
|
+
"add",
|
|
26260
|
+
"row",
|
|
26261
|
+
"rows",
|
|
26262
|
+
"range",
|
|
26263
|
+
"current",
|
|
26264
|
+
"to"
|
|
26265
|
+
]);
|
|
26266
|
+
RESOLUTION_ENVELOPE_VERSION = "generate_v1_1";
|
|
26267
|
+
__resolution = {
|
|
26268
|
+
target: "",
|
|
26269
|
+
input: { name: "", fields: null },
|
|
26270
|
+
body: { transformations: [] },
|
|
26271
|
+
actionsTaken: [],
|
|
26272
|
+
dryRun: false,
|
|
26273
|
+
jsonMode: false
|
|
26274
|
+
};
|
|
26275
|
+
TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
|
|
26276
|
+
DEFAULT_FIELDS = [["name", "string"]];
|
|
26277
|
+
GENERATORS = {
|
|
26278
|
+
model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
|
|
26279
|
+
route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
26280
|
+
crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
|
|
26281
|
+
migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
26282
|
+
middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
|
|
26283
|
+
test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
|
|
26284
|
+
form: { handler: generateForm, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" },
|
|
26285
|
+
view: { handler: generateView, usage: '<Name> [--fields "..."]', summary: "List + detail view templates" },
|
|
26286
|
+
auth: { handler: (_n, f) => generateAuth(f), usage: "", summary: "Login/register routes (public) + User model + templates" },
|
|
26287
|
+
service: { handler: generateService, usage: '<Name> [--every 5m | --cron "..."]', summary: "Scheduled ServiceRunner task (src/services/)" },
|
|
26288
|
+
queue: { handler: generateQueue, usage: "<topic>", summary: "Producer + consumer daemon worker (src/services/)" },
|
|
26289
|
+
validator: { handler: generateValidator, usage: "<Name>", summary: "Request-body Validator (src/validators/)" },
|
|
26290
|
+
seeder: { handler: generateSeeder, usage: "<Model>", summary: "FakeData + seedOrm seeder (src/seeds/)" },
|
|
26291
|
+
websocket: { handler: generateWebsocket, usage: "<path>", summary: "websocket() handler (src/routes/)" },
|
|
26292
|
+
listener: { handler: generateListener, usage: "<event>", summary: "Events.on(event) listener (src/listeners/)" }
|
|
26293
|
+
};
|
|
26294
|
+
GENERATOR_LIST = Object.keys(GENERATORS).join(", ");
|
|
26295
|
+
NEXT_STEPS = {
|
|
26296
|
+
model: ({ name, table: table2 }) => [
|
|
26297
|
+
`Edit src/models/${name}.ts to add fields beyond the default 'name'`,
|
|
26298
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
26299
|
+
`Run its test: npx tsx tests/${table2}_model.test.ts`,
|
|
26300
|
+
`Add CRUD scaffolding: npx tina4nodejs generate crud ${name}`
|
|
26301
|
+
],
|
|
26302
|
+
route: ({ name, table: table2 }) => [
|
|
26303
|
+
`Fill the AI-FILL stubs in src/routes/api/${name.replace(/^\//, "")}/`,
|
|
26304
|
+
`Run its test: npx tsx tests/${table2}.test.ts`,
|
|
26305
|
+
`Serve and try: npx tina4nodejs serve -> curl http://localhost:7148/api/${name.replace(/^\//, "")}`
|
|
26306
|
+
],
|
|
26307
|
+
crud: ({ name, table: table2 }) => [
|
|
26308
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
26309
|
+
`Serve and try: npx tina4nodejs serve -> visit /swagger`,
|
|
26310
|
+
`Run the gate test: npx tsx tests/${toPlural(table2)}.test.ts`,
|
|
26311
|
+
`Change fields: edit src/models/${name}.ts then re-run generate crud`
|
|
26312
|
+
],
|
|
26313
|
+
migration: () => [
|
|
26314
|
+
`Apply pending migrations: npx tina4nodejs migrate`,
|
|
26315
|
+
`Check status: npx tina4nodejs migrate:status`,
|
|
26316
|
+
`Roll back the batch: npx tina4nodejs migrate:rollback`
|
|
26317
|
+
],
|
|
26318
|
+
middleware: ({ name }) => [
|
|
26319
|
+
`Wire it: router.middleware(before${name}, after${name}) \u2014 or bind per-route`,
|
|
26320
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
26321
|
+
],
|
|
26322
|
+
test: ({ name }) => [
|
|
26323
|
+
`Fill the TODOs in tests/${toSnake(name)}.test.ts`,
|
|
26324
|
+
`Run it: npx tsx tests/${toSnake(name)}.test.ts`
|
|
26325
|
+
],
|
|
26326
|
+
form: ({ name, table: table2 }) => [
|
|
26327
|
+
`Render from a route: res.render("forms/${table2}.twig", { item })`,
|
|
26328
|
+
`Add the POST route: npx tina4nodejs generate route ${toPlural(table2)} --model ${name}`
|
|
26329
|
+
],
|
|
26330
|
+
view: ({ table: table2 }) => [
|
|
26331
|
+
`Wire routes to render list -> ${toPlural(table2)}.twig, detail -> ${table2}.twig`,
|
|
26332
|
+
`Customize the templates in src/templates/pages/`
|
|
26333
|
+
],
|
|
26334
|
+
auth: () => [
|
|
26335
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
26336
|
+
`Run the auth test: npx tsx tests/auth.test.ts`,
|
|
26337
|
+
`Try register: curl -X POST http://localhost:7148/api/auth/register -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`,
|
|
26338
|
+
`Login: curl -X POST http://localhost:7148/api/auth/login -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`
|
|
26339
|
+
],
|
|
26340
|
+
service: ({ name }) => [
|
|
26341
|
+
`Wire ServiceRunner in app.ts: await ServiceRunner.discover("src/services"); ServiceRunner.start();`,
|
|
26342
|
+
`Fill the task body in src/services/${toSnake(name)}.ts`,
|
|
26343
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
26344
|
+
],
|
|
26345
|
+
queue: ({ name }) => {
|
|
26346
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
|
|
26347
|
+
return [
|
|
26348
|
+
`Fill handle${toPascal(name)}() in src/services/${slug}_consumer.ts`,
|
|
26349
|
+
`Produce a job: publish${toPascal(name)}({ ... })`,
|
|
26350
|
+
`Run the worker: npx tina4nodejs queue work ${name}`,
|
|
26351
|
+
`Run its test: npx tsx tests/${slug}.test.ts`
|
|
26352
|
+
];
|
|
26353
|
+
},
|
|
26354
|
+
validator: ({ name }) => [
|
|
26355
|
+
`Add rules in src/validators/${toSnake(name)}.ts (.email/.minLength/.integer/.inList/.pattern)`,
|
|
26356
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
26357
|
+
],
|
|
26358
|
+
seeder: ({ name, table: table2 }) => [
|
|
26359
|
+
`Override any fields that need a specific shape in src/seeds/${table2}_seeder.ts`,
|
|
26360
|
+
`Seed the table: npx tina4nodejs seed`,
|
|
26361
|
+
`Run its test: npx tsx tests/${table2}_seeder.test.ts`
|
|
26362
|
+
],
|
|
26363
|
+
websocket: ({ name }) => {
|
|
26364
|
+
const raw = name.trim();
|
|
26365
|
+
const slugRaw = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
|
|
26366
|
+
const base = slugRaw.startsWith("ws_") ? slugRaw.slice(3) : slugRaw;
|
|
26367
|
+
return [
|
|
26368
|
+
`Import once in app.ts to register: import "./src/routes/ws_${base}.js";`,
|
|
26369
|
+
`Fill the "message" branch in src/routes/ws_${base}.ts`,
|
|
26370
|
+
`Run its test: npx tsx tests/ws_${base}.test.ts`
|
|
26371
|
+
];
|
|
26372
|
+
},
|
|
26373
|
+
listener: ({ name }) => {
|
|
26374
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
|
|
26375
|
+
return [
|
|
26376
|
+
`Import once in app.ts to register: import "./src/listeners/${slug}.js";`,
|
|
26377
|
+
`Fill the reaction in src/listeners/${slug}.ts`,
|
|
26378
|
+
`Run its test: npx tsx tests/${slug}.test.ts`
|
|
26379
|
+
];
|
|
26380
|
+
}
|
|
26381
|
+
};
|
|
26382
|
+
}
|
|
26383
|
+
});
|
|
26384
|
+
|
|
24326
26385
|
// src/mcp.ts
|
|
24327
26386
|
var mcp_exports = {};
|
|
24328
26387
|
__export(mcp_exports, {
|
|
@@ -24823,10 +26882,10 @@ function registerDevTools(server) {
|
|
|
24823
26882
|
"swagger_spec",
|
|
24824
26883
|
(_args) => {
|
|
24825
26884
|
try {
|
|
24826
|
-
const { generate:
|
|
26885
|
+
const { generate: generate3 } = reqSibling("swagger");
|
|
24827
26886
|
const { defaultRouter: defaultRouter2 } = req("./router.js");
|
|
24828
26887
|
const routes = defaultRouter2?.getRoutes?.() ?? [];
|
|
24829
|
-
return
|
|
26888
|
+
return generate3?.(routes, []) ?? { info: "Swagger not available" };
|
|
24830
26889
|
} catch (e) {
|
|
24831
26890
|
return { error: e.message };
|
|
24832
26891
|
}
|
|
@@ -24985,20 +27044,43 @@ function registerDevTools(server) {
|
|
|
24985
27044
|
);
|
|
24986
27045
|
server.registerTool(
|
|
24987
27046
|
"migration_create",
|
|
24988
|
-
(args) => {
|
|
24989
|
-
|
|
24990
|
-
|
|
24991
|
-
|
|
24992
|
-
|
|
24993
|
-
|
|
24994
|
-
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
|
|
24999
|
-
|
|
27047
|
+
async (args) => {
|
|
27048
|
+
try {
|
|
27049
|
+
const rawDesc = String(args.description ?? "").trim();
|
|
27050
|
+
if (!rawDesc) return { ok: false, error: "description is required" };
|
|
27051
|
+
const slug = rawDesc.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
27052
|
+
if (!slug) return { ok: false, error: "description sanitised to an empty slug" };
|
|
27053
|
+
const migrationsDir = path3.join(projectRoot3, "migrations");
|
|
27054
|
+
if (fs4.existsSync(migrationsDir)) {
|
|
27055
|
+
const upSuffix = `_${slug}.sql`;
|
|
27056
|
+
const downSuffix = `_${slug}.down.sql`;
|
|
27057
|
+
const existing = fs4.readdirSync(migrationsDir).filter(
|
|
27058
|
+
(f) => f.endsWith(upSuffix) && !f.endsWith(downSuffix) || f.endsWith(downSuffix)
|
|
27059
|
+
);
|
|
27060
|
+
if (existing.length > 0) {
|
|
27061
|
+
return {
|
|
27062
|
+
ok: false,
|
|
27063
|
+
error: `A migration with slug "${slug}" already exists`,
|
|
27064
|
+
existing
|
|
27065
|
+
};
|
|
27066
|
+
}
|
|
27067
|
+
}
|
|
27068
|
+
const originalCwd = process.cwd();
|
|
27069
|
+
try {
|
|
27070
|
+
process.chdir(projectRoot3);
|
|
27071
|
+
const gen = await Promise.resolve().then(() => (init_generate(), generate_exports));
|
|
27072
|
+
const envelope = await gen.generateProgrammatic("migration", slug, ["--no-test"]);
|
|
27073
|
+
const migrationPath = envelope.resolution?.migration_path;
|
|
27074
|
+
const created = migrationPath ? path3.basename(migrationPath) : "";
|
|
27075
|
+
return { ok: true, created, resolution: envelope };
|
|
27076
|
+
} finally {
|
|
27077
|
+
process.chdir(originalCwd);
|
|
27078
|
+
}
|
|
27079
|
+
} catch (e) {
|
|
27080
|
+
return { ok: false, error: e.message };
|
|
27081
|
+
}
|
|
25000
27082
|
},
|
|
25001
|
-
"Create a new migration file",
|
|
27083
|
+
"Create a new migration file (delegates to `generate migration` \u2014 emits the ADR-0063 generate_v1_1 envelope + timestamped filename)",
|
|
25002
27084
|
schemaFromParams([{ name: "description", type: "string" }])
|
|
25003
27085
|
);
|
|
25004
27086
|
server.registerTool(
|
|
@@ -25766,14 +27848,14 @@ data: ${channel.buffer.shift()}
|
|
|
25766
27848
|
`;
|
|
25767
27849
|
continue;
|
|
25768
27850
|
}
|
|
25769
|
-
const gotMessage = await new Promise((
|
|
27851
|
+
const gotMessage = await new Promise((resolve21) => {
|
|
25770
27852
|
const timer = setTimeout(() => {
|
|
25771
27853
|
channel.wake = null;
|
|
25772
|
-
|
|
27854
|
+
resolve21(false);
|
|
25773
27855
|
}, keepaliveMs);
|
|
25774
27856
|
channel.wake = () => {
|
|
25775
27857
|
clearTimeout(timer);
|
|
25776
|
-
|
|
27858
|
+
resolve21(true);
|
|
25777
27859
|
};
|
|
25778
27860
|
});
|
|
25779
27861
|
if (!gotMessage) yield `: keep-alive
|
|
@@ -27092,8 +29174,8 @@ __export(context_exports, {
|
|
|
27092
29174
|
fts5Supported: () => fts5Supported
|
|
27093
29175
|
});
|
|
27094
29176
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
27095
|
-
import { existsSync as
|
|
27096
|
-
import { basename as basename4, dirname as dirname10, extname as extname5, isAbsolute as isAbsolute6, join as
|
|
29177
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync15, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
|
|
29178
|
+
import { basename as basename4, dirname as dirname10, extname as extname5, isAbsolute as isAbsolute6, join as join22, relative as relative4, resolve as resolve12 } from "node:path";
|
|
27097
29179
|
function fts5Supported() {
|
|
27098
29180
|
try {
|
|
27099
29181
|
const conn = new DatabaseSync4(":memory:");
|
|
@@ -27116,13 +29198,13 @@ function realResolve(abs) {
|
|
|
27116
29198
|
} catch {
|
|
27117
29199
|
}
|
|
27118
29200
|
try {
|
|
27119
|
-
return
|
|
29201
|
+
return join22(realpathSync5(dirname10(abs)), basename4(abs));
|
|
27120
29202
|
} catch {
|
|
27121
29203
|
return abs;
|
|
27122
29204
|
}
|
|
27123
29205
|
}
|
|
27124
29206
|
function dbKey(db) {
|
|
27125
|
-
return
|
|
29207
|
+
return resolve12(db ? String(db) : join22(process.cwd(), ".tina4", "context.db"));
|
|
27126
29208
|
}
|
|
27127
29209
|
function defaultContext(root, db) {
|
|
27128
29210
|
const key = dbKey(db);
|
|
@@ -27206,7 +29288,7 @@ var init_context = __esm({
|
|
|
27206
29288
|
if (!this.available) return;
|
|
27207
29289
|
const parent = dirname10(this.path);
|
|
27208
29290
|
if (parent !== "" && parent !== ".") {
|
|
27209
|
-
|
|
29291
|
+
mkdirSync14(parent, { recursive: true });
|
|
27210
29292
|
}
|
|
27211
29293
|
this.conn = new DatabaseSync4(this.path);
|
|
27212
29294
|
this.ensureTable();
|
|
@@ -27278,7 +29360,7 @@ var init_context = __esm({
|
|
|
27278
29360
|
*/
|
|
27279
29361
|
indexRoot(root) {
|
|
27280
29362
|
if (!this.available) return 0;
|
|
27281
|
-
const rootAbs = realResolve(
|
|
29363
|
+
const rootAbs = realResolve(resolve12(String(root)));
|
|
27282
29364
|
this.root = rootAbs;
|
|
27283
29365
|
let total = 0;
|
|
27284
29366
|
const walk2 = (dir) => {
|
|
@@ -27295,11 +29377,11 @@ var init_context = __esm({
|
|
|
27295
29377
|
files.sort();
|
|
27296
29378
|
for (const fn of files) {
|
|
27297
29379
|
if (!_Context.eligible(fn)) continue;
|
|
27298
|
-
const full =
|
|
27299
|
-
const rel =
|
|
29380
|
+
const full = join22(dir, fn);
|
|
29381
|
+
const rel = relative4(rootAbs, full);
|
|
27300
29382
|
total += this.indexPath(full, rel);
|
|
27301
29383
|
}
|
|
27302
|
-
for (const d of subdirs) walk2(
|
|
29384
|
+
for (const d of subdirs) walk2(join22(dir, d));
|
|
27303
29385
|
};
|
|
27304
29386
|
walk2(rootAbs);
|
|
27305
29387
|
return total;
|
|
@@ -27315,9 +29397,9 @@ var init_context = __esm({
|
|
|
27315
29397
|
reindexFile(changedPath) {
|
|
27316
29398
|
if (!this.available || this.root === null) return -1;
|
|
27317
29399
|
const raw = String(changedPath);
|
|
27318
|
-
const abs = isAbsolute6(raw) ? raw :
|
|
27319
|
-
const resolved = realResolve(
|
|
27320
|
-
const rel =
|
|
29400
|
+
const abs = isAbsolute6(raw) ? raw : join22(process.cwd(), raw);
|
|
29401
|
+
const resolved = realResolve(resolve12(abs));
|
|
29402
|
+
const rel = relative4(this.root, resolved);
|
|
27321
29403
|
if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) {
|
|
27322
29404
|
return -1;
|
|
27323
29405
|
}
|
|
@@ -27328,7 +29410,7 @@ var init_context = __esm({
|
|
|
27328
29410
|
}
|
|
27329
29411
|
if (!_Context.eligible(basename4(rel))) return -1;
|
|
27330
29412
|
const stored = rel;
|
|
27331
|
-
if (!
|
|
29413
|
+
if (!existsSync17(abs)) {
|
|
27332
29414
|
this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
|
|
27333
29415
|
return 0;
|
|
27334
29416
|
}
|
|
@@ -28074,7 +30156,7 @@ var init_websocket = __esm({
|
|
|
28074
30156
|
* Start the WebSocket server.
|
|
28075
30157
|
*/
|
|
28076
30158
|
async start() {
|
|
28077
|
-
return new Promise((
|
|
30159
|
+
return new Promise((resolve21, reject) => {
|
|
28078
30160
|
this.server = createServer((req2, res) => {
|
|
28079
30161
|
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
28080
30162
|
res.end("Upgrade Required");
|
|
@@ -28084,7 +30166,7 @@ var init_websocket = __esm({
|
|
|
28084
30166
|
});
|
|
28085
30167
|
this.server.listen(this.port, () => {
|
|
28086
30168
|
this.startIdleReaper();
|
|
28087
|
-
|
|
30169
|
+
resolve21();
|
|
28088
30170
|
});
|
|
28089
30171
|
this.server.on("error", (err) => {
|
|
28090
30172
|
this.emit("error", err);
|
|
@@ -29305,8 +31387,8 @@ var init_job = __esm({
|
|
|
29305
31387
|
});
|
|
29306
31388
|
|
|
29307
31389
|
// src/queueBackends/liteBackend.ts
|
|
29308
|
-
import { mkdirSync as
|
|
29309
|
-
import { join as
|
|
31390
|
+
import { mkdirSync as mkdirSync15, readdirSync as readdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7, existsSync as existsSync18 } from "node:fs";
|
|
31391
|
+
import { join as join23 } from "node:path";
|
|
29310
31392
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
29311
31393
|
var LiteBackend;
|
|
29312
31394
|
var init_liteBackend = __esm({
|
|
@@ -29330,22 +31412,22 @@ var init_liteBackend = __esm({
|
|
|
29330
31412
|
this.visibilityTimeout = visibilityTimeout;
|
|
29331
31413
|
}
|
|
29332
31414
|
ensureDir(queue) {
|
|
29333
|
-
const dir =
|
|
29334
|
-
|
|
31415
|
+
const dir = join23(this.basePath, queue);
|
|
31416
|
+
mkdirSync15(dir, { recursive: true });
|
|
29335
31417
|
return dir;
|
|
29336
31418
|
}
|
|
29337
31419
|
ensureFailedDir(queue) {
|
|
29338
|
-
const dir =
|
|
29339
|
-
|
|
31420
|
+
const dir = join23(this.basePath, queue, "failed");
|
|
31421
|
+
mkdirSync15(dir, { recursive: true });
|
|
29340
31422
|
return dir;
|
|
29341
31423
|
}
|
|
29342
31424
|
ensureReservedDir(queue) {
|
|
29343
|
-
const dir =
|
|
29344
|
-
|
|
31425
|
+
const dir = join23(this.basePath, queue, "reserved");
|
|
31426
|
+
mkdirSync15(dir, { recursive: true });
|
|
29345
31427
|
return dir;
|
|
29346
31428
|
}
|
|
29347
31429
|
reservedPath(queue, jobId) {
|
|
29348
|
-
return
|
|
31430
|
+
return join23(this.ensureReservedDir(queue), `${jobId}.queue-data`);
|
|
29349
31431
|
}
|
|
29350
31432
|
nowIso() {
|
|
29351
31433
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -29383,7 +31465,7 @@ var init_liteBackend = __esm({
|
|
|
29383
31465
|
error: void 0
|
|
29384
31466
|
};
|
|
29385
31467
|
const prefix = this.nextPrefix();
|
|
29386
|
-
|
|
31468
|
+
writeFileSync11(join23(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
|
|
29387
31469
|
return id;
|
|
29388
31470
|
}
|
|
29389
31471
|
/**
|
|
@@ -29402,7 +31484,7 @@ var init_liteBackend = __esm({
|
|
|
29402
31484
|
}
|
|
29403
31485
|
const candidates = [];
|
|
29404
31486
|
for (const filename of filenames) {
|
|
29405
|
-
const filePath =
|
|
31487
|
+
const filePath = join23(dir, filename);
|
|
29406
31488
|
let job;
|
|
29407
31489
|
try {
|
|
29408
31490
|
job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
@@ -29445,7 +31527,7 @@ var init_liteBackend = __esm({
|
|
|
29445
31527
|
createdAt: job.createdAt ?? now,
|
|
29446
31528
|
topic: job.topic ?? queue
|
|
29447
31529
|
};
|
|
29448
|
-
|
|
31530
|
+
writeFileSync11(this.reservedPath(queue, record.id), JSON.stringify(record, null, 2));
|
|
29449
31531
|
}
|
|
29450
31532
|
/**
|
|
29451
31533
|
* Return expired reservations to the queue (at-least-once delivery).
|
|
@@ -29466,7 +31548,7 @@ var init_liteBackend = __esm({
|
|
|
29466
31548
|
return;
|
|
29467
31549
|
}
|
|
29468
31550
|
for (const filename of filenames) {
|
|
29469
|
-
const filePath =
|
|
31551
|
+
const filePath = join23(reservedDir, filename);
|
|
29470
31552
|
let record;
|
|
29471
31553
|
try {
|
|
29472
31554
|
record = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
@@ -29504,7 +31586,7 @@ var init_liteBackend = __esm({
|
|
|
29504
31586
|
this.reclaimExpired(queue, bridge.getMaxRetries(), this.nowIso());
|
|
29505
31587
|
const now = this.nowIso();
|
|
29506
31588
|
for (const [filename, job] of this.availableCandidates(queue, now)) {
|
|
29507
|
-
const filePath =
|
|
31589
|
+
const filePath = join23(dir, filename);
|
|
29508
31590
|
job.topic = queue;
|
|
29509
31591
|
job.priority = job.priority ?? 0;
|
|
29510
31592
|
this.writeReserved(queue, job);
|
|
@@ -29529,7 +31611,7 @@ var init_liteBackend = __esm({
|
|
|
29529
31611
|
const results = [];
|
|
29530
31612
|
for (const [filename, job] of this.availableCandidates(queue, now)) {
|
|
29531
31613
|
if (results.length >= count) break;
|
|
29532
|
-
const filePath =
|
|
31614
|
+
const filePath = join23(dir, filename);
|
|
29533
31615
|
job.topic = queue;
|
|
29534
31616
|
job.priority = job.priority ?? 0;
|
|
29535
31617
|
this.writeReserved(queue, job);
|
|
@@ -29582,7 +31664,7 @@ var init_liteBackend = __esm({
|
|
|
29582
31664
|
let count = 0;
|
|
29583
31665
|
for (const file of files) {
|
|
29584
31666
|
try {
|
|
29585
|
-
const job = JSON.parse(readFileSync16(
|
|
31667
|
+
const job = JSON.parse(readFileSync16(join23(scanDir, file), "utf-8"));
|
|
29586
31668
|
if (job.status === status2) count++;
|
|
29587
31669
|
} catch {
|
|
29588
31670
|
}
|
|
@@ -29595,28 +31677,28 @@ var init_liteBackend = __esm({
|
|
|
29595
31677
|
try {
|
|
29596
31678
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
|
|
29597
31679
|
for (const file of files) {
|
|
29598
|
-
unlinkSync7(
|
|
31680
|
+
unlinkSync7(join23(dir, file));
|
|
29599
31681
|
count++;
|
|
29600
31682
|
}
|
|
29601
31683
|
} catch {
|
|
29602
31684
|
}
|
|
29603
|
-
const failedDir =
|
|
31685
|
+
const failedDir = join23(dir, "failed");
|
|
29604
31686
|
try {
|
|
29605
|
-
if (
|
|
31687
|
+
if (existsSync18(failedDir)) {
|
|
29606
31688
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
29607
31689
|
for (const file of files) {
|
|
29608
|
-
unlinkSync7(
|
|
31690
|
+
unlinkSync7(join23(failedDir, file));
|
|
29609
31691
|
count++;
|
|
29610
31692
|
}
|
|
29611
31693
|
}
|
|
29612
31694
|
} catch {
|
|
29613
31695
|
}
|
|
29614
|
-
const reservedDir =
|
|
31696
|
+
const reservedDir = join23(dir, "reserved");
|
|
29615
31697
|
try {
|
|
29616
|
-
if (
|
|
31698
|
+
if (existsSync18(reservedDir)) {
|
|
29617
31699
|
const files = readdirSync11(reservedDir).filter((f) => f.endsWith(".queue-data"));
|
|
29618
31700
|
for (const file of files) {
|
|
29619
|
-
unlinkSync7(
|
|
31701
|
+
unlinkSync7(join23(reservedDir, file));
|
|
29620
31702
|
count++;
|
|
29621
31703
|
}
|
|
29622
31704
|
}
|
|
@@ -29639,7 +31721,7 @@ var init_liteBackend = __esm({
|
|
|
29639
31721
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
29640
31722
|
for (const file of files) {
|
|
29641
31723
|
try {
|
|
29642
|
-
const job = JSON.parse(readFileSync16(
|
|
31724
|
+
const job = JSON.parse(readFileSync16(join23(dir, file), "utf-8"));
|
|
29643
31725
|
const attempts = job.attempts || 0;
|
|
29644
31726
|
if (attempts > 0 && attempts < maxRetries) {
|
|
29645
31727
|
results.push(job);
|
|
@@ -29662,9 +31744,9 @@ var init_liteBackend = __esm({
|
|
|
29662
31744
|
try {
|
|
29663
31745
|
const queues = readdirSync11(this.basePath);
|
|
29664
31746
|
for (const q of queues) {
|
|
29665
|
-
const failedDir =
|
|
29666
|
-
const filePath =
|
|
29667
|
-
if (
|
|
31747
|
+
const failedDir = join23(this.basePath, q, "failed");
|
|
31748
|
+
const filePath = join23(failedDir, `${jobId}.queue-data`);
|
|
31749
|
+
if (existsSync18(filePath)) {
|
|
29668
31750
|
const job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
29669
31751
|
job.status = "pending";
|
|
29670
31752
|
job.attempts = (job.attempts || 0) + 1;
|
|
@@ -29672,8 +31754,8 @@ var init_liteBackend = __esm({
|
|
|
29672
31754
|
job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29673
31755
|
job.delayUntil = delaySeconds ? new Date(Date.now() + delaySeconds * 1e3).toISOString() : null;
|
|
29674
31756
|
const prefix = this.nextPrefix();
|
|
29675
|
-
const queueDir =
|
|
29676
|
-
|
|
31757
|
+
const queueDir = join23(this.basePath, q);
|
|
31758
|
+
writeFileSync11(join23(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
|
|
29677
31759
|
unlinkSync7(filePath);
|
|
29678
31760
|
return true;
|
|
29679
31761
|
}
|
|
@@ -29689,7 +31771,7 @@ var init_liteBackend = __esm({
|
|
|
29689
31771
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
29690
31772
|
for (const file of files) {
|
|
29691
31773
|
try {
|
|
29692
|
-
const job = JSON.parse(readFileSync16(
|
|
31774
|
+
const job = JSON.parse(readFileSync16(join23(failedDir, file), "utf-8"));
|
|
29693
31775
|
if ((job.attempts || 0) >= maxRetries) {
|
|
29694
31776
|
job.status = "dead";
|
|
29695
31777
|
results.push(job);
|
|
@@ -29710,7 +31792,7 @@ var init_liteBackend = __esm({
|
|
|
29710
31792
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
29711
31793
|
for (const file of files) {
|
|
29712
31794
|
try {
|
|
29713
|
-
unlinkSync7(
|
|
31795
|
+
unlinkSync7(join23(failedDir, file));
|
|
29714
31796
|
count++;
|
|
29715
31797
|
} catch {
|
|
29716
31798
|
}
|
|
@@ -29723,9 +31805,9 @@ var init_liteBackend = __esm({
|
|
|
29723
31805
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
|
|
29724
31806
|
for (const file of files) {
|
|
29725
31807
|
try {
|
|
29726
|
-
const job = JSON.parse(readFileSync16(
|
|
31808
|
+
const job = JSON.parse(readFileSync16(join23(dir, file), "utf-8"));
|
|
29727
31809
|
if (job.status === status2) {
|
|
29728
|
-
unlinkSync7(
|
|
31810
|
+
unlinkSync7(join23(dir, file));
|
|
29729
31811
|
count++;
|
|
29730
31812
|
}
|
|
29731
31813
|
} catch {
|
|
@@ -29749,7 +31831,7 @@ var init_liteBackend = __esm({
|
|
|
29749
31831
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
29750
31832
|
for (const file of files) {
|
|
29751
31833
|
try {
|
|
29752
|
-
const filePath =
|
|
31834
|
+
const filePath = join23(failedDir, file);
|
|
29753
31835
|
const job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
29754
31836
|
if ((job.attempts || 0) >= maxRetries) {
|
|
29755
31837
|
continue;
|
|
@@ -29759,7 +31841,7 @@ var init_liteBackend = __esm({
|
|
|
29759
31841
|
job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29760
31842
|
job.delayUntil = null;
|
|
29761
31843
|
const prefix = this.nextPrefix();
|
|
29762
|
-
|
|
31844
|
+
writeFileSync11(join23(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
|
|
29763
31845
|
unlinkSync7(filePath);
|
|
29764
31846
|
count++;
|
|
29765
31847
|
} catch {
|
|
@@ -29779,7 +31861,7 @@ var init_liteBackend = __esm({
|
|
|
29779
31861
|
}
|
|
29780
31862
|
for (const file of files) {
|
|
29781
31863
|
if (!file.includes(id)) continue;
|
|
29782
|
-
const filePath =
|
|
31864
|
+
const filePath = join23(dir, file);
|
|
29783
31865
|
let job;
|
|
29784
31866
|
try {
|
|
29785
31867
|
job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
@@ -29827,7 +31909,7 @@ var init_liteBackend = __esm({
|
|
|
29827
31909
|
error
|
|
29828
31910
|
};
|
|
29829
31911
|
const prefix = this.nextPrefix();
|
|
29830
|
-
|
|
31912
|
+
writeFileSync11(join23(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
|
|
29831
31913
|
}
|
|
29832
31914
|
/**
|
|
29833
31915
|
* Move the job to the dead-letter (failed/) directory. Terminal until a
|
|
@@ -29847,7 +31929,7 @@ var init_liteBackend = __esm({
|
|
|
29847
31929
|
error,
|
|
29848
31930
|
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
29849
31931
|
};
|
|
29850
|
-
|
|
31932
|
+
writeFileSync11(join23(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
|
|
29851
31933
|
}
|
|
29852
31934
|
/**
|
|
29853
31935
|
* Record a failed attempt.
|
|
@@ -29883,7 +31965,7 @@ var init_liteBackend = __esm({
|
|
|
29883
31965
|
retryJob(queue, job, delaySeconds) {
|
|
29884
31966
|
this.clearReservation(queue, job.id);
|
|
29885
31967
|
try {
|
|
29886
|
-
unlinkSync7(
|
|
31968
|
+
unlinkSync7(join23(this.ensureFailedDir(queue), `${job.id}.queue-data`));
|
|
29887
31969
|
} catch {
|
|
29888
31970
|
}
|
|
29889
31971
|
job.attempts = (job.attempts || 0) + 1;
|
|
@@ -30302,7 +32384,7 @@ var init_queue = __esm({
|
|
|
30302
32384
|
const jobs = this.popBatch(resolvedBatchSize);
|
|
30303
32385
|
if (jobs.length === 0) {
|
|
30304
32386
|
if (resolvedPollInterval <= 0) break;
|
|
30305
|
-
await new Promise((
|
|
32387
|
+
await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
|
|
30306
32388
|
continue;
|
|
30307
32389
|
}
|
|
30308
32390
|
yield jobs;
|
|
@@ -30312,7 +32394,7 @@ var init_queue = __esm({
|
|
|
30312
32394
|
const raw = this.pop();
|
|
30313
32395
|
if (raw === null) {
|
|
30314
32396
|
if (resolvedPollInterval <= 0) break;
|
|
30315
|
-
await new Promise((
|
|
32397
|
+
await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
|
|
30316
32398
|
continue;
|
|
30317
32399
|
}
|
|
30318
32400
|
yield createJob(raw, this);
|
|
@@ -32387,8 +34469,8 @@ ${end}
|
|
|
32387
34469
|
|
|
32388
34470
|
// src/devAdmin.ts
|
|
32389
34471
|
import { cpus as osCpus } from "node:os";
|
|
32390
|
-
import { readFileSync as readFileSync20, writeFileSync as
|
|
32391
|
-
import { join as
|
|
34472
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync15, existsSync as existsSync22, readdirSync as readdirSync15, mkdirSync as mkdirSync18, copyFileSync, statSync as statSync16 } from "node:fs";
|
|
34473
|
+
import { join as join27, dirname as dirname12, resolve as resolve16, relative as relative8 } from "node:path";
|
|
32392
34474
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
32393
34475
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
32394
34476
|
function escapeHtml(value) {
|
|
@@ -32502,12 +34584,12 @@ function mapQueueJob(job, topic, status2) {
|
|
|
32502
34584
|
};
|
|
32503
34585
|
}
|
|
32504
34586
|
function readQueueDir(dir, topic, status2) {
|
|
32505
|
-
if (!
|
|
34587
|
+
if (!existsSync22(dir)) return [];
|
|
32506
34588
|
const jobs = [];
|
|
32507
34589
|
for (const filename of readdirSync15(dir).sort()) {
|
|
32508
34590
|
if (!filename.endsWith(".queue-data")) continue;
|
|
32509
34591
|
try {
|
|
32510
|
-
jobs.push(mapQueueJob(JSON.parse(readFileSync20(
|
|
34592
|
+
jobs.push(mapQueueJob(JSON.parse(readFileSync20(join27(dir, filename), "utf-8")), topic, status2));
|
|
32511
34593
|
} catch {
|
|
32512
34594
|
}
|
|
32513
34595
|
}
|
|
@@ -32624,8 +34706,8 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
|
|
|
32624
34706
|
function resolveDevEnvVar(key) {
|
|
32625
34707
|
const live = process.env[key];
|
|
32626
34708
|
if (live !== void 0 && live !== "") return live;
|
|
32627
|
-
const envPath =
|
|
32628
|
-
if (!
|
|
34709
|
+
const envPath = join27(process.cwd(), ".env");
|
|
34710
|
+
if (!existsSync22(envPath)) return "";
|
|
32629
34711
|
for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
|
|
32630
34712
|
const t = line.trim();
|
|
32631
34713
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
@@ -32635,8 +34717,8 @@ function resolveDevEnvVar(key) {
|
|
|
32635
34717
|
return "";
|
|
32636
34718
|
}
|
|
32637
34719
|
function upsertDevEnvVar(key, value) {
|
|
32638
|
-
const envPath =
|
|
32639
|
-
const lines =
|
|
34720
|
+
const envPath = join27(process.cwd(), ".env");
|
|
34721
|
+
const lines = existsSync22(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
|
|
32640
34722
|
let found = false;
|
|
32641
34723
|
const out = [];
|
|
32642
34724
|
for (const line of lines) {
|
|
@@ -32651,7 +34733,7 @@ function upsertDevEnvVar(key, value) {
|
|
|
32651
34733
|
} else out.push(line);
|
|
32652
34734
|
}
|
|
32653
34735
|
if (!found) out.push(`${key}=${value}`);
|
|
32654
|
-
|
|
34736
|
+
writeFileSync15(envPath, out.join("\n").replace(/\n+$/, "") + "\n");
|
|
32655
34737
|
}
|
|
32656
34738
|
function formatUptime(seconds) {
|
|
32657
34739
|
const d = Math.floor(seconds / 86400);
|
|
@@ -32666,9 +34748,9 @@ function formatUptime(seconds) {
|
|
|
32666
34748
|
return parts.join(" ");
|
|
32667
34749
|
}
|
|
32668
34750
|
function parseEnvFile() {
|
|
32669
|
-
const envPath =
|
|
34751
|
+
const envPath = join27(process.cwd(), ".env");
|
|
32670
34752
|
const result = {};
|
|
32671
|
-
if (!
|
|
34753
|
+
if (!existsSync22(envPath)) return result;
|
|
32672
34754
|
const lines = readFileSync20(envPath, "utf-8").split("\n");
|
|
32673
34755
|
for (const line of lines) {
|
|
32674
34756
|
const trimmed = line.trim();
|
|
@@ -32680,9 +34762,9 @@ function parseEnvFile() {
|
|
|
32680
34762
|
}
|
|
32681
34763
|
function walkDirRecursive(dir) {
|
|
32682
34764
|
const results = [];
|
|
32683
|
-
if (!
|
|
34765
|
+
if (!existsSync22(dir)) return results;
|
|
32684
34766
|
for (const entry of readdirSync15(dir)) {
|
|
32685
|
-
const full =
|
|
34767
|
+
const full = join27(dir, entry);
|
|
32686
34768
|
if (statSync16(full).isDirectory()) {
|
|
32687
34769
|
results.push(...walkDirRecursive(full));
|
|
32688
34770
|
} else {
|
|
@@ -32699,25 +34781,25 @@ function handleGalleryDeploy(router) {
|
|
|
32699
34781
|
res.json({ error: "No gallery item specified" }, 400);
|
|
32700
34782
|
return;
|
|
32701
34783
|
}
|
|
32702
|
-
const galleryDir =
|
|
32703
|
-
const gallerySrc =
|
|
32704
|
-
if (!
|
|
34784
|
+
const galleryDir = resolve16(__devAdminDirname, "..", "gallery");
|
|
34785
|
+
const gallerySrc = join27(galleryDir, name, "src");
|
|
34786
|
+
if (!existsSync22(gallerySrc)) {
|
|
32705
34787
|
res.json({ error: `Gallery item '${name}' not found` }, 404);
|
|
32706
34788
|
return;
|
|
32707
34789
|
}
|
|
32708
|
-
const projectSrc =
|
|
34790
|
+
const projectSrc = resolve16(process.cwd(), "src");
|
|
32709
34791
|
const copied = [];
|
|
32710
34792
|
const allFiles = walkDirRecursive(gallerySrc);
|
|
32711
34793
|
for (const srcFile of allFiles) {
|
|
32712
|
-
const rel =
|
|
32713
|
-
const dest =
|
|
32714
|
-
|
|
34794
|
+
const rel = relative8(gallerySrc, srcFile);
|
|
34795
|
+
const dest = join27(projectSrc, rel);
|
|
34796
|
+
mkdirSync18(dirname12(dest), { recursive: true });
|
|
32715
34797
|
copyFileSync(srcFile, dest);
|
|
32716
34798
|
copied.push(rel);
|
|
32717
34799
|
}
|
|
32718
34800
|
try {
|
|
32719
|
-
const routesDir =
|
|
32720
|
-
if (
|
|
34801
|
+
const routesDir = resolve16(process.cwd(), "src", "routes");
|
|
34802
|
+
if (existsSync22(routesDir)) {
|
|
32721
34803
|
const { discoverRoutes: discoverRoutes2 } = await Promise.resolve().then(() => (init_routeDiscovery(), routeDiscovery_exports));
|
|
32722
34804
|
const routes = await discoverRoutes2(routesDir);
|
|
32723
34805
|
for (const route of routes) {
|
|
@@ -32733,7 +34815,7 @@ function handleGalleryDeploy(router) {
|
|
|
32733
34815
|
};
|
|
32734
34816
|
}
|
|
32735
34817
|
function safeJoin(projectRoot3, rel) {
|
|
32736
|
-
const resolved =
|
|
34818
|
+
const resolved = resolve16(projectRoot3, rel);
|
|
32737
34819
|
if (!resolved.startsWith(projectRoot3)) return null;
|
|
32738
34820
|
return resolved;
|
|
32739
34821
|
}
|
|
@@ -33696,16 +35778,16 @@ var init_devAdmin = __esm({
|
|
|
33696
35778
|
failed: queue.size("failed"),
|
|
33697
35779
|
reserved: queue.size("reserved")
|
|
33698
35780
|
};
|
|
33699
|
-
const topicDir =
|
|
35781
|
+
const topicDir = join27(queueBasePath2(), topic);
|
|
33700
35782
|
const jobs = [];
|
|
33701
35783
|
if (!statusFilter || statusFilter === "pending") {
|
|
33702
35784
|
jobs.push(...readQueueDir(topicDir, topic, "pending"));
|
|
33703
35785
|
}
|
|
33704
35786
|
if (!statusFilter || statusFilter === "reserved") {
|
|
33705
|
-
jobs.push(...readQueueDir(
|
|
35787
|
+
jobs.push(...readQueueDir(join27(topicDir, "reserved"), topic, "reserved"));
|
|
33706
35788
|
}
|
|
33707
35789
|
if (!statusFilter || statusFilter === "failed" || statusFilter === "dead") {
|
|
33708
|
-
jobs.push(...readQueueDir(
|
|
35790
|
+
jobs.push(...readQueueDir(join27(topicDir, "failed"), topic, "dead_letter"));
|
|
33709
35791
|
}
|
|
33710
35792
|
res.json({ stats, jobs });
|
|
33711
35793
|
} catch (e) {
|
|
@@ -33721,10 +35803,10 @@ var init_devAdmin = __esm({
|
|
|
33721
35803
|
const { queueBasePath: queueBasePath2 } = await Promise.resolve().then(() => (init_queue(), queue_exports));
|
|
33722
35804
|
const queueDir = queueBasePath2();
|
|
33723
35805
|
let topics = [];
|
|
33724
|
-
if (
|
|
35806
|
+
if (existsSync22(queueDir)) {
|
|
33725
35807
|
topics = readdirSync15(queueDir).filter((d) => {
|
|
33726
35808
|
try {
|
|
33727
|
-
return statSync16(
|
|
35809
|
+
return statSync16(join27(queueDir, d)).isDirectory();
|
|
33728
35810
|
} catch {
|
|
33729
35811
|
return false;
|
|
33730
35812
|
}
|
|
@@ -34039,7 +36121,7 @@ var init_devAdmin = __esm({
|
|
|
34039
36121
|
const count = parseInt(String(body.count ?? "10"), 10) || 10;
|
|
34040
36122
|
try {
|
|
34041
36123
|
const orm = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
34042
|
-
const dirs = ["src/orm", "src/models"].map((d) =>
|
|
36124
|
+
const dirs = ["src/orm", "src/models"].map((d) => resolve16(process.cwd(), d)).filter((d) => existsSync22(d));
|
|
34043
36125
|
const classes = [];
|
|
34044
36126
|
for (const dir of dirs) {
|
|
34045
36127
|
for (const m of await orm.discoverModels(dir)) classes.push(m.modelClass);
|
|
@@ -34066,7 +36148,7 @@ var init_devAdmin = __esm({
|
|
|
34066
36148
|
const run = promisify(execFile);
|
|
34067
36149
|
try {
|
|
34068
36150
|
const { stdout, stderr } = await run("npm", ["test"], {
|
|
34069
|
-
cwd:
|
|
36151
|
+
cwd: resolve16(process.cwd()),
|
|
34070
36152
|
timeout: 18e4,
|
|
34071
36153
|
encoding: "utf-8",
|
|
34072
36154
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -34180,8 +36262,8 @@ var init_devAdmin = __esm({
|
|
|
34180
36262
|
return;
|
|
34181
36263
|
}
|
|
34182
36264
|
try {
|
|
34183
|
-
const envPath =
|
|
34184
|
-
const lines =
|
|
36265
|
+
const envPath = join27(process.cwd(), ".env");
|
|
36266
|
+
const lines = existsSync22(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
|
|
34185
36267
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
34186
36268
|
const newLines = [];
|
|
34187
36269
|
for (const line of lines) {
|
|
@@ -34208,7 +36290,7 @@ var init_devAdmin = __esm({
|
|
|
34208
36290
|
for (const [key, found] of Object.entries(keysFound)) {
|
|
34209
36291
|
if (!found) newLines.push(`${key}=${values[key]}`);
|
|
34210
36292
|
}
|
|
34211
|
-
|
|
36293
|
+
writeFileSync15(envPath, newLines.join("\n") + "\n");
|
|
34212
36294
|
res.json({ success: true });
|
|
34213
36295
|
} catch (e) {
|
|
34214
36296
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -34218,26 +36300,26 @@ var init_devAdmin = __esm({
|
|
|
34218
36300
|
__devAdminFilename = fileURLToPath5(import.meta.url);
|
|
34219
36301
|
__devAdminDirname = dirname12(__devAdminFilename);
|
|
34220
36302
|
handleGalleryList = (_req, res) => {
|
|
34221
|
-
const galleryDir =
|
|
36303
|
+
const galleryDir = resolve16(__devAdminDirname, "..", "gallery");
|
|
34222
36304
|
const items = [];
|
|
34223
|
-
if (
|
|
36305
|
+
if (existsSync22(galleryDir)) {
|
|
34224
36306
|
const entries = readdirSync15(galleryDir).sort();
|
|
34225
36307
|
for (const entry of entries) {
|
|
34226
|
-
const entryPath =
|
|
34227
|
-
const metaFile =
|
|
34228
|
-
if (statSync16(entryPath).isDirectory() &&
|
|
36308
|
+
const entryPath = join27(galleryDir, entry);
|
|
36309
|
+
const metaFile = join27(entryPath, "meta.json");
|
|
36310
|
+
if (statSync16(entryPath).isDirectory() && existsSync22(metaFile)) {
|
|
34229
36311
|
try {
|
|
34230
36312
|
const meta = JSON.parse(readFileSync20(metaFile, "utf-8"));
|
|
34231
36313
|
meta.id = entry;
|
|
34232
|
-
const srcDir =
|
|
34233
|
-
if (
|
|
36314
|
+
const srcDir = join27(entryPath, "src");
|
|
36315
|
+
if (existsSync22(srcDir)) {
|
|
34234
36316
|
const allFiles = walkDirRecursive(srcDir);
|
|
34235
|
-
meta.files = allFiles.map((f) =>
|
|
36317
|
+
meta.files = allFiles.map((f) => relative8(srcDir, f));
|
|
34236
36318
|
}
|
|
34237
|
-
const projectSrc =
|
|
34238
|
-
if (
|
|
36319
|
+
const projectSrc = resolve16(process.cwd(), "src");
|
|
36320
|
+
if (existsSync22(srcDir) && meta.files) {
|
|
34239
36321
|
meta.deployed = meta.files.every(
|
|
34240
|
-
(f) =>
|
|
36322
|
+
(f) => existsSync22(join27(projectSrc, f))
|
|
34241
36323
|
);
|
|
34242
36324
|
} else {
|
|
34243
36325
|
meta.deployed = false;
|
|
@@ -34333,10 +36415,10 @@ var init_devAdmin = __esm({
|
|
|
34333
36415
|
handleFiles = async (req2, res) => {
|
|
34334
36416
|
const url = new URL(req2.url ?? "/", "http://localhost");
|
|
34335
36417
|
const rel = url.searchParams.get("path") ?? ".";
|
|
34336
|
-
const root =
|
|
36418
|
+
const root = resolve16(process.cwd());
|
|
34337
36419
|
const target = safeJoin(root, rel);
|
|
34338
36420
|
const { branch, gitRoot, status: gitStatus } = await devGitInfo(root);
|
|
34339
|
-
if (!target || !
|
|
36421
|
+
if (!target || !existsSync22(target) || !statSync16(target).isDirectory()) {
|
|
34340
36422
|
res.json({ path: rel, branch, entries: [], error: "not a directory" });
|
|
34341
36423
|
return;
|
|
34342
36424
|
}
|
|
@@ -34349,8 +36431,8 @@ var init_devAdmin = __esm({
|
|
|
34349
36431
|
const entries = [];
|
|
34350
36432
|
for (const name of readdirSync15(target).sort()) {
|
|
34351
36433
|
if (devFilesHidden(name)) continue;
|
|
34352
|
-
const full =
|
|
34353
|
-
const entryRel =
|
|
36434
|
+
const full = join27(target, name);
|
|
36435
|
+
const entryRel = relative8(root, full).replace(/\\/g, "/");
|
|
34354
36436
|
if (isSecretPath(entryRel)) continue;
|
|
34355
36437
|
let isDir = false;
|
|
34356
36438
|
let size = null;
|
|
@@ -34395,7 +36477,7 @@ var init_devAdmin = __esm({
|
|
|
34395
36477
|
size
|
|
34396
36478
|
});
|
|
34397
36479
|
}
|
|
34398
|
-
res.json({ path:
|
|
36480
|
+
res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
|
|
34399
36481
|
};
|
|
34400
36482
|
DEV_ADMIN_LANG_MAP = {
|
|
34401
36483
|
".py": "python",
|
|
@@ -34440,15 +36522,15 @@ var init_devAdmin = __esm({
|
|
|
34440
36522
|
res.json({ error: "Refused: secret file", path: rel, content: "", language: "text", bytes: 0 }, 403);
|
|
34441
36523
|
return;
|
|
34442
36524
|
}
|
|
34443
|
-
const root =
|
|
36525
|
+
const root = resolve16(process.cwd());
|
|
34444
36526
|
const target = safeJoin(root, rel);
|
|
34445
|
-
if (!target || !
|
|
36527
|
+
if (!target || !existsSync22(target) || !statSync16(target).isFile()) {
|
|
34446
36528
|
res.json({ error: `File not found: ${rel}` }, 404);
|
|
34447
36529
|
return;
|
|
34448
36530
|
}
|
|
34449
36531
|
try {
|
|
34450
36532
|
const content = readFileSync20(target, "utf-8");
|
|
34451
|
-
const path8 =
|
|
36533
|
+
const path8 = relative8(root, target);
|
|
34452
36534
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
34453
36535
|
} catch (e) {
|
|
34454
36536
|
res.json({ error: e.message }, 500);
|
|
@@ -34458,22 +36540,22 @@ var init_devAdmin = __esm({
|
|
|
34458
36540
|
const body = req2.body || {};
|
|
34459
36541
|
const rel = body.path || "";
|
|
34460
36542
|
const content = body.content ?? "";
|
|
34461
|
-
const root =
|
|
36543
|
+
const root = resolve16(process.cwd());
|
|
34462
36544
|
const target = safeJoin(root, rel);
|
|
34463
36545
|
if (!target) {
|
|
34464
36546
|
res.json({ error: `Path escapes project directory: ${rel}` }, 400);
|
|
34465
36547
|
return;
|
|
34466
36548
|
}
|
|
34467
36549
|
try {
|
|
34468
|
-
|
|
34469
|
-
const existed =
|
|
34470
|
-
|
|
36550
|
+
mkdirSync18(dirname12(target), { recursive: true });
|
|
36551
|
+
const existed = existsSync22(target);
|
|
36552
|
+
writeFileSync15(target, content, "utf-8");
|
|
34471
36553
|
try {
|
|
34472
36554
|
const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
34473
|
-
Plan2.recordAction(existed ? "patched" : "created",
|
|
36555
|
+
Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
|
|
34474
36556
|
} catch {
|
|
34475
36557
|
}
|
|
34476
|
-
res.json({ ok: true, path:
|
|
36558
|
+
res.json({ ok: true, path: relative8(root, target), bytes: Buffer.byteLength(content, "utf-8") });
|
|
34477
36559
|
} catch (e) {
|
|
34478
36560
|
res.json({ error: e.message }, 500);
|
|
34479
36561
|
}
|
|
@@ -34485,9 +36567,9 @@ var init_devAdmin = __esm({
|
|
|
34485
36567
|
res.json({ error: "Refused: secret file" }, 403);
|
|
34486
36568
|
return;
|
|
34487
36569
|
}
|
|
34488
|
-
const root =
|
|
36570
|
+
const root = resolve16(process.cwd());
|
|
34489
36571
|
const target = safeJoin(root, rel);
|
|
34490
|
-
if (!target || !
|
|
36572
|
+
if (!target || !existsSync22(target) || !statSync16(target).isFile()) {
|
|
34491
36573
|
res.raw.writeHead(404);
|
|
34492
36574
|
res.raw.end("Not found");
|
|
34493
36575
|
return;
|
|
@@ -34520,22 +36602,22 @@ var init_devAdmin = __esm({
|
|
|
34520
36602
|
const body = req2.body || {};
|
|
34521
36603
|
const from = body.from || "";
|
|
34522
36604
|
const to = body.to || "";
|
|
34523
|
-
const root =
|
|
36605
|
+
const root = resolve16(process.cwd());
|
|
34524
36606
|
const src = safeJoin(root, from);
|
|
34525
36607
|
const dst = safeJoin(root, to);
|
|
34526
36608
|
if (!src || !dst) {
|
|
34527
36609
|
res.json({ error: "Invalid path" }, 400);
|
|
34528
36610
|
return;
|
|
34529
36611
|
}
|
|
34530
|
-
if (!
|
|
36612
|
+
if (!existsSync22(src)) {
|
|
34531
36613
|
res.json({ error: `Source not found: ${from}` }, 404);
|
|
34532
36614
|
return;
|
|
34533
36615
|
}
|
|
34534
36616
|
try {
|
|
34535
36617
|
const { renameSync: renameSync3 } = await import("node:fs");
|
|
34536
|
-
|
|
36618
|
+
mkdirSync18(dirname12(dst), { recursive: true });
|
|
34537
36619
|
renameSync3(src, dst);
|
|
34538
|
-
res.json({ ok: true, from:
|
|
36620
|
+
res.json({ ok: true, from: relative8(root, src), to: relative8(root, dst) });
|
|
34539
36621
|
} catch (e) {
|
|
34540
36622
|
res.json({ error: e.message }, 500);
|
|
34541
36623
|
}
|
|
@@ -34543,20 +36625,20 @@ var init_devAdmin = __esm({
|
|
|
34543
36625
|
handleFileDelete = async (req2, res) => {
|
|
34544
36626
|
const body = req2.body || {};
|
|
34545
36627
|
const rel = body.path || "";
|
|
34546
|
-
const root =
|
|
36628
|
+
const root = resolve16(process.cwd());
|
|
34547
36629
|
const target = safeJoin(root, rel);
|
|
34548
36630
|
if (!target) {
|
|
34549
36631
|
res.json({ error: "Invalid path" }, 400);
|
|
34550
36632
|
return;
|
|
34551
36633
|
}
|
|
34552
|
-
if (!
|
|
36634
|
+
if (!existsSync22(target)) {
|
|
34553
36635
|
res.json({ error: `Not found: ${rel}` }, 404);
|
|
34554
36636
|
return;
|
|
34555
36637
|
}
|
|
34556
36638
|
try {
|
|
34557
36639
|
const { rmSync } = await import("node:fs");
|
|
34558
36640
|
rmSync(target, { recursive: true, force: true });
|
|
34559
|
-
res.json({ ok: true, deleted:
|
|
36641
|
+
res.json({ ok: true, deleted: relative8(root, target) });
|
|
34560
36642
|
} catch (e) {
|
|
34561
36643
|
res.json({ error: e.message }, 500);
|
|
34562
36644
|
}
|
|
@@ -34594,7 +36676,7 @@ var init_devAdmin = __esm({
|
|
|
34594
36676
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
34595
36677
|
const args = ["install", dev ? "--save-dev" : "--save", pkg];
|
|
34596
36678
|
const output = execFileSync7("npm", args, {
|
|
34597
|
-
cwd:
|
|
36679
|
+
cwd: resolve16(process.cwd()),
|
|
34598
36680
|
timeout: 12e4,
|
|
34599
36681
|
encoding: "utf-8"
|
|
34600
36682
|
}).toString();
|
|
@@ -34606,7 +36688,7 @@ var init_devAdmin = __esm({
|
|
|
34606
36688
|
handleGitStatus = async (_req, res) => {
|
|
34607
36689
|
try {
|
|
34608
36690
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
34609
|
-
const cwd =
|
|
36691
|
+
const cwd = resolve16(process.cwd());
|
|
34610
36692
|
try {
|
|
34611
36693
|
execFileSync7("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 3e3 });
|
|
34612
36694
|
} catch {
|
|
@@ -34745,7 +36827,7 @@ var init_devAdmin = __esm({
|
|
|
34745
36827
|
try {
|
|
34746
36828
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
34747
36829
|
const output = execFileSync7("npx", ["tina4nodejs", "generate", kind, name], {
|
|
34748
|
-
cwd:
|
|
36830
|
+
cwd: resolve16(process.cwd()),
|
|
34749
36831
|
timeout: 3e4,
|
|
34750
36832
|
encoding: "utf-8"
|
|
34751
36833
|
}).toString();
|
|
@@ -34890,21 +36972,21 @@ var init_devAdmin = __esm({
|
|
|
34890
36972
|
});
|
|
34891
36973
|
};
|
|
34892
36974
|
handleDevAdminJs = async (_req, res) => {
|
|
34893
|
-
const { readFileSync: readFileSync27, existsSync:
|
|
34894
|
-
const { dirname: dirname15, join:
|
|
36975
|
+
const { readFileSync: readFileSync27, existsSync: existsSync28 } = await import("node:fs");
|
|
36976
|
+
const { dirname: dirname15, join: join33, resolve: resolve21 } = await import("node:path");
|
|
34895
36977
|
const { fileURLToPath: fileURLToPath8 } = await import("node:url");
|
|
34896
36978
|
const dir = dirname15(fileURLToPath8(import.meta.url));
|
|
34897
36979
|
const candidates = [
|
|
34898
|
-
|
|
36980
|
+
join33(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
|
|
34899
36981
|
// src/../public/js/
|
|
34900
|
-
|
|
36982
|
+
join33(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
|
|
34901
36983
|
// deeper nesting
|
|
34902
|
-
|
|
34903
|
-
|
|
36984
|
+
resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
|
|
36985
|
+
resolve21(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
|
|
34904
36986
|
// project public/
|
|
34905
36987
|
];
|
|
34906
36988
|
for (const jsPath of candidates) {
|
|
34907
|
-
if (
|
|
36989
|
+
if (existsSync28(jsPath)) {
|
|
34908
36990
|
try {
|
|
34909
36991
|
const content = readFileSync27(jsPath, "utf-8");
|
|
34910
36992
|
res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
@@ -34929,8 +37011,8 @@ var init_devAdmin = __esm({
|
|
|
34929
37011
|
});
|
|
34930
37012
|
|
|
34931
37013
|
// src/i18n.ts
|
|
34932
|
-
import { readFileSync as readFileSync21, readdirSync as readdirSync16, existsSync as
|
|
34933
|
-
import { join as
|
|
37014
|
+
import { readFileSync as readFileSync21, readdirSync as readdirSync16, existsSync as existsSync23 } from "node:fs";
|
|
37015
|
+
import { join as join28, resolve as resolve17 } from "node:path";
|
|
34934
37016
|
var I18n;
|
|
34935
37017
|
var init_i18n = __esm({
|
|
34936
37018
|
"src/i18n.ts"() {
|
|
@@ -34950,7 +37032,7 @@ var init_i18n = __esm({
|
|
|
34950
37032
|
* (BUG-7, BREAKING in 3.13.x — was previously (localeDir, defaultLocale)).
|
|
34951
37033
|
*/
|
|
34952
37034
|
constructor(locale, path8) {
|
|
34953
|
-
this._localeDir =
|
|
37035
|
+
this._localeDir = resolve17(
|
|
34954
37036
|
path8 ?? process.env.TINA4_LOCALE_DIR ?? "src/locales"
|
|
34955
37037
|
);
|
|
34956
37038
|
this._defaultLocale = locale ?? process.env.TINA4_LOCALE ?? "en";
|
|
@@ -35007,7 +37089,7 @@ var init_i18n = __esm({
|
|
|
35007
37089
|
}
|
|
35008
37090
|
/** List available locale codes based on JSON files in the locale directory. */
|
|
35009
37091
|
availableLocales() {
|
|
35010
|
-
if (!
|
|
37092
|
+
if (!existsSync23(this._localeDir)) {
|
|
35011
37093
|
return [this._defaultLocale];
|
|
35012
37094
|
}
|
|
35013
37095
|
try {
|
|
@@ -35023,8 +37105,8 @@ var init_i18n = __esm({
|
|
|
35023
37105
|
if (this._translations.has(locale)) {
|
|
35024
37106
|
return;
|
|
35025
37107
|
}
|
|
35026
|
-
const filePath =
|
|
35027
|
-
if (
|
|
37108
|
+
const filePath = join28(this._localeDir, `${locale}.json`);
|
|
37109
|
+
if (existsSync23(filePath)) {
|
|
35028
37110
|
try {
|
|
35029
37111
|
const raw = readFileSync21(filePath, "utf-8");
|
|
35030
37112
|
const data = JSON.parse(raw);
|
|
@@ -35036,8 +37118,8 @@ var init_i18n = __esm({
|
|
|
35036
37118
|
}
|
|
35037
37119
|
}
|
|
35038
37120
|
for (const ext of [".yml", ".yaml"]) {
|
|
35039
|
-
const yamlPath =
|
|
35040
|
-
if (
|
|
37121
|
+
const yamlPath = join28(this._localeDir, `${locale}${ext}`);
|
|
37122
|
+
if (existsSync23(yamlPath)) {
|
|
35041
37123
|
try {
|
|
35042
37124
|
const raw = readFileSync21(yamlPath, "utf-8");
|
|
35043
37125
|
const data = _I18n._parseSimpleYaml(raw);
|
|
@@ -35382,7 +37464,7 @@ function sanitizeSecurity(reqs, schemes) {
|
|
|
35382
37464
|
return clean;
|
|
35383
37465
|
});
|
|
35384
37466
|
}
|
|
35385
|
-
function
|
|
37467
|
+
function generate2(routes, models = []) {
|
|
35386
37468
|
const info = {
|
|
35387
37469
|
title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
|
|
35388
37470
|
// The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
|
|
@@ -35830,7 +37912,7 @@ __export(src_exports2, {
|
|
|
35830
37912
|
addSchema: () => addSchema,
|
|
35831
37913
|
addSecurityScheme: () => addSecurityScheme,
|
|
35832
37914
|
createSwaggerRoutes: () => createSwaggerRoutes,
|
|
35833
|
-
generate: () =>
|
|
37915
|
+
generate: () => generate2,
|
|
35834
37916
|
resetRegistry: () => resetRegistry,
|
|
35835
37917
|
swaggerEnabled: () => swaggerEnabled
|
|
35836
37918
|
});
|
|
@@ -36184,8 +38266,8 @@ function writeMcpDiscovery(projectRoot3, port) {
|
|
|
36184
38266
|
const lines = contents.split(/\r?\n/);
|
|
36185
38267
|
const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
|
|
36186
38268
|
if (!already) {
|
|
36187
|
-
const
|
|
36188
|
-
fs8.writeFileSync(gitignorePath, `${contents}${
|
|
38269
|
+
const sep7 = contents.endsWith("\n") || contents === "" ? "" : "\n";
|
|
38270
|
+
fs8.writeFileSync(gitignorePath, `${contents}${sep7}${GITIGNORE_LINE}
|
|
36189
38271
|
`, "utf-8");
|
|
36190
38272
|
}
|
|
36191
38273
|
}
|
|
@@ -36204,8 +38286,8 @@ var init_docsAutoDiscovery = __esm({
|
|
|
36204
38286
|
// src/server.ts
|
|
36205
38287
|
import { createServer as createServer2 } from "node:http";
|
|
36206
38288
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
36207
|
-
import { resolve as
|
|
36208
|
-
import { existsSync as
|
|
38289
|
+
import { resolve as resolve19, dirname as dirname13, join as join30, relative as relative9 } from "node:path";
|
|
38290
|
+
import { existsSync as existsSync25, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
|
|
36209
38291
|
import { isatty } from "node:tty";
|
|
36210
38292
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
36211
38293
|
import { execFileSync as execFileSync3, exec } from "node:child_process";
|
|
@@ -36243,8 +38325,8 @@ function swaggerAdvertised() {
|
|
|
36243
38325
|
return TRUTHY2.includes(raw);
|
|
36244
38326
|
}
|
|
36245
38327
|
async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
|
|
36246
|
-
const dir =
|
|
36247
|
-
if (!
|
|
38328
|
+
const dir = resolve19(base, migrationDir);
|
|
38329
|
+
if (!existsSync25(dir)) return;
|
|
36248
38330
|
let hasSql = false;
|
|
36249
38331
|
try {
|
|
36250
38332
|
hasSql = readdirSync17(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
|
|
@@ -36385,12 +38467,12 @@ async function renderErrorPage(code, data, templatesDir) {
|
|
|
36385
38467
|
}
|
|
36386
38468
|
return instance;
|
|
36387
38469
|
};
|
|
36388
|
-
const userTemplatePath =
|
|
36389
|
-
if (
|
|
38470
|
+
const userTemplatePath = join30(templatesDir, templateFile);
|
|
38471
|
+
if (existsSync25(userTemplatePath)) {
|
|
36390
38472
|
return getCachedFrond(templatesDir).render(templateFile, data);
|
|
36391
38473
|
}
|
|
36392
|
-
const builtinTemplatePath =
|
|
36393
|
-
if (
|
|
38474
|
+
const builtinTemplatePath = join30(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
|
|
38475
|
+
if (existsSync25(builtinTemplatePath)) {
|
|
36394
38476
|
return getCachedFrond(BUILTIN_ERROR_TEMPLATES_DIR).render(templateFile, data);
|
|
36395
38477
|
}
|
|
36396
38478
|
return null;
|
|
@@ -36407,29 +38489,29 @@ function injectDevToolbar(html, ctx) {
|
|
|
36407
38489
|
}
|
|
36408
38490
|
function walkGalleryFiles(dir) {
|
|
36409
38491
|
const results = [];
|
|
36410
|
-
if (!
|
|
38492
|
+
if (!existsSync25(dir)) return results;
|
|
36411
38493
|
for (const f of readdirSync17(dir)) {
|
|
36412
|
-
const full =
|
|
38494
|
+
const full = join30(dir, f);
|
|
36413
38495
|
if (statSync17(full).isDirectory()) results.push(...walkGalleryFiles(full));
|
|
36414
38496
|
else results.push(full);
|
|
36415
38497
|
}
|
|
36416
38498
|
return results;
|
|
36417
38499
|
}
|
|
36418
38500
|
function getGalleryDeployedState() {
|
|
36419
|
-
const galleryDir =
|
|
38501
|
+
const galleryDir = resolve19(__dirname, "..", "gallery");
|
|
36420
38502
|
const state = {};
|
|
36421
|
-
if (!
|
|
38503
|
+
if (!existsSync25(galleryDir)) return state;
|
|
36422
38504
|
try {
|
|
36423
38505
|
const entries = readdirSync17(galleryDir).sort();
|
|
36424
38506
|
for (const entry of entries) {
|
|
36425
|
-
const entryPath =
|
|
36426
|
-
const metaFile =
|
|
36427
|
-
if (statSync17(entryPath).isDirectory() &&
|
|
36428
|
-
const srcDir =
|
|
36429
|
-
if (
|
|
38507
|
+
const entryPath = join30(galleryDir, entry);
|
|
38508
|
+
const metaFile = join30(entryPath, "meta.json");
|
|
38509
|
+
if (statSync17(entryPath).isDirectory() && existsSync25(metaFile)) {
|
|
38510
|
+
const srcDir = join30(entryPath, "src");
|
|
38511
|
+
if (existsSync25(srcDir)) {
|
|
36430
38512
|
const files = walkGalleryFiles(srcDir);
|
|
36431
|
-
const projectSrc =
|
|
36432
|
-
state[entry] = files.every((f) =>
|
|
38513
|
+
const projectSrc = resolve19(process.cwd(), "src");
|
|
38514
|
+
state[entry] = files.every((f) => existsSync25(join30(projectSrc, relative9(srcDir, f))));
|
|
36433
38515
|
} else {
|
|
36434
38516
|
state[entry] = false;
|
|
36435
38517
|
}
|
|
@@ -36457,9 +38539,9 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
36457
38539
|
const isDev2 = (process.env.TINA4_DEBUG ?? "false").toLowerCase() === "true";
|
|
36458
38540
|
if (isDev2) {
|
|
36459
38541
|
if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
|
|
36460
|
-
const pagesDir =
|
|
38542
|
+
const pagesDir = resolve19(templatesDir, TEMPLATE_PAGES_DIR);
|
|
36461
38543
|
for (const ext of [".twig", ".html"]) {
|
|
36462
|
-
if (
|
|
38544
|
+
if (existsSync25(resolve19(pagesDir, cleanPath + ext))) {
|
|
36463
38545
|
return `${TEMPLATE_PAGES_DIR}/${cleanPath}${ext}`;
|
|
36464
38546
|
}
|
|
36465
38547
|
}
|
|
@@ -36467,14 +38549,14 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
36467
38549
|
}
|
|
36468
38550
|
if (!templateCache) {
|
|
36469
38551
|
templateCache = /* @__PURE__ */ new Map();
|
|
36470
|
-
const pagesDir =
|
|
36471
|
-
if (
|
|
38552
|
+
const pagesDir = resolve19(templatesDir, TEMPLATE_PAGES_DIR);
|
|
38553
|
+
if (existsSync25(pagesDir)) {
|
|
36472
38554
|
const scan = (dir, prefix) => {
|
|
36473
38555
|
for (const entry of readdirSync17(dir, { withFileTypes: true })) {
|
|
36474
38556
|
if (entry.name.startsWith("_")) continue;
|
|
36475
38557
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
36476
38558
|
if (entry.isDirectory()) {
|
|
36477
|
-
scan(
|
|
38559
|
+
scan(resolve19(dir, entry.name), rel);
|
|
36478
38560
|
} else if (entry.name.endsWith(".twig") || entry.name.endsWith(".html")) {
|
|
36479
38561
|
const urlPath = rel.replace(/\.(twig|html)$/, "");
|
|
36480
38562
|
if (!templateCache.has(urlPath)) {
|
|
@@ -36879,7 +38961,7 @@ function serveTemplateFallback(ctx) {
|
|
|
36879
38961
|
if ((ctx.req.method ?? "GET") !== "GET") return false;
|
|
36880
38962
|
const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
|
|
36881
38963
|
if (!tplFile) return false;
|
|
36882
|
-
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync23(
|
|
38964
|
+
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync23(resolve19(ctx.templatesDir, tplFile), "utf-8");
|
|
36883
38965
|
ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
36884
38966
|
ctx.res.raw.end(html);
|
|
36885
38967
|
return true;
|
|
@@ -36918,9 +39000,9 @@ function serveMethodNotAllowed(ctx) {
|
|
|
36918
39000
|
}
|
|
36919
39001
|
function serveStaticAsset(ctx) {
|
|
36920
39002
|
const custom = process.env.TINA4_PUBLIC_DIR;
|
|
36921
|
-
if (custom &&
|
|
36922
|
-
if (
|
|
36923
|
-
if (
|
|
39003
|
+
if (custom && existsSync25(custom) && tryServeStatic(custom, ctx.req, ctx.res)) return true;
|
|
39004
|
+
if (existsSync25(ctx.staticDir) && tryServeStatic(ctx.staticDir, ctx.req, ctx.res)) return true;
|
|
39005
|
+
if (existsSync25(ctx.srcPublicDir) && tryServeStatic(ctx.srcPublicDir, ctx.req, ctx.res)) return true;
|
|
36924
39006
|
if (ctx.swaggerAssetsEnabled || !isSwaggerAssetPath(ctx.pathname)) {
|
|
36925
39007
|
if (tryServeStatic(BUILTIN_PUBLIC_DIR, ctx.req, ctx.res)) return true;
|
|
36926
39008
|
}
|
|
@@ -36944,10 +39026,10 @@ async function serveNotFound(ctx) {
|
|
|
36944
39026
|
return true;
|
|
36945
39027
|
}
|
|
36946
39028
|
async function buildDispatchContext(router, base) {
|
|
36947
|
-
const root = base ?
|
|
36948
|
-
const staticDir =
|
|
36949
|
-
const srcPublicDir =
|
|
36950
|
-
const templatesDir =
|
|
39029
|
+
const root = base ? resolve19(base) : process.cwd();
|
|
39030
|
+
const staticDir = resolve19(root, "public");
|
|
39031
|
+
const srcPublicDir = resolve19(root, "src/public");
|
|
39032
|
+
const templatesDir = resolve19(root, "src/templates");
|
|
36951
39033
|
let frondEngine = null;
|
|
36952
39034
|
try {
|
|
36953
39035
|
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
@@ -37111,13 +39193,13 @@ ${reset2}
|
|
|
37111
39193
|
};
|
|
37112
39194
|
}
|
|
37113
39195
|
}
|
|
37114
|
-
const base = config?.basePath ?
|
|
37115
|
-
const routesDir =
|
|
37116
|
-
const modelsDir =
|
|
37117
|
-
const ormDir =
|
|
37118
|
-
const staticDir =
|
|
37119
|
-
const srcPublicDir =
|
|
37120
|
-
const templatesDir =
|
|
39196
|
+
const base = config?.basePath ? resolve19(config.basePath) : process.cwd();
|
|
39197
|
+
const routesDir = resolve19(base, config?.routesDir ?? "src/routes");
|
|
39198
|
+
const modelsDir = resolve19(base, config?.modelsDir ?? "src/models");
|
|
39199
|
+
const ormDir = resolve19(base, "src/orm");
|
|
39200
|
+
const staticDir = resolve19(base, config?.staticDir ?? "public");
|
|
39201
|
+
const srcPublicDir = resolve19(base, "src/public");
|
|
39202
|
+
const templatesDir = resolve19(base, config?.templatesDir ?? "src/templates");
|
|
37121
39203
|
const router = new Router();
|
|
37122
39204
|
const middleware = new MiddlewareChain();
|
|
37123
39205
|
globalThis.__tina4_router = router;
|
|
@@ -37151,8 +39233,8 @@ ${reset2}
|
|
|
37151
39233
|
} catch {
|
|
37152
39234
|
}
|
|
37153
39235
|
if (frondEngine) {
|
|
37154
|
-
const localeDir =
|
|
37155
|
-
if (
|
|
39236
|
+
const localeDir = resolve19(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
|
|
39237
|
+
if (existsSync25(localeDir)) {
|
|
37156
39238
|
try {
|
|
37157
39239
|
const localeFiles = readdirSync17(localeDir).filter((f) => f.endsWith(".json"));
|
|
37158
39240
|
if (localeFiles.length > 0 && !frondEngine.globals?.t) {
|
|
@@ -37167,7 +39249,7 @@ ${reset2}
|
|
|
37167
39249
|
middleware.use(requestLogger());
|
|
37168
39250
|
middleware.use(rateLimiter());
|
|
37169
39251
|
MiddlewareRunner.use(SecurityHeadersMiddleware);
|
|
37170
|
-
if (
|
|
39252
|
+
if (existsSync25(routesDir)) {
|
|
37171
39253
|
const routes = await discoverRoutes(routesDir);
|
|
37172
39254
|
for (const route of routes) {
|
|
37173
39255
|
router.addRoute(route);
|
|
@@ -37187,8 +39269,8 @@ ${reset2}
|
|
|
37187
39269
|
console.log(`
|
|
37188
39270
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
37189
39271
|
}
|
|
37190
|
-
const hasOrmDir =
|
|
37191
|
-
const hasModelsDir =
|
|
39272
|
+
const hasOrmDir = existsSync25(ormDir);
|
|
39273
|
+
const hasModelsDir = existsSync25(modelsDir);
|
|
37192
39274
|
if (hasOrmDir || hasModelsDir) {
|
|
37193
39275
|
try {
|
|
37194
39276
|
const orm = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
@@ -37245,7 +39327,7 @@ ${reset2}
|
|
|
37245
39327
|
let modelDefs = [];
|
|
37246
39328
|
try {
|
|
37247
39329
|
const orm = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
37248
|
-
const allModelDirs = [ormDir, modelsDir].filter((d) =>
|
|
39330
|
+
const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync25(d));
|
|
37249
39331
|
const seenTables = /* @__PURE__ */ new Set();
|
|
37250
39332
|
for (const dir of allModelDirs) {
|
|
37251
39333
|
const discovered = await orm.discoverModels(dir);
|
|
@@ -37480,8 +39562,8 @@ var init_server = __esm({
|
|
|
37480
39562
|
init_version();
|
|
37481
39563
|
__filename = fileURLToPath6(import.meta.url);
|
|
37482
39564
|
__dirname = dirname13(__filename);
|
|
37483
|
-
BUILTIN_ERROR_TEMPLATES_DIR =
|
|
37484
|
-
BUILTIN_PUBLIC_DIR =
|
|
39565
|
+
BUILTIN_ERROR_TEMPLATES_DIR = resolve19(__dirname, "..", "templates");
|
|
39566
|
+
BUILTIN_PUBLIC_DIR = resolve19(__dirname, "..", "public");
|
|
37485
39567
|
swaggerAssetsEnabled = false;
|
|
37486
39568
|
DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 30;
|
|
37487
39569
|
frondCache = /* @__PURE__ */ new Map();
|
|
@@ -37708,7 +39790,7 @@ var init_mqttMessage = __esm({
|
|
|
37708
39790
|
import net2 from "node:net";
|
|
37709
39791
|
import tls from "node:tls";
|
|
37710
39792
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
37711
|
-
import { existsSync as
|
|
39793
|
+
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
|
|
37712
39794
|
var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
|
|
37713
39795
|
var init_mqtt = __esm({
|
|
37714
39796
|
"src/mqtt.ts"() {
|
|
@@ -37911,7 +39993,7 @@ var init_mqtt = __esm({
|
|
|
37911
39993
|
*/
|
|
37912
39994
|
async connect() {
|
|
37913
39995
|
this.closeSocket();
|
|
37914
|
-
if (this.secure && this.tlsVerify && this.caFile && !
|
|
39996
|
+
if (this.secure && this.tlsVerify && this.caFile && !existsSync26(this.caFile)) {
|
|
37915
39997
|
throw new MqttError(
|
|
37916
39998
|
`MQTT CA file not found: ${this.caFile} -- TINA4_MQTT_CA_FILE (or caFile) must point at the broker's CA certificate in PEM form`
|
|
37917
39999
|
);
|
|
@@ -38148,7 +40230,7 @@ var init_mqtt = __esm({
|
|
|
38148
40230
|
* a later client.
|
|
38149
40231
|
*/
|
|
38150
40232
|
openSocket() {
|
|
38151
|
-
return new Promise((
|
|
40233
|
+
return new Promise((resolve21, reject) => {
|
|
38152
40234
|
let settled = false;
|
|
38153
40235
|
const settle = (fn) => {
|
|
38154
40236
|
if (settled) return;
|
|
@@ -38174,9 +40256,9 @@ var init_mqtt = __esm({
|
|
|
38174
40256
|
rejectUnauthorized: this.tlsVerify
|
|
38175
40257
|
};
|
|
38176
40258
|
if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
|
|
38177
|
-
sock = tls.connect(opts, () => settle(() =>
|
|
40259
|
+
sock = tls.connect(opts, () => settle(() => resolve21(sock)));
|
|
38178
40260
|
} else {
|
|
38179
|
-
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() =>
|
|
40261
|
+
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
|
|
38180
40262
|
}
|
|
38181
40263
|
sock.once("error", (err) => {
|
|
38182
40264
|
settle(() => {
|
|
@@ -38215,13 +40297,13 @@ var init_mqtt = __esm({
|
|
|
38215
40297
|
writePacket(header, body) {
|
|
38216
40298
|
if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
|
|
38217
40299
|
const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
|
|
38218
|
-
return new Promise((
|
|
40300
|
+
return new Promise((resolve21, reject) => {
|
|
38219
40301
|
this.socket.write(packet, (err) => {
|
|
38220
40302
|
if (err) {
|
|
38221
40303
|
reject(new MqttError(`MQTT write failed: ${err.message}`));
|
|
38222
40304
|
} else {
|
|
38223
40305
|
this.lastWriteAt = Date.now();
|
|
38224
|
-
|
|
40306
|
+
resolve21();
|
|
38225
40307
|
}
|
|
38226
40308
|
});
|
|
38227
40309
|
});
|
|
@@ -38254,7 +40336,7 @@ var init_mqtt = __esm({
|
|
|
38254
40336
|
if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
|
|
38255
40337
|
if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
|
|
38256
40338
|
if (this.socketError !== null) return Promise.reject(this.socketError);
|
|
38257
|
-
return new Promise((
|
|
40339
|
+
return new Promise((resolve21, reject) => {
|
|
38258
40340
|
let timer = null;
|
|
38259
40341
|
if (deadline !== null) {
|
|
38260
40342
|
const remaining = deadline - Date.now();
|
|
@@ -38269,7 +40351,7 @@ var init_mqtt = __esm({
|
|
|
38269
40351
|
}
|
|
38270
40352
|
}, remaining);
|
|
38271
40353
|
}
|
|
38272
|
-
this.waiter = { need, resolve:
|
|
40354
|
+
this.waiter = { need, resolve: resolve21, reject, timer };
|
|
38273
40355
|
this.serviceWaiter();
|
|
38274
40356
|
});
|
|
38275
40357
|
}
|
|
@@ -38394,7 +40476,7 @@ var init_mqtt = __esm({
|
|
|
38394
40476
|
|
|
38395
40477
|
// src/service.ts
|
|
38396
40478
|
import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
|
|
38397
|
-
import { join as
|
|
40479
|
+
import { join as join31, extname as extname7 } from "node:path";
|
|
38398
40480
|
import { pathToFileURL } from "node:url";
|
|
38399
40481
|
function matchCronField(field, value) {
|
|
38400
40482
|
if (field === "*") return true;
|
|
@@ -38568,7 +40650,7 @@ var init_service = __esm({
|
|
|
38568
40650
|
for (const entry of entries) {
|
|
38569
40651
|
const ext = extname7(entry);
|
|
38570
40652
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
38571
|
-
const fullPath =
|
|
40653
|
+
const fullPath = join31(dir, entry);
|
|
38572
40654
|
const stat = statSync18(fullPath);
|
|
38573
40655
|
if (!stat.isFile()) continue;
|
|
38574
40656
|
try {
|
|
@@ -38695,7 +40777,7 @@ var init_service = __esm({
|
|
|
38695
40777
|
for (const entry of entries) {
|
|
38696
40778
|
const ext = extname7(entry);
|
|
38697
40779
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
38698
|
-
const fullPath =
|
|
40780
|
+
const fullPath = join31(dir, entry);
|
|
38699
40781
|
if (watchedFiles.has(fullPath)) continue;
|
|
38700
40782
|
watchedFiles.add(fullPath);
|
|
38701
40783
|
watchFile(fullPath, { interval: 1e3 }, async () => {
|
|
@@ -39304,7 +41386,7 @@ var init_api = __esm({
|
|
|
39304
41386
|
* `res.destroy()`.
|
|
39305
41387
|
*/
|
|
39306
41388
|
openStreamRequest(method, url, headers, data, connectSec) {
|
|
39307
|
-
return new Promise((
|
|
41389
|
+
return new Promise((resolve21, reject) => {
|
|
39308
41390
|
let parsed;
|
|
39309
41391
|
try {
|
|
39310
41392
|
parsed = new URL2(url);
|
|
@@ -39326,7 +41408,7 @@ var init_api = __esm({
|
|
|
39326
41408
|
options.rejectUnauthorized = false;
|
|
39327
41409
|
}
|
|
39328
41410
|
const req2 = protocolModule.request(options, (res) => {
|
|
39329
|
-
|
|
41411
|
+
resolve21({ res });
|
|
39330
41412
|
});
|
|
39331
41413
|
req2.on("timeout", () => {
|
|
39332
41414
|
req2.destroy(new ApiStreamError(`stream connect timeout after ${connectSec}s`, null));
|
|
@@ -39464,12 +41546,12 @@ var init_api = __esm({
|
|
|
39464
41546
|
* authenticate to.
|
|
39465
41547
|
*/
|
|
39466
41548
|
performRequest(method, url, headers, data, redirectsLeft) {
|
|
39467
|
-
return new Promise((
|
|
41549
|
+
return new Promise((resolve21) => {
|
|
39468
41550
|
let parsed;
|
|
39469
41551
|
try {
|
|
39470
41552
|
parsed = new URL2(url);
|
|
39471
41553
|
} catch (err) {
|
|
39472
|
-
|
|
41554
|
+
resolve21({ kind: "error", error: err instanceof Error ? err.message : String(err) });
|
|
39473
41555
|
return;
|
|
39474
41556
|
}
|
|
39475
41557
|
const isHttps = parsed.protocol === "https:";
|
|
@@ -39494,7 +41576,7 @@ var init_api = __esm({
|
|
|
39494
41576
|
try {
|
|
39495
41577
|
nextUrl = new URL2(location, url).toString();
|
|
39496
41578
|
} catch {
|
|
39497
|
-
|
|
41579
|
+
resolve21({ kind: "response", res });
|
|
39498
41580
|
return;
|
|
39499
41581
|
}
|
|
39500
41582
|
const crossOrigin = !sameOrigin(url, nextUrl);
|
|
@@ -39512,17 +41594,17 @@ var init_api = __esm({
|
|
|
39512
41594
|
deleteHeaderCaseInsensitive(nextHeaders, name);
|
|
39513
41595
|
}
|
|
39514
41596
|
}
|
|
39515
|
-
this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(
|
|
41597
|
+
this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve21);
|
|
39516
41598
|
return;
|
|
39517
41599
|
}
|
|
39518
|
-
|
|
41600
|
+
resolve21({ kind: "response", res });
|
|
39519
41601
|
});
|
|
39520
41602
|
req2.on("timeout", () => {
|
|
39521
41603
|
req2.destroy();
|
|
39522
|
-
|
|
41604
|
+
resolve21({ kind: "error", error: `Request timed out after ${this.timeout}s` });
|
|
39523
41605
|
});
|
|
39524
41606
|
req2.on("error", (err) => {
|
|
39525
|
-
|
|
41607
|
+
resolve21({ kind: "error", error: err.message });
|
|
39526
41608
|
});
|
|
39527
41609
|
if (data) {
|
|
39528
41610
|
req2.write(data);
|
|
@@ -39532,7 +41614,7 @@ var init_api = __esm({
|
|
|
39532
41614
|
}
|
|
39533
41615
|
/** Buffer a response body, parse JSON if possible, and store cookies. */
|
|
39534
41616
|
readResponse(res) {
|
|
39535
|
-
return new Promise((
|
|
41617
|
+
return new Promise((resolve21) => {
|
|
39536
41618
|
const chunks = [];
|
|
39537
41619
|
res.on("data", (chunk) => {
|
|
39538
41620
|
chunks.push(chunk);
|
|
@@ -39547,7 +41629,7 @@ var init_api = __esm({
|
|
|
39547
41629
|
} catch {
|
|
39548
41630
|
parsed = raw;
|
|
39549
41631
|
}
|
|
39550
|
-
|
|
41632
|
+
resolve21({
|
|
39551
41633
|
http_code: res.statusCode ?? null,
|
|
39552
41634
|
body: parsed,
|
|
39553
41635
|
headers: respHeaders,
|
|
@@ -39555,7 +41637,7 @@ var init_api = __esm({
|
|
|
39555
41637
|
});
|
|
39556
41638
|
});
|
|
39557
41639
|
res.on("error", (err) => {
|
|
39558
|
-
|
|
41640
|
+
resolve21({ http_code: null, body: null, headers: {}, error: err.message });
|
|
39559
41641
|
});
|
|
39560
41642
|
});
|
|
39561
41643
|
}
|
|
@@ -39619,7 +41701,7 @@ function parseMailRedirectList(raw) {
|
|
|
39619
41701
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
39620
41702
|
}
|
|
39621
41703
|
function readResponse(socket) {
|
|
39622
|
-
return new Promise((
|
|
41704
|
+
return new Promise((resolve21, reject) => {
|
|
39623
41705
|
let buffer = "";
|
|
39624
41706
|
const onData = (chunk) => {
|
|
39625
41707
|
buffer += chunk.toString("utf-8");
|
|
@@ -39631,7 +41713,7 @@ function readResponse(socket) {
|
|
|
39631
41713
|
if (line.length >= 4 && line[3] === " ") {
|
|
39632
41714
|
socket.removeListener("data", onData);
|
|
39633
41715
|
socket.removeListener("error", onError);
|
|
39634
|
-
|
|
41716
|
+
resolve21({ code, text: buffer.trim() });
|
|
39635
41717
|
return;
|
|
39636
41718
|
}
|
|
39637
41719
|
}
|
|
@@ -39645,10 +41727,10 @@ function readResponse(socket) {
|
|
|
39645
41727
|
});
|
|
39646
41728
|
}
|
|
39647
41729
|
function sendCommand(socket, command) {
|
|
39648
|
-
return new Promise((
|
|
41730
|
+
return new Promise((resolve21, reject) => {
|
|
39649
41731
|
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
39650
41732
|
if (err) return reject(err);
|
|
39651
|
-
readResponse(socket).then(
|
|
41733
|
+
readResponse(socket).then(resolve21, reject);
|
|
39652
41734
|
});
|
|
39653
41735
|
});
|
|
39654
41736
|
}
|
|
@@ -39748,7 +41830,7 @@ function imapQuote(s) {
|
|
|
39748
41830
|
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
39749
41831
|
}
|
|
39750
41832
|
function imapReadLine(socket) {
|
|
39751
|
-
return new Promise((
|
|
41833
|
+
return new Promise((resolve21, reject) => {
|
|
39752
41834
|
let buffer = "";
|
|
39753
41835
|
const onData = (chunk) => {
|
|
39754
41836
|
buffer += chunk.toString("utf-8");
|
|
@@ -39756,7 +41838,7 @@ function imapReadLine(socket) {
|
|
|
39756
41838
|
if (nlIndex !== -1) {
|
|
39757
41839
|
socket.removeListener("data", onData);
|
|
39758
41840
|
socket.removeListener("error", onError);
|
|
39759
|
-
|
|
41841
|
+
resolve21(buffer);
|
|
39760
41842
|
}
|
|
39761
41843
|
};
|
|
39762
41844
|
const onError = (err) => {
|
|
@@ -39768,7 +41850,7 @@ function imapReadLine(socket) {
|
|
|
39768
41850
|
});
|
|
39769
41851
|
}
|
|
39770
41852
|
function imapCommand(socket, command) {
|
|
39771
|
-
return new Promise((
|
|
41853
|
+
return new Promise((resolve21, reject) => {
|
|
39772
41854
|
imapTagCounter++;
|
|
39773
41855
|
const tag = `T${imapTagCounter}`;
|
|
39774
41856
|
const fullCommand = `${tag} ${command}\r
|
|
@@ -39779,7 +41861,7 @@ function imapCommand(socket, command) {
|
|
|
39779
41861
|
if (buffer.includes(`${tag} OK`)) {
|
|
39780
41862
|
socket.removeListener("data", onData);
|
|
39781
41863
|
socket.removeListener("error", onError);
|
|
39782
|
-
|
|
41864
|
+
resolve21(buffer);
|
|
39783
41865
|
return;
|
|
39784
41866
|
}
|
|
39785
41867
|
if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
|
|
@@ -40084,14 +42166,14 @@ var init_messenger = __esm({
|
|
|
40084
42166
|
let socket;
|
|
40085
42167
|
if (this.port === 465) {
|
|
40086
42168
|
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
40087
|
-
await new Promise((
|
|
40088
|
-
socket.once("secureConnect",
|
|
42169
|
+
await new Promise((resolve21, reject) => {
|
|
42170
|
+
socket.once("secureConnect", resolve21);
|
|
40089
42171
|
socket.once("error", reject);
|
|
40090
42172
|
});
|
|
40091
42173
|
} else {
|
|
40092
42174
|
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
40093
|
-
await new Promise((
|
|
40094
|
-
socket.once("connect",
|
|
42175
|
+
await new Promise((resolve21, reject) => {
|
|
42176
|
+
socket.once("connect", resolve21);
|
|
40095
42177
|
socket.once("error", reject);
|
|
40096
42178
|
});
|
|
40097
42179
|
}
|
|
@@ -40115,8 +42197,8 @@ var init_messenger = __esm({
|
|
|
40115
42197
|
socket = tls2.connect(
|
|
40116
42198
|
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
40117
42199
|
);
|
|
40118
|
-
await new Promise((
|
|
40119
|
-
socket.once("secureConnect",
|
|
42200
|
+
await new Promise((resolve21, reject) => {
|
|
42201
|
+
socket.once("secureConnect", resolve21);
|
|
40120
42202
|
socket.once("error", reject);
|
|
40121
42203
|
});
|
|
40122
42204
|
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
@@ -40209,14 +42291,14 @@ var init_messenger = __esm({
|
|
|
40209
42291
|
let socket;
|
|
40210
42292
|
if (this.port === 465) {
|
|
40211
42293
|
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
40212
|
-
await new Promise((
|
|
40213
|
-
socket.once("secureConnect",
|
|
42294
|
+
await new Promise((resolve21, reject) => {
|
|
42295
|
+
socket.once("secureConnect", resolve21);
|
|
40214
42296
|
socket.once("error", reject);
|
|
40215
42297
|
});
|
|
40216
42298
|
} else {
|
|
40217
42299
|
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
40218
|
-
await new Promise((
|
|
40219
|
-
socket.once("connect",
|
|
42300
|
+
await new Promise((resolve21, reject) => {
|
|
42301
|
+
socket.once("connect", resolve21);
|
|
40220
42302
|
socket.once("error", reject);
|
|
40221
42303
|
});
|
|
40222
42304
|
}
|
|
@@ -40251,14 +42333,14 @@ var init_messenger = __esm({
|
|
|
40251
42333
|
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
40252
42334
|
if (useTls) {
|
|
40253
42335
|
socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
40254
|
-
await new Promise((
|
|
40255
|
-
socket.once("secureConnect",
|
|
42336
|
+
await new Promise((resolve21, reject) => {
|
|
42337
|
+
socket.once("secureConnect", resolve21);
|
|
40256
42338
|
socket.once("error", reject);
|
|
40257
42339
|
});
|
|
40258
42340
|
} else {
|
|
40259
42341
|
socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
40260
|
-
await new Promise((
|
|
40261
|
-
socket.once("connect",
|
|
42342
|
+
await new Promise((resolve21, reject) => {
|
|
42343
|
+
socket.once("connect", resolve21);
|
|
40262
42344
|
socket.once("error", reject);
|
|
40263
42345
|
});
|
|
40264
42346
|
}
|
|
@@ -41168,16 +43250,16 @@ var init_htmlElement = __esm({
|
|
|
41168
43250
|
});
|
|
41169
43251
|
|
|
41170
43252
|
// src/ai.ts
|
|
41171
|
-
import { existsSync as
|
|
43253
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync21, writeFileSync as writeFileSync18, readFileSync as readFileSync26 } from "node:fs";
|
|
41172
43254
|
import { homedir } from "node:os";
|
|
41173
|
-
import { join as
|
|
43255
|
+
import { join as join32, resolve as resolve20, relative as relative10, dirname as dirname14 } from "node:path";
|
|
41174
43256
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
41175
43257
|
import { execSync, execFileSync as execFileSync4 } from "node:child_process";
|
|
41176
43258
|
import { createInterface } from "node:readline";
|
|
41177
43259
|
function readVersion() {
|
|
41178
43260
|
try {
|
|
41179
43261
|
const thisDir = dirname14(fileURLToPath7(import.meta.url));
|
|
41180
|
-
const rootPkg =
|
|
43262
|
+
const rootPkg = resolve20(thisDir, "..", "..", "..", "package.json");
|
|
41181
43263
|
const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
|
|
41182
43264
|
return pkg.version ?? "0.0.0";
|
|
41183
43265
|
} catch {
|
|
@@ -41241,8 +43323,8 @@ function downloadSkillsSync(jobs) {
|
|
|
41241
43323
|
function installSkills(root = ".", targets) {
|
|
41242
43324
|
const ref = skillsRef();
|
|
41243
43325
|
const dests = targets ?? [
|
|
41244
|
-
|
|
41245
|
-
|
|
43326
|
+
join32(resolve20(root), ".claude", "skills"),
|
|
43327
|
+
join32(homedir(), ".claude", "skills")
|
|
41246
43328
|
];
|
|
41247
43329
|
const jobs = [];
|
|
41248
43330
|
const index = /* @__PURE__ */ new Map();
|
|
@@ -41260,9 +43342,9 @@ function installSkills(root = ".", targets) {
|
|
|
41260
43342
|
const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
|
|
41261
43343
|
skillMdUrl[skill] = `${base}/SKILL.md`;
|
|
41262
43344
|
for (const dest of dests) {
|
|
41263
|
-
add(`${base}/SKILL.md`,
|
|
43345
|
+
add(`${base}/SKILL.md`, join32(dest, skill, "SKILL.md"));
|
|
41264
43346
|
for (const r of spec.references) {
|
|
41265
|
-
add(`${base}/references/${r}`,
|
|
43347
|
+
add(`${base}/references/${r}`, join32(dest, skill, "references", r));
|
|
41266
43348
|
}
|
|
41267
43349
|
}
|
|
41268
43350
|
}
|
|
@@ -41274,10 +43356,10 @@ function installSkills(root = ".", targets) {
|
|
|
41274
43356
|
return installed;
|
|
41275
43357
|
}
|
|
41276
43358
|
function isInstalled(root, tool) {
|
|
41277
|
-
return
|
|
43359
|
+
return existsSync27(join32(resolve20(root), tool.contextFile));
|
|
41278
43360
|
}
|
|
41279
43361
|
function showMenu(root = ".") {
|
|
41280
|
-
const r =
|
|
43362
|
+
const r = resolve20(root);
|
|
41281
43363
|
console.log("\n Tina4 AI Context Installer\n");
|
|
41282
43364
|
for (let i = 0; i < AI_TOOLS.length; i++) {
|
|
41283
43365
|
const tool = AI_TOOLS[i];
|
|
@@ -41295,16 +43377,16 @@ function showMenu(root = ".") {
|
|
|
41295
43377
|
const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
|
|
41296
43378
|
console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
|
|
41297
43379
|
console.log();
|
|
41298
|
-
return new Promise((
|
|
43380
|
+
return new Promise((resolve21) => {
|
|
41299
43381
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
41300
43382
|
rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
|
|
41301
43383
|
rl.close();
|
|
41302
|
-
|
|
43384
|
+
resolve21(answer.trim());
|
|
41303
43385
|
});
|
|
41304
43386
|
});
|
|
41305
43387
|
}
|
|
41306
43388
|
function installSelected(root, selection) {
|
|
41307
|
-
const rootPath =
|
|
43389
|
+
const rootPath = resolve20(root);
|
|
41308
43390
|
const created = [];
|
|
41309
43391
|
let indices;
|
|
41310
43392
|
let doInstallTina4Ai = false;
|
|
@@ -41397,35 +43479,35 @@ function looksLikeOldFrameworkInstall(existing) {
|
|
|
41397
43479
|
function writeOrMerge(contextPath, contextFile, frameworkGuide) {
|
|
41398
43480
|
const block = skillBlock(contextFile);
|
|
41399
43481
|
const [start2, end] = markersFor(contextFile);
|
|
41400
|
-
if (!
|
|
41401
|
-
|
|
43482
|
+
if (!existsSync27(contextPath)) {
|
|
43483
|
+
writeFileSync18(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
41402
43484
|
return "Installed";
|
|
41403
43485
|
}
|
|
41404
43486
|
const existing = readFileSync26(contextPath, "utf-8");
|
|
41405
43487
|
if (hasMarkers(existing, start2, end)) {
|
|
41406
|
-
|
|
43488
|
+
writeFileSync18(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
|
|
41407
43489
|
return "Refreshed skill block in";
|
|
41408
43490
|
}
|
|
41409
43491
|
if (looksLikeOldFrameworkInstall(existing)) {
|
|
41410
43492
|
const head = existing.replace(/^\s+/, "");
|
|
41411
43493
|
const preamble = existing.slice(0, existing.length - head.length);
|
|
41412
43494
|
const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
|
|
41413
|
-
|
|
43495
|
+
writeFileSync18(contextPath, newContent, "utf-8");
|
|
41414
43496
|
return "Migrated (replaced old framework dump in)";
|
|
41415
43497
|
}
|
|
41416
|
-
|
|
43498
|
+
writeFileSync18(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
41417
43499
|
return "Appended skill block to";
|
|
41418
43500
|
}
|
|
41419
43501
|
function installForTool(root, tool, context) {
|
|
41420
43502
|
const created = [];
|
|
41421
|
-
const contextPath =
|
|
43503
|
+
const contextPath = join32(root, tool.contextFile);
|
|
41422
43504
|
if (tool.configDir) {
|
|
41423
|
-
|
|
43505
|
+
mkdirSync21(join32(root, tool.configDir), { recursive: true });
|
|
41424
43506
|
}
|
|
41425
43507
|
const parentDir = dirname14(contextPath);
|
|
41426
|
-
|
|
43508
|
+
mkdirSync21(parentDir, { recursive: true });
|
|
41427
43509
|
const action = writeOrMerge(contextPath, tool.contextFile, context);
|
|
41428
|
-
const rel =
|
|
43510
|
+
const rel = relative10(root, contextPath);
|
|
41429
43511
|
created.push(rel);
|
|
41430
43512
|
console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
|
|
41431
43513
|
if (tool.name === "claude-code") {
|
|
@@ -41458,7 +43540,7 @@ function installTina4Ai() {
|
|
|
41458
43540
|
function installClaudeSkills(root) {
|
|
41459
43541
|
const created = [];
|
|
41460
43542
|
for (const skill of installSkills(root)) {
|
|
41461
|
-
created.push(
|
|
43543
|
+
created.push(join32(".claude", "skills", skill));
|
|
41462
43544
|
console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
|
|
41463
43545
|
}
|
|
41464
43546
|
return created;
|
|
@@ -41800,9 +43882,9 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
|
|
|
41800
43882
|
function generateClaudeCodeContext() {
|
|
41801
43883
|
try {
|
|
41802
43884
|
const thisDir = dirname14(fileURLToPath7(import.meta.url));
|
|
41803
|
-
const repoRoot =
|
|
41804
|
-
const claudeMdPath =
|
|
41805
|
-
if (
|
|
43885
|
+
const repoRoot = resolve20(thisDir, "..", "..", "..");
|
|
43886
|
+
const claudeMdPath = join32(repoRoot, "CLAUDE.md");
|
|
43887
|
+
if (existsSync27(claudeMdPath)) {
|
|
41806
43888
|
return readFileSync26(claudeMdPath, "utf-8");
|
|
41807
43889
|
}
|
|
41808
43890
|
} catch {
|
|
@@ -42349,11 +44431,11 @@ var init_aiClient = __esm({
|
|
|
42349
44431
|
const payload = JSON.stringify(body);
|
|
42350
44432
|
const controller = new AbortController();
|
|
42351
44433
|
const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
|
|
42352
|
-
return new Promise((
|
|
44434
|
+
return new Promise((resolve21, reject) => {
|
|
42353
44435
|
const client = url.protocol === "https:" ? https2 : http2;
|
|
42354
44436
|
const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
|
|
42355
44437
|
clearTimeout(connectTimer);
|
|
42356
|
-
|
|
44438
|
+
resolve21({ response, cleanup: () => {
|
|
42357
44439
|
clearTimeout(totalTimer);
|
|
42358
44440
|
clearTimeout(connectTimer);
|
|
42359
44441
|
} });
|
|
@@ -42382,7 +44464,7 @@ var init_aiClient = __esm({
|
|
|
42382
44464
|
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
42383
44465
|
const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
|
|
42384
44466
|
const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
|
|
42385
|
-
return new Promise((
|
|
44467
|
+
return new Promise((resolve21) => setTimeout(resolve21, delay));
|
|
42386
44468
|
}
|
|
42387
44469
|
static async requestJson(config, headers, body) {
|
|
42388
44470
|
const deadline = performance.now() + config.totalTimeout * 1e3;
|