tina4-nodejs 3.13.119 → 3.13.121
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 +21604 -21370
- package/packages/cli/src/bin.ts +13 -6
- package/packages/cli/src/commands/generate.ts +391 -32
- package/packages/cli/src/commands/migrateCreate.ts +32 -37
- package/packages/core/dist/index.js +2394 -339
- package/packages/core/src/mcp.ts +62 -11
- package/packages/orm/dist/index.js +2443 -388
- package/types/cli/src/commands/generate.d.ts +37 -1
- package/types/cli/src/commands/migrateCreate.d.ts +1 -1
|
@@ -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)) {
|
|
@@ -7753,7 +7753,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
|
|
|
7753
7753
|
const elapsedMs = () => performance.now() - startedAt;
|
|
7754
7754
|
if (budgetMs === null) return attempt();
|
|
7755
7755
|
const started = attempt();
|
|
7756
|
-
return new Promise((
|
|
7756
|
+
return new Promise((resolve21, reject) => {
|
|
7757
7757
|
let expired = false;
|
|
7758
7758
|
const timer = setTimeout(() => {
|
|
7759
7759
|
expired = true;
|
|
@@ -7763,7 +7763,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
|
|
|
7763
7763
|
(arrived) => {
|
|
7764
7764
|
clearTimeout(timer);
|
|
7765
7765
|
if (expired) abandon?.(arrived);
|
|
7766
|
-
else
|
|
7766
|
+
else resolve21(arrived);
|
|
7767
7767
|
},
|
|
7768
7768
|
(failure) => {
|
|
7769
7769
|
clearTimeout(timer);
|
|
@@ -8352,10 +8352,10 @@ var init_mysql = __esm({
|
|
|
8352
8352
|
...timeoutOption
|
|
8353
8353
|
});
|
|
8354
8354
|
}
|
|
8355
|
-
return new Promise((
|
|
8355
|
+
return new Promise((resolve21, reject) => {
|
|
8356
8356
|
this.connection.connect((err) => {
|
|
8357
8357
|
if (err) reject(err);
|
|
8358
|
-
else
|
|
8358
|
+
else resolve21();
|
|
8359
8359
|
});
|
|
8360
8360
|
});
|
|
8361
8361
|
},
|
|
@@ -8377,10 +8377,10 @@ var init_mysql = __esm({
|
|
|
8377
8377
|
}
|
|
8378
8378
|
}
|
|
8379
8379
|
queryPromise(sql, params) {
|
|
8380
|
-
return new Promise((
|
|
8380
|
+
return new Promise((resolve21, reject) => {
|
|
8381
8381
|
this.connection.query(sql, params ?? [], (err, results) => {
|
|
8382
8382
|
if (err) reject(err);
|
|
8383
|
-
else
|
|
8383
|
+
else resolve21(results);
|
|
8384
8384
|
});
|
|
8385
8385
|
});
|
|
8386
8386
|
}
|
|
@@ -8766,11 +8766,11 @@ var init_mssql = __esm({
|
|
|
8766
8766
|
};
|
|
8767
8767
|
}
|
|
8768
8768
|
await withConnectTimeout(
|
|
8769
|
-
() => new Promise((
|
|
8769
|
+
() => new Promise((resolve21, reject) => {
|
|
8770
8770
|
this.connection = new Connection(tediousConfig);
|
|
8771
8771
|
this.connection.on("connect", (err) => {
|
|
8772
8772
|
if (err) reject(err);
|
|
8773
|
-
else
|
|
8773
|
+
else resolve21();
|
|
8774
8774
|
});
|
|
8775
8775
|
this.connection.connect();
|
|
8776
8776
|
}),
|
|
@@ -8815,11 +8815,11 @@ var init_mssql = __esm({
|
|
|
8815
8815
|
const tediousModule = requireTedious();
|
|
8816
8816
|
const Request = tediousModule.Request;
|
|
8817
8817
|
const TYPES = tediousModule.TYPES;
|
|
8818
|
-
return new Promise((
|
|
8818
|
+
return new Promise((resolve21, reject) => {
|
|
8819
8819
|
const rows = [];
|
|
8820
8820
|
const request = new Request(sql, (err, rowCount) => {
|
|
8821
8821
|
if (err) reject(err);
|
|
8822
|
-
else
|
|
8822
|
+
else resolve21({ rows, rowCount });
|
|
8823
8823
|
});
|
|
8824
8824
|
if (params) {
|
|
8825
8825
|
params.forEach((p, i) => {
|
|
@@ -9021,8 +9021,8 @@ var init_mssql = __esm({
|
|
|
9021
9021
|
throw new Error("Use startTransactionAsync() for MSSQL.");
|
|
9022
9022
|
}
|
|
9023
9023
|
async startTransactionAsync() {
|
|
9024
|
-
await new Promise((
|
|
9025
|
-
this.connection.beginTransaction((err) => err ? reject(err) :
|
|
9024
|
+
await new Promise((resolve21, reject) => {
|
|
9025
|
+
this.connection.beginTransaction((err) => err ? reject(err) : resolve21());
|
|
9026
9026
|
});
|
|
9027
9027
|
this._inTransaction = true;
|
|
9028
9028
|
}
|
|
@@ -9030,8 +9030,8 @@ var init_mssql = __esm({
|
|
|
9030
9030
|
throw new Error("Use commitAsync() for MSSQL.");
|
|
9031
9031
|
}
|
|
9032
9032
|
async commitAsync() {
|
|
9033
|
-
await new Promise((
|
|
9034
|
-
this.connection.commitTransaction((err) => err ? reject(err) :
|
|
9033
|
+
await new Promise((resolve21, reject) => {
|
|
9034
|
+
this.connection.commitTransaction((err) => err ? reject(err) : resolve21());
|
|
9035
9035
|
});
|
|
9036
9036
|
this._inTransaction = false;
|
|
9037
9037
|
}
|
|
@@ -9039,8 +9039,8 @@ var init_mssql = __esm({
|
|
|
9039
9039
|
throw new Error("Use rollbackAsync() for MSSQL.");
|
|
9040
9040
|
}
|
|
9041
9041
|
async rollbackAsync() {
|
|
9042
|
-
await new Promise((
|
|
9043
|
-
this.connection.rollbackTransaction((err) => err ? reject(err) :
|
|
9042
|
+
await new Promise((resolve21, reject) => {
|
|
9043
|
+
this.connection.rollbackTransaction((err) => err ? reject(err) : resolve21());
|
|
9044
9044
|
});
|
|
9045
9045
|
this._inTransaction = false;
|
|
9046
9046
|
}
|
|
@@ -9365,8 +9365,8 @@ var init_firebird = __esm({
|
|
|
9365
9365
|
}
|
|
9366
9366
|
attachOnce(config) {
|
|
9367
9367
|
const fb = requireFirebird();
|
|
9368
|
-
return new Promise((
|
|
9369
|
-
fb.attach(config, (err, db) => err ? reject(err) :
|
|
9368
|
+
return new Promise((resolve21, reject) => {
|
|
9369
|
+
fb.attach(config, (err, db) => err ? reject(err) : resolve21(db));
|
|
9370
9370
|
});
|
|
9371
9371
|
}
|
|
9372
9372
|
/**
|
|
@@ -9385,7 +9385,7 @@ var init_firebird = __esm({
|
|
|
9385
9385
|
} catch (err) {
|
|
9386
9386
|
lastError = err;
|
|
9387
9387
|
if (attempt < attempts - 1) {
|
|
9388
|
-
await new Promise((
|
|
9388
|
+
await new Promise((resolve21) => setTimeout(resolve21, 100 * (attempt + 1)));
|
|
9389
9389
|
}
|
|
9390
9390
|
}
|
|
9391
9391
|
}
|
|
@@ -9468,19 +9468,19 @@ var init_firebird = __esm({
|
|
|
9468
9468
|
}
|
|
9469
9469
|
queryPromise(sql, params) {
|
|
9470
9470
|
const translated = this.translateSql(sql);
|
|
9471
|
-
return this.withReconnect(() => new Promise((
|
|
9471
|
+
return this.withReconnect(() => new Promise((resolve21, reject) => {
|
|
9472
9472
|
this.statementHandle().query(translated, params ?? [], (err, result) => {
|
|
9473
9473
|
if (err) reject(err);
|
|
9474
|
-
else
|
|
9474
|
+
else resolve21(result ?? []);
|
|
9475
9475
|
});
|
|
9476
9476
|
}));
|
|
9477
9477
|
}
|
|
9478
9478
|
executePromise(sql, params) {
|
|
9479
9479
|
const translated = this.translateSql(sql);
|
|
9480
|
-
return this.withReconnect(() => new Promise((
|
|
9480
|
+
return this.withReconnect(() => new Promise((resolve21, reject) => {
|
|
9481
9481
|
this.statementHandle().execute(translated, params ?? [], (err) => {
|
|
9482
9482
|
if (err) reject(err);
|
|
9483
|
-
else
|
|
9483
|
+
else resolve21();
|
|
9484
9484
|
});
|
|
9485
9485
|
}));
|
|
9486
9486
|
}
|
|
@@ -9519,13 +9519,13 @@ var init_firebird = __esm({
|
|
|
9519
9519
|
* and no bytes round-tripped (FB-BLOB-SRP-UNVERIFIED).
|
|
9520
9520
|
*/
|
|
9521
9521
|
readBlob(blobFn) {
|
|
9522
|
-
return new Promise((
|
|
9522
|
+
return new Promise((resolve21, reject) => {
|
|
9523
9523
|
blobFn((err, _name, emitter) => {
|
|
9524
9524
|
if (err) return reject(err);
|
|
9525
|
-
if (!emitter) return
|
|
9525
|
+
if (!emitter) return resolve21(null);
|
|
9526
9526
|
const chunks = [];
|
|
9527
9527
|
emitter.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
9528
|
-
emitter.on("end", () =>
|
|
9528
|
+
emitter.on("end", () => resolve21(Buffer.concat(chunks)));
|
|
9529
9529
|
emitter.on("error", (streamErr) => reject(streamErr));
|
|
9530
9530
|
});
|
|
9531
9531
|
});
|
|
@@ -9661,12 +9661,12 @@ var init_firebird = __esm({
|
|
|
9661
9661
|
}
|
|
9662
9662
|
async startTransactionAsync() {
|
|
9663
9663
|
this.ensureConnected();
|
|
9664
|
-
await new Promise((
|
|
9664
|
+
await new Promise((resolve21, reject) => {
|
|
9665
9665
|
this.db.transaction(0, (err, transaction) => {
|
|
9666
9666
|
if (err) reject(err);
|
|
9667
9667
|
else {
|
|
9668
9668
|
this.transaction = transaction;
|
|
9669
|
-
|
|
9669
|
+
resolve21();
|
|
9670
9670
|
}
|
|
9671
9671
|
});
|
|
9672
9672
|
});
|
|
@@ -9676,12 +9676,12 @@ var init_firebird = __esm({
|
|
|
9676
9676
|
}
|
|
9677
9677
|
async commitAsync() {
|
|
9678
9678
|
if (!this.transaction) throw new Error("No active transaction to commit.");
|
|
9679
|
-
await new Promise((
|
|
9679
|
+
await new Promise((resolve21, reject) => {
|
|
9680
9680
|
this.transaction.commit((err) => {
|
|
9681
9681
|
if (err) reject(err);
|
|
9682
9682
|
else {
|
|
9683
9683
|
this.transaction = null;
|
|
9684
|
-
|
|
9684
|
+
resolve21();
|
|
9685
9685
|
}
|
|
9686
9686
|
});
|
|
9687
9687
|
});
|
|
@@ -9691,12 +9691,12 @@ var init_firebird = __esm({
|
|
|
9691
9691
|
}
|
|
9692
9692
|
async rollbackAsync() {
|
|
9693
9693
|
if (!this.transaction) throw new Error("No active transaction to rollback.");
|
|
9694
|
-
await new Promise((
|
|
9694
|
+
await new Promise((resolve21, reject) => {
|
|
9695
9695
|
this.transaction.rollback((err) => {
|
|
9696
9696
|
if (err) reject(err);
|
|
9697
9697
|
else {
|
|
9698
9698
|
this.transaction = null;
|
|
9699
|
-
|
|
9699
|
+
resolve21();
|
|
9700
9700
|
}
|
|
9701
9701
|
});
|
|
9702
9702
|
});
|
|
@@ -12845,7 +12845,7 @@ async function createMigration(description, options) {
|
|
|
12845
12845
|
}
|
|
12846
12846
|
const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
12847
12847
|
const now = /* @__PURE__ */ new Date();
|
|
12848
|
-
const
|
|
12848
|
+
const timestamp2 = [
|
|
12849
12849
|
now.getFullYear(),
|
|
12850
12850
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
12851
12851
|
String(now.getDate()).padStart(2, "0"),
|
|
@@ -12853,8 +12853,8 @@ async function createMigration(description, options) {
|
|
|
12853
12853
|
String(now.getMinutes()).padStart(2, "0"),
|
|
12854
12854
|
String(now.getSeconds()).padStart(2, "0")
|
|
12855
12855
|
].join("");
|
|
12856
|
-
const upFileName = `${
|
|
12857
|
-
const downFileName = `${
|
|
12856
|
+
const upFileName = `${timestamp2}_${safeName}.sql`;
|
|
12857
|
+
const downFileName = `${timestamp2}_${safeName}.down.sql`;
|
|
12858
12858
|
const upPath = join7(dir, upFileName);
|
|
12859
12859
|
const downPath = join7(dir, downFileName);
|
|
12860
12860
|
const upTemplate = `-- Migration: ${description}
|
|
@@ -12877,7 +12877,7 @@ async function createClassMigration(description, options) {
|
|
|
12877
12877
|
const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
12878
12878
|
const className = description.replace(/[^a-zA-Z0-9 ]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
12879
12879
|
const now = /* @__PURE__ */ new Date();
|
|
12880
|
-
const
|
|
12880
|
+
const timestamp2 = [
|
|
12881
12881
|
now.getFullYear(),
|
|
12882
12882
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
12883
12883
|
String(now.getDate()).padStart(2, "0"),
|
|
@@ -12885,7 +12885,7 @@ async function createClassMigration(description, options) {
|
|
|
12885
12885
|
String(now.getMinutes()).padStart(2, "0"),
|
|
12886
12886
|
String(now.getSeconds()).padStart(2, "0")
|
|
12887
12887
|
].join("");
|
|
12888
|
-
const fileName = `${
|
|
12888
|
+
const fileName = `${timestamp2}_${safeName}.ts`;
|
|
12889
12889
|
const filePath = join7(dir, fileName);
|
|
12890
12890
|
const content = `// Migration: ${description}
|
|
12891
12891
|
// Created: ${now.toISOString()}
|
|
@@ -18802,7 +18802,7 @@ ${s}\r
|
|
|
18802
18802
|
connect() {
|
|
18803
18803
|
if (this.connected) return Promise.resolve();
|
|
18804
18804
|
if (this.connecting) return this.connecting;
|
|
18805
|
-
this.connecting = new Promise((
|
|
18805
|
+
this.connecting = new Promise((resolve21, reject) => {
|
|
18806
18806
|
const sock = net.createConnection({ host: this.host, port: this.port });
|
|
18807
18807
|
sock.setNoDelay(true);
|
|
18808
18808
|
const onError = (err) => {
|
|
@@ -18839,7 +18839,7 @@ ${s}\r
|
|
|
18839
18839
|
sock.on("error", (e) => {
|
|
18840
18840
|
this.brokenError = e;
|
|
18841
18841
|
});
|
|
18842
|
-
|
|
18842
|
+
resolve21();
|
|
18843
18843
|
} catch (e) {
|
|
18844
18844
|
onError(e);
|
|
18845
18845
|
}
|
|
@@ -18902,12 +18902,12 @@ ${s}\r
|
|
|
18902
18902
|
}
|
|
18903
18903
|
/** Send one command and await its reply (assumes socket is up). */
|
|
18904
18904
|
raw(args) {
|
|
18905
|
-
return new Promise((
|
|
18905
|
+
return new Promise((resolve21, reject) => {
|
|
18906
18906
|
if (!this.sock || this.sock.destroyed) {
|
|
18907
18907
|
reject(this.brokenError ?? new Error("redis socket not connected"));
|
|
18908
18908
|
return;
|
|
18909
18909
|
}
|
|
18910
|
-
this.waiters.push({ resolve:
|
|
18910
|
+
this.waiters.push({ resolve: resolve21, reject });
|
|
18911
18911
|
this.sock.write(_RespClient.encode(args));
|
|
18912
18912
|
});
|
|
18913
18913
|
}
|
|
@@ -19249,7 +19249,7 @@ ${s}\r
|
|
|
19249
19249
|
connect() {
|
|
19250
19250
|
if (this.connected) return Promise.resolve();
|
|
19251
19251
|
if (this.connecting) return this.connecting;
|
|
19252
|
-
this.connecting = new Promise((
|
|
19252
|
+
this.connecting = new Promise((resolve21, reject) => {
|
|
19253
19253
|
const sock = net.createConnection({ host: this.host, port: this.port });
|
|
19254
19254
|
sock.setNoDelay(true);
|
|
19255
19255
|
sock.once("error", (err) => {
|
|
@@ -19270,7 +19270,7 @@ ${s}\r
|
|
|
19270
19270
|
p.resolve(this.buffer.toString("utf-8"));
|
|
19271
19271
|
}
|
|
19272
19272
|
});
|
|
19273
|
-
|
|
19273
|
+
resolve21();
|
|
19274
19274
|
});
|
|
19275
19275
|
});
|
|
19276
19276
|
return this.connecting;
|
|
@@ -19303,13 +19303,13 @@ ${s}\r
|
|
|
19303
19303
|
async send(payload, terminator) {
|
|
19304
19304
|
await this.connect();
|
|
19305
19305
|
if (!this.sock || this.sock.destroyed) return "";
|
|
19306
|
-
return new Promise((
|
|
19306
|
+
return new Promise((resolve21) => {
|
|
19307
19307
|
this.buffer = Buffer.alloc(0);
|
|
19308
|
-
this.pending = { terminator, resolve:
|
|
19308
|
+
this.pending = { terminator, resolve: resolve21 };
|
|
19309
19309
|
const timer = setTimeout(() => {
|
|
19310
|
-
if (this.pending && this.pending.resolve ===
|
|
19310
|
+
if (this.pending && this.pending.resolve === resolve21) {
|
|
19311
19311
|
this.pending = null;
|
|
19312
|
-
|
|
19312
|
+
resolve21(this.buffer.toString("utf-8"));
|
|
19313
19313
|
}
|
|
19314
19314
|
}, 4e3);
|
|
19315
19315
|
if (timer.unref) timer.unref();
|
|
@@ -21075,7 +21075,7 @@ async function parseBody(req2) {
|
|
|
21075
21075
|
}
|
|
21076
21076
|
const contentType = req2.headers["content-type"] ?? "";
|
|
21077
21077
|
const chunks = [];
|
|
21078
|
-
await new Promise((
|
|
21078
|
+
await new Promise((resolve21, reject) => {
|
|
21079
21079
|
let received = 0;
|
|
21080
21080
|
let refused = false;
|
|
21081
21081
|
req2.on("data", (chunk) => {
|
|
@@ -21090,7 +21090,7 @@ async function parseBody(req2) {
|
|
|
21090
21090
|
chunks.push(chunk);
|
|
21091
21091
|
});
|
|
21092
21092
|
req2.on("end", () => {
|
|
21093
|
-
if (!refused)
|
|
21093
|
+
if (!refused) resolve21();
|
|
21094
21094
|
});
|
|
21095
21095
|
req2.on("error", reject);
|
|
21096
21096
|
});
|
|
@@ -24323,6 +24323,2038 @@ var init_version = __esm({
|
|
|
24323
24323
|
}
|
|
24324
24324
|
});
|
|
24325
24325
|
|
|
24326
|
+
// ../cli/src/commands/generate.ts
|
|
24327
|
+
var generate_exports = {};
|
|
24328
|
+
__export(generate_exports, {
|
|
24329
|
+
DEFAULT_FIELDS: () => DEFAULT_FIELDS,
|
|
24330
|
+
GENERATORS: () => GENERATORS,
|
|
24331
|
+
RESOLUTION_ENVELOPE_VERSION: () => RESOLUTION_ENVELOPE_VERSION,
|
|
24332
|
+
SQL_RESERVED_TABLE_NAMES: () => SQL_RESERVED_TABLE_NAMES,
|
|
24333
|
+
aiFill: () => aiFill,
|
|
24334
|
+
currentResolution: () => currentResolution,
|
|
24335
|
+
extend: () => extend,
|
|
24336
|
+
fieldsOrDefault: () => fieldsOrDefault,
|
|
24337
|
+
generate: () => generate,
|
|
24338
|
+
generateMigration: () => generateMigration,
|
|
24339
|
+
generateProgrammatic: () => generateProgrammatic,
|
|
24340
|
+
parseCliArgs: () => parseCliArgs,
|
|
24341
|
+
parseEvery: () => parseEvery,
|
|
24342
|
+
parseFields: () => parseFields,
|
|
24343
|
+
pluralizeReserved: () => pluralizeReserved,
|
|
24344
|
+
toPascal: () => toPascal,
|
|
24345
|
+
toSnake: () => toSnake,
|
|
24346
|
+
toTableName: () => toTableName
|
|
24347
|
+
});
|
|
24348
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync9 } from "node:fs";
|
|
24349
|
+
import { join as join20, relative as relative2, resolve as resolve10, sep as sep4 } from "node:path";
|
|
24350
|
+
function ensureDir(dir) {
|
|
24351
|
+
if (__resolution.dryRun) return;
|
|
24352
|
+
if (!existsSync15(dir)) {
|
|
24353
|
+
mkdirSync12(dir, { recursive: true });
|
|
24354
|
+
}
|
|
24355
|
+
}
|
|
24356
|
+
function writeFileSafe(path8, content) {
|
|
24357
|
+
captureEditHints(path8, content);
|
|
24358
|
+
if (__resolution.dryRun) {
|
|
24359
|
+
return;
|
|
24360
|
+
}
|
|
24361
|
+
if (existsSync15(path8)) {
|
|
24362
|
+
if (!__resolution.jsonMode) console.log(` File already exists: ${path8}`);
|
|
24363
|
+
return;
|
|
24364
|
+
}
|
|
24365
|
+
writeFileSync9(path8, content, "utf-8");
|
|
24366
|
+
__resolution.actionsTaken.push(`wrote ${path8}`);
|
|
24367
|
+
if (!__resolution.jsonMode) console.log(` Created ${path8}`);
|
|
24368
|
+
}
|
|
24369
|
+
function toSnake(name) {
|
|
24370
|
+
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
24371
|
+
}
|
|
24372
|
+
function pluralizeReserved(name) {
|
|
24373
|
+
if (name.endsWith("y") && !/[aeiouy]y$/i.test(name)) return name.slice(0, -1) + "ies";
|
|
24374
|
+
if (/(s|x|z|ch|sh)$/.test(name)) return name + "es";
|
|
24375
|
+
return name + "s";
|
|
24376
|
+
}
|
|
24377
|
+
function toTableName(name) {
|
|
24378
|
+
const raw = toSnake(name);
|
|
24379
|
+
if (SQL_RESERVED_TABLE_NAMES.has(raw)) {
|
|
24380
|
+
const safe = pluralizeReserved(raw);
|
|
24381
|
+
recordTransformation({
|
|
24382
|
+
kind: "reserved_word_pluralize",
|
|
24383
|
+
from: raw,
|
|
24384
|
+
to: safe,
|
|
24385
|
+
reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
|
|
24386
|
+
override: `--table ${raw} --quote (requires quoted-identifier mode, not yet implemented)`
|
|
24387
|
+
});
|
|
24388
|
+
return safe;
|
|
24389
|
+
}
|
|
24390
|
+
return raw;
|
|
24391
|
+
}
|
|
24392
|
+
function resetResolution(target, input, opts) {
|
|
24393
|
+
__resolution.target = target;
|
|
24394
|
+
__resolution.input = input;
|
|
24395
|
+
__resolution.body = { transformations: [] };
|
|
24396
|
+
__resolution.actionsTaken = [];
|
|
24397
|
+
__resolution.dryRun = opts.dryRun;
|
|
24398
|
+
__resolution.jsonMode = opts.jsonMode;
|
|
24399
|
+
}
|
|
24400
|
+
function recordTransformation(t) {
|
|
24401
|
+
__resolution.body.transformations.push(t);
|
|
24402
|
+
}
|
|
24403
|
+
function currentResolution() {
|
|
24404
|
+
const body = {
|
|
24405
|
+
...__resolution.body,
|
|
24406
|
+
transformations: [...__resolution.body.transformations]
|
|
24407
|
+
};
|
|
24408
|
+
if (__resolution.body.edit_hints) {
|
|
24409
|
+
body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
|
|
24410
|
+
}
|
|
24411
|
+
if (__resolution.body.next) {
|
|
24412
|
+
body.next = [...__resolution.body.next];
|
|
24413
|
+
}
|
|
24414
|
+
if (__resolution.body.test_paths) {
|
|
24415
|
+
body.test_paths = [...__resolution.body.test_paths];
|
|
24416
|
+
}
|
|
24417
|
+
if (__resolution.body.routes) {
|
|
24418
|
+
body.routes = [...__resolution.body.routes];
|
|
24419
|
+
}
|
|
24420
|
+
return {
|
|
24421
|
+
command: "generate",
|
|
24422
|
+
target: __resolution.target,
|
|
24423
|
+
input: { ...__resolution.input },
|
|
24424
|
+
resolution: body,
|
|
24425
|
+
actions_taken: [...__resolution.actionsTaken],
|
|
24426
|
+
dry_run: __resolution.dryRun
|
|
24427
|
+
};
|
|
24428
|
+
}
|
|
24429
|
+
function setResolutionField(key, value) {
|
|
24430
|
+
__resolution.body[key] = value;
|
|
24431
|
+
}
|
|
24432
|
+
function pushRoute(routePattern) {
|
|
24433
|
+
if (!__resolution.body.routes) __resolution.body.routes = [];
|
|
24434
|
+
__resolution.body.routes.push(routePattern);
|
|
24435
|
+
}
|
|
24436
|
+
function pushTestPath(path8) {
|
|
24437
|
+
if (!__resolution.body.test_paths) __resolution.body.test_paths = [];
|
|
24438
|
+
__resolution.body.test_paths.push(path8);
|
|
24439
|
+
}
|
|
24440
|
+
function pushEditHint(hint) {
|
|
24441
|
+
if (!__resolution.body.edit_hints) __resolution.body.edit_hints = [];
|
|
24442
|
+
__resolution.body.edit_hints.push(hint);
|
|
24443
|
+
}
|
|
24444
|
+
function setNextSteps(steps) {
|
|
24445
|
+
if (steps.length === 0) return;
|
|
24446
|
+
__resolution.body.next = [...steps];
|
|
24447
|
+
}
|
|
24448
|
+
function toRelPath(absPath) {
|
|
24449
|
+
const cwd = process.cwd();
|
|
24450
|
+
const rel = relative2(cwd, absPath);
|
|
24451
|
+
if (!rel) return absPath;
|
|
24452
|
+
return sep4 === "/" ? rel : rel.split(sep4).join("/");
|
|
24453
|
+
}
|
|
24454
|
+
function captureEditHints(absPath, content) {
|
|
24455
|
+
if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
|
|
24456
|
+
const relPath = toRelPath(absPath);
|
|
24457
|
+
const lines = content.split("\n");
|
|
24458
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24459
|
+
const match = TINA4_EDIT_MARKER.exec(lines[i]);
|
|
24460
|
+
if (match) {
|
|
24461
|
+
pushEditHint({ file: relPath, line: i + 1, label: match[1].trim() });
|
|
24462
|
+
}
|
|
24463
|
+
}
|
|
24464
|
+
}
|
|
24465
|
+
function printResolution() {
|
|
24466
|
+
if (__resolution.jsonMode) {
|
|
24467
|
+
process.stdout.write(JSON.stringify(currentResolution(), null, 2) + "\n");
|
|
24468
|
+
return;
|
|
24469
|
+
}
|
|
24470
|
+
const b = __resolution.body;
|
|
24471
|
+
const lines = [];
|
|
24472
|
+
lines.push("");
|
|
24473
|
+
lines.push(`Generated ${__resolution.target} ${__resolution.input.name}`);
|
|
24474
|
+
if (b.class_name || b.file_path) {
|
|
24475
|
+
const where = b.file_path ? ` (in ${b.file_path})` : "";
|
|
24476
|
+
lines.push(` class ${b.class_name ?? __resolution.input.name}${where}`);
|
|
24477
|
+
}
|
|
24478
|
+
if (b.table_name) {
|
|
24479
|
+
const t = b.transformations.find((x) => x.kind === "reserved_word_pluralize");
|
|
24480
|
+
const note = t ? ` (auto-pluralized: '${t.from}' is a SQL reserved word)` : "";
|
|
24481
|
+
lines.push(` table ${b.table_name}${note}`);
|
|
24482
|
+
}
|
|
24483
|
+
if (b.routes && b.routes.length) {
|
|
24484
|
+
lines.push(` routes ${b.routes.join(", ")}`);
|
|
24485
|
+
}
|
|
24486
|
+
if (b.migration_path) {
|
|
24487
|
+
lines.push(` migration ${b.migration_path}`);
|
|
24488
|
+
}
|
|
24489
|
+
const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
|
|
24490
|
+
if (reserved && reserved.from && reserved.override) {
|
|
24491
|
+
lines.push("");
|
|
24492
|
+
lines.push(` To keep the raw name '${reserved.from}' as the table:`);
|
|
24493
|
+
lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
|
|
24494
|
+
}
|
|
24495
|
+
if (b.test_paths && b.test_paths.length > 0) {
|
|
24496
|
+
lines.push("");
|
|
24497
|
+
lines.push(" Tests:");
|
|
24498
|
+
for (const testPath of b.test_paths) lines.push(` ${testPath}`);
|
|
24499
|
+
}
|
|
24500
|
+
if (b.edit_hints && b.edit_hints.length > 0) {
|
|
24501
|
+
lines.push("");
|
|
24502
|
+
lines.push(" Edit these lines:");
|
|
24503
|
+
for (const hint of b.edit_hints) {
|
|
24504
|
+
lines.push(` ${hint.file}:${hint.line} ${hint.label}`);
|
|
24505
|
+
}
|
|
24506
|
+
}
|
|
24507
|
+
if (b.next && b.next.length > 0) {
|
|
24508
|
+
lines.push("");
|
|
24509
|
+
lines.push(" Next:");
|
|
24510
|
+
for (const step of b.next) lines.push(` ${step}`);
|
|
24511
|
+
}
|
|
24512
|
+
lines.push("");
|
|
24513
|
+
process.stderr.write(lines.join("\n"));
|
|
24514
|
+
}
|
|
24515
|
+
function toPlural(name) {
|
|
24516
|
+
const lower = name.toLowerCase();
|
|
24517
|
+
if (lower.endsWith("s")) return lower;
|
|
24518
|
+
if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
|
|
24519
|
+
return lower + "s";
|
|
24520
|
+
}
|
|
24521
|
+
function toCamel(name) {
|
|
24522
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
24523
|
+
}
|
|
24524
|
+
function toPascal(name) {
|
|
24525
|
+
return name.split(/[^0-9a-zA-Z]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
24526
|
+
}
|
|
24527
|
+
function parseFields(fieldsStr) {
|
|
24528
|
+
if (!fieldsStr || !fieldsStr.trim()) return [];
|
|
24529
|
+
const result = [];
|
|
24530
|
+
for (const part of fieldsStr.split(",")) {
|
|
24531
|
+
const trimmed = part.trim();
|
|
24532
|
+
if (trimmed.includes(":")) {
|
|
24533
|
+
const [fname, ftype] = trimmed.split(":", 2);
|
|
24534
|
+
if (fname.trim()) result.push([fname.trim(), ftype.trim().toLowerCase()]);
|
|
24535
|
+
} else if (trimmed) {
|
|
24536
|
+
result.push([trimmed, "string"]);
|
|
24537
|
+
}
|
|
24538
|
+
}
|
|
24539
|
+
return result;
|
|
24540
|
+
}
|
|
24541
|
+
function fieldsOrDefault(fieldsStr) {
|
|
24542
|
+
const parsed = parseFields(fieldsStr);
|
|
24543
|
+
return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
24544
|
+
}
|
|
24545
|
+
function parseCliArgs(args) {
|
|
24546
|
+
const booleanFlags = /* @__PURE__ */ new Set([
|
|
24547
|
+
"no-browser",
|
|
24548
|
+
"no-reload",
|
|
24549
|
+
"production",
|
|
24550
|
+
"managed",
|
|
24551
|
+
"all",
|
|
24552
|
+
"clear",
|
|
24553
|
+
"public",
|
|
24554
|
+
"no-migration",
|
|
24555
|
+
// Suppress the co-emitted migration test (used by the migrate:create
|
|
24556
|
+
// delegation — a plain migrate:create is "just a migration, no test",
|
|
24557
|
+
// matching its pre-3.13.121 UX now that it routes through generate migration).
|
|
24558
|
+
"no-test",
|
|
24559
|
+
// Resolution transparency (Feature B, 3.13.117): both accept NO value.
|
|
24560
|
+
"json",
|
|
24561
|
+
"dry-run"
|
|
24562
|
+
]);
|
|
24563
|
+
const flags = {};
|
|
24564
|
+
const positional = [];
|
|
24565
|
+
let i = 0;
|
|
24566
|
+
while (i < args.length) {
|
|
24567
|
+
if (args[i].startsWith("--")) {
|
|
24568
|
+
const key = args[i].slice(2);
|
|
24569
|
+
if (booleanFlags.has(key)) {
|
|
24570
|
+
flags[key] = true;
|
|
24571
|
+
i += 1;
|
|
24572
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
24573
|
+
flags[key] = args[i + 1];
|
|
24574
|
+
i += 2;
|
|
24575
|
+
} else {
|
|
24576
|
+
flags[key] = true;
|
|
24577
|
+
i += 1;
|
|
24578
|
+
}
|
|
24579
|
+
} else {
|
|
24580
|
+
positional.push(args[i]);
|
|
24581
|
+
i += 1;
|
|
24582
|
+
}
|
|
24583
|
+
}
|
|
24584
|
+
return { flags, positional };
|
|
24585
|
+
}
|
|
24586
|
+
function parseEvery(every) {
|
|
24587
|
+
if (!every || every === true) return 60;
|
|
24588
|
+
const s = String(every).trim().toLowerCase();
|
|
24589
|
+
const units = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
24590
|
+
const unit = s.slice(-1);
|
|
24591
|
+
if (unit in units) {
|
|
24592
|
+
const n2 = parseFloat(s.slice(0, -1));
|
|
24593
|
+
return Number.isFinite(n2) ? Math.max(1, Math.round(n2 * units[unit])) : 60;
|
|
24594
|
+
}
|
|
24595
|
+
const n = parseFloat(s);
|
|
24596
|
+
return Number.isFinite(n) ? Math.max(1, Math.round(n)) : 60;
|
|
24597
|
+
}
|
|
24598
|
+
function aiFill(fn, spec, indent = " ") {
|
|
24599
|
+
const rule = (label) => "\u2500".repeat(Math.max(4, 46 - label.length));
|
|
24600
|
+
const lines = [`${indent}// \u2500\u2500\u2500 AI-FILL: ${fn} ${rule(fn)}`];
|
|
24601
|
+
lines.push(`${indent}// Intent: ${spec.intent}`);
|
|
24602
|
+
if (spec.given) lines.push(`${indent}// Given: ${spec.given}`);
|
|
24603
|
+
lines.push(`${indent}// Use: ${spec.use}`);
|
|
24604
|
+
if (spec.ret) lines.push(`${indent}// Return: ${spec.ret}`);
|
|
24605
|
+
lines.push(`${indent}// Ground: ${spec.ground}`);
|
|
24606
|
+
lines.push(`${indent}throw new Error(${JSON.stringify(spec.raise)}); // remove when implemented`);
|
|
24607
|
+
lines.push(`${indent}// ${"\u2500".repeat(52)}`);
|
|
24608
|
+
return lines.join("\n") + "\n";
|
|
24609
|
+
}
|
|
24610
|
+
function extend(note, hint = "", indent = " ") {
|
|
24611
|
+
let out = `${indent}// \u2500\u2500\u2500 EXTEND: ${note} ${"\u2500".repeat(Math.max(4, 46 - note.length))}
|
|
24612
|
+
`;
|
|
24613
|
+
if (hint) out += `${indent}// ${hint}
|
|
24614
|
+
`;
|
|
24615
|
+
return out;
|
|
24616
|
+
}
|
|
24617
|
+
function timestamp() {
|
|
24618
|
+
const now = /* @__PURE__ */ new Date();
|
|
24619
|
+
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");
|
|
24620
|
+
}
|
|
24621
|
+
function isoNow() {
|
|
24622
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
24623
|
+
}
|
|
24624
|
+
async function generate(what, name, extraArgs = []) {
|
|
24625
|
+
if (!what) {
|
|
24626
|
+
console.error(" Usage: tina4nodejs generate <what> <name> [options]");
|
|
24627
|
+
console.error(` Generators: ${GENERATOR_LIST}`);
|
|
24628
|
+
console.error(' Options: --fields "name:string,price:float" --model ModelName');
|
|
24629
|
+
console.error(" --public open a route's writes (default: secure)");
|
|
24630
|
+
console.error(' --every 5m | --cron "\u2026" service schedule');
|
|
24631
|
+
console.error(" --json emit machine-readable resolution envelope on stdout");
|
|
24632
|
+
console.error(" --dry-run report resolution without writing any files");
|
|
24633
|
+
process.exit(1);
|
|
24634
|
+
}
|
|
24635
|
+
const noNameGenerators = /* @__PURE__ */ new Set(["auth"]);
|
|
24636
|
+
if (noNameGenerators.has(what) && name.startsWith("--")) {
|
|
24637
|
+
extraArgs = [name, ...extraArgs];
|
|
24638
|
+
name = "";
|
|
24639
|
+
}
|
|
24640
|
+
if (!noNameGenerators.has(what) && !name) {
|
|
24641
|
+
console.error(` Usage: tina4nodejs generate ${what} <name> [options]`);
|
|
24642
|
+
process.exit(1);
|
|
24643
|
+
}
|
|
24644
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
24645
|
+
const jsonMode = Boolean(flags.json);
|
|
24646
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
24647
|
+
resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode });
|
|
24648
|
+
const spec = GENERATORS[what];
|
|
24649
|
+
if (spec) {
|
|
24650
|
+
spec.handler(name, flags);
|
|
24651
|
+
} else {
|
|
24652
|
+
console.error(` Unknown generator: ${what}`);
|
|
24653
|
+
console.error(` Available: ${GENERATOR_LIST}`);
|
|
24654
|
+
process.exit(1);
|
|
24655
|
+
}
|
|
24656
|
+
const nextFn = NEXT_STEPS[what];
|
|
24657
|
+
if (nextFn) {
|
|
24658
|
+
const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
|
|
24659
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
24660
|
+
}
|
|
24661
|
+
printResolution();
|
|
24662
|
+
}
|
|
24663
|
+
async function generateProgrammatic(what, name, extraArgs = []) {
|
|
24664
|
+
const spec = GENERATORS[what];
|
|
24665
|
+
if (!spec) throw new Error(`Unknown generator: ${what} (available: ${GENERATOR_LIST})`);
|
|
24666
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
24667
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
24668
|
+
resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode: true });
|
|
24669
|
+
spec.handler(name, flags);
|
|
24670
|
+
const nextFn = NEXT_STEPS[what];
|
|
24671
|
+
if (nextFn) {
|
|
24672
|
+
const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
|
|
24673
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
24674
|
+
}
|
|
24675
|
+
return currentResolution();
|
|
24676
|
+
}
|
|
24677
|
+
function generateModel(name, flags, emitTest = true) {
|
|
24678
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
24679
|
+
const table2 = toTableName(name);
|
|
24680
|
+
const dir = resolve10("src/models");
|
|
24681
|
+
ensureDir(dir);
|
|
24682
|
+
const path8 = join20(dir, `${name}.ts`);
|
|
24683
|
+
setResolutionField("class_name", name);
|
|
24684
|
+
setResolutionField("table_name", table2);
|
|
24685
|
+
setResolutionField("file_path", `src/models/${name}.ts`);
|
|
24686
|
+
pushTestPath(`tests/${table2}_model.test.ts`);
|
|
24687
|
+
const fieldLines = [
|
|
24688
|
+
` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`,
|
|
24689
|
+
` // tina4:edit add or change fields for this model (string,int,float,bool,text,datetime)`
|
|
24690
|
+
];
|
|
24691
|
+
for (const [fname, ftype] of fields) {
|
|
24692
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
24693
|
+
fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
|
|
24694
|
+
}
|
|
24695
|
+
fieldLines.push(` created_at: { type: "datetime" as const },`);
|
|
24696
|
+
const content = `import { BaseModel } from "tina4-nodejs/orm";
|
|
24697
|
+
|
|
24698
|
+
export default class ${name} extends BaseModel {
|
|
24699
|
+
static tableName = "${table2}";
|
|
24700
|
+
static fields = {
|
|
24701
|
+
${fieldLines.join("\n")}
|
|
24702
|
+
};
|
|
24703
|
+
}
|
|
24704
|
+
`;
|
|
24705
|
+
writeFileSafe(path8, content);
|
|
24706
|
+
if (!flags["no-migration"]) {
|
|
24707
|
+
generateMigration(`create_${table2}`, flags, fields, table2, false);
|
|
24708
|
+
}
|
|
24709
|
+
if (emitTest) emitModelTest(name, table2, fields);
|
|
24710
|
+
}
|
|
24711
|
+
function secureOptOut(isPublic) {
|
|
24712
|
+
return isPublic ? `export const secure = false;
|
|
24713
|
+
|
|
24714
|
+
` : "";
|
|
24715
|
+
}
|
|
24716
|
+
function generateRoute(name, flags, emitTest = true) {
|
|
24717
|
+
const routePath = name.replace(/^\//, "");
|
|
24718
|
+
const singular = routePath.endsWith("s") ? routePath.slice(0, -1) : routePath;
|
|
24719
|
+
const model = flags.model;
|
|
24720
|
+
const isPublic = Boolean(flags.public);
|
|
24721
|
+
const base = resolve10("src/routes/api", routePath);
|
|
24722
|
+
const idDir = join20(base, "[id]");
|
|
24723
|
+
ensureDir(base);
|
|
24724
|
+
ensureDir(idDir);
|
|
24725
|
+
pushRoute(`/api/${routePath}`);
|
|
24726
|
+
pushRoute(`/api/${routePath}/{id}`);
|
|
24727
|
+
if (__resolution.target === "route") {
|
|
24728
|
+
setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
|
|
24729
|
+
}
|
|
24730
|
+
const table2 = model ? toTableName(model) : "";
|
|
24731
|
+
const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
|
|
24732
|
+
` : "";
|
|
24733
|
+
const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
|
|
24734
|
+
` : "";
|
|
24735
|
+
const writeDoc = isPublic ? "Public (--public): no token required." : "Secure by default: requires a Bearer token (use --public to open).";
|
|
24736
|
+
if (model) {
|
|
24737
|
+
writeFileSafe(
|
|
24738
|
+
join20(base, "get.ts"),
|
|
24739
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24740
|
+
${modelImportBase}
|
|
24741
|
+
export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
|
|
24742
|
+
|
|
24743
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24744
|
+
// tina4:edit tune pagination defaults or add filter/sort parsing here
|
|
24745
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
24746
|
+
const limit = parseInt(req.query.limit as string) || 20;
|
|
24747
|
+
const offset = (page - 1) * limit;
|
|
24748
|
+
const rows = await ${model}.select("SELECT * FROM ${table2} LIMIT ? OFFSET ?", [limit, offset]);
|
|
24749
|
+
res.json({ data: rows.map((r) => r.toObject()), page, limit });
|
|
24750
|
+
}
|
|
24751
|
+
`
|
|
24752
|
+
);
|
|
24753
|
+
} else {
|
|
24754
|
+
writeFileSafe(
|
|
24755
|
+
join20(base, "get.ts"),
|
|
24756
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24757
|
+
|
|
24758
|
+
export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
|
|
24759
|
+
|
|
24760
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24761
|
+
${aiFill(`list_${routePath}`, {
|
|
24762
|
+
intent: `return the ${routePath} collection (add pagination if it grows)`,
|
|
24763
|
+
given: "req.query -> filters/paging",
|
|
24764
|
+
use: `Model.select("SELECT \u2026 LIMIT ? OFFSET ?", [limit, offset]) then r.toObject()`,
|
|
24765
|
+
ret: "res.json({ data: rows })",
|
|
24766
|
+
ground: `tina4_context("list ORM records with pagination", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24767
|
+
raise: `${routePath} list not implemented`
|
|
24768
|
+
})}}
|
|
24769
|
+
`
|
|
24770
|
+
);
|
|
24771
|
+
}
|
|
24772
|
+
if (model) {
|
|
24773
|
+
writeFileSafe(
|
|
24774
|
+
join20(base, "post.ts"),
|
|
24775
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24776
|
+
${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
|
|
24777
|
+
|
|
24778
|
+
// ${writeDoc}
|
|
24779
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24780
|
+
// tina4:edit validate the body before persist (Validator or hand-checks)
|
|
24781
|
+
${extend(
|
|
24782
|
+
"validate / business rules before persist",
|
|
24783
|
+
`e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`
|
|
24784
|
+
)} const item = new ${model}(req.body as Record<string, unknown>);
|
|
24785
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
24786
|
+
// write is reported to the client as a 201 carrying unsaved data.
|
|
24787
|
+
if ((await item.save()) === false) {
|
|
24788
|
+
res.json({ error: "Could not create ${singular}" }, 400);
|
|
24789
|
+
return;
|
|
24790
|
+
}
|
|
24791
|
+
res.json({ data: item.toObject() }, 201);
|
|
24792
|
+
}
|
|
24793
|
+
`
|
|
24794
|
+
);
|
|
24795
|
+
} else {
|
|
24796
|
+
writeFileSafe(
|
|
24797
|
+
join20(base, "post.ts"),
|
|
24798
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24799
|
+
|
|
24800
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
|
|
24801
|
+
|
|
24802
|
+
// ${writeDoc}
|
|
24803
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24804
|
+
// tina4:edit fill the create handler (see AI-FILL fill-spec below)
|
|
24805
|
+
${aiFill(`create_${singular}`, {
|
|
24806
|
+
intent: `validate the body and persist a new ${singular}`,
|
|
24807
|
+
given: "req.body -> the posted fields",
|
|
24808
|
+
use: "new Model(req.body).save() then item.toObject() (import your model)",
|
|
24809
|
+
ret: "res.json({ data: item }, 201)",
|
|
24810
|
+
ground: `tina4_context("create ORM record and return 201", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24811
|
+
raise: `create ${singular} not implemented`
|
|
24812
|
+
})}}
|
|
24813
|
+
`
|
|
24814
|
+
);
|
|
24815
|
+
}
|
|
24816
|
+
if (model) {
|
|
24817
|
+
writeFileSafe(
|
|
24818
|
+
join20(idDir, "get.ts"),
|
|
24819
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24820
|
+
${modelImportId}
|
|
24821
|
+
export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
|
|
24822
|
+
|
|
24823
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24824
|
+
const { id } = req.params;
|
|
24825
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
24826
|
+
if (!item) {
|
|
24827
|
+
res.json({ error: "Not found" }, 404);
|
|
24828
|
+
return;
|
|
24829
|
+
}
|
|
24830
|
+
res.json({ data: item.toObject() });
|
|
24831
|
+
}
|
|
24832
|
+
`
|
|
24833
|
+
);
|
|
24834
|
+
} else {
|
|
24835
|
+
writeFileSafe(
|
|
24836
|
+
join20(idDir, "get.ts"),
|
|
24837
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24838
|
+
|
|
24839
|
+
export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
|
|
24840
|
+
|
|
24841
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24842
|
+
${aiFill(`get_${singular}`, {
|
|
24843
|
+
intent: `fetch one ${singular} by id`,
|
|
24844
|
+
given: "req.params.id -> the record id",
|
|
24845
|
+
use: `Model.selectOne("SELECT \u2026 WHERE id = ?", [req.params.id])`,
|
|
24846
|
+
ret: "res.json({ data: item }) or res.json({ error: 'Not found' }, 404)",
|
|
24847
|
+
ground: `tina4_context("find ORM record by id", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24848
|
+
raise: `get ${singular} not implemented`
|
|
24849
|
+
})}}
|
|
24850
|
+
`
|
|
24851
|
+
);
|
|
24852
|
+
}
|
|
24853
|
+
if (model) {
|
|
24854
|
+
writeFileSafe(
|
|
24855
|
+
join20(idDir, "put.ts"),
|
|
24856
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24857
|
+
${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
|
|
24858
|
+
|
|
24859
|
+
// ${writeDoc}
|
|
24860
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24861
|
+
const { id } = req.params;
|
|
24862
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
24863
|
+
if (!item) {
|
|
24864
|
+
res.json({ error: "Not found" }, 404);
|
|
24865
|
+
return;
|
|
24866
|
+
}
|
|
24867
|
+
// tina4:edit guard which fields may be updated and who may update this row
|
|
24868
|
+
${extend(
|
|
24869
|
+
"guard which fields / who may update",
|
|
24870
|
+
`e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`
|
|
24871
|
+
)} Object.assign(item, req.body as Record<string, unknown>);
|
|
24872
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
24873
|
+
// write is reported to the client as a 200 carrying unsaved data.
|
|
24874
|
+
if ((await item.save()) === false) {
|
|
24875
|
+
res.json({ error: "Could not update ${singular}" }, 400);
|
|
24876
|
+
return;
|
|
24877
|
+
}
|
|
24878
|
+
res.json({ data: item.toObject() });
|
|
24879
|
+
}
|
|
24880
|
+
`
|
|
24881
|
+
);
|
|
24882
|
+
} else {
|
|
24883
|
+
writeFileSafe(
|
|
24884
|
+
join20(idDir, "put.ts"),
|
|
24885
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24886
|
+
|
|
24887
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
|
|
24888
|
+
|
|
24889
|
+
// ${writeDoc}
|
|
24890
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24891
|
+
// tina4:edit fill the update handler (see AI-FILL fill-spec below)
|
|
24892
|
+
${aiFill(`update_${singular}`, {
|
|
24893
|
+
intent: `load, mutate and save an existing ${singular}`,
|
|
24894
|
+
given: "req.params.id -> id; req.body -> changed fields",
|
|
24895
|
+
use: "Model.selectOne(\u2026) then Object.assign(item, req.body) then item.save()",
|
|
24896
|
+
ret: "res.json({ data: item }) or 404",
|
|
24897
|
+
ground: `tina4_context("update ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24898
|
+
raise: `update ${singular} not implemented`
|
|
24899
|
+
})}}
|
|
24900
|
+
`
|
|
24901
|
+
);
|
|
24902
|
+
}
|
|
24903
|
+
if (model) {
|
|
24904
|
+
writeFileSafe(
|
|
24905
|
+
join20(idDir, "delete.ts"),
|
|
24906
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24907
|
+
${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
|
|
24908
|
+
|
|
24909
|
+
// ${writeDoc}
|
|
24910
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24911
|
+
const { id } = req.params;
|
|
24912
|
+
const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
24913
|
+
if (!item) {
|
|
24914
|
+
res.json({ error: "Not found" }, 404);
|
|
24915
|
+
return;
|
|
24916
|
+
}
|
|
24917
|
+
await item.delete();
|
|
24918
|
+
res.json({ message: "deleted", id });
|
|
24919
|
+
}
|
|
24920
|
+
`
|
|
24921
|
+
);
|
|
24922
|
+
} else {
|
|
24923
|
+
writeFileSafe(
|
|
24924
|
+
join20(idDir, "delete.ts"),
|
|
24925
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
24926
|
+
|
|
24927
|
+
${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
|
|
24928
|
+
|
|
24929
|
+
// ${writeDoc}
|
|
24930
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
24931
|
+
${aiFill(`delete_${singular}`, {
|
|
24932
|
+
intent: `delete a ${singular} by id`,
|
|
24933
|
+
given: "req.params.id -> id",
|
|
24934
|
+
use: "Model.selectOne(\u2026) then item.delete()",
|
|
24935
|
+
ret: "res.json({ message: 'deleted', id }) or 404",
|
|
24936
|
+
ground: `tina4_context("delete ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
24937
|
+
raise: `delete ${singular} not implemented`
|
|
24938
|
+
})}}
|
|
24939
|
+
`
|
|
24940
|
+
);
|
|
24941
|
+
}
|
|
24942
|
+
if (emitTest) {
|
|
24943
|
+
if (model) {
|
|
24944
|
+
generateTest(routePath, { model, "secure-writes": true, public: isPublic });
|
|
24945
|
+
} else {
|
|
24946
|
+
emitRouteStubTest(routePath);
|
|
24947
|
+
}
|
|
24948
|
+
}
|
|
24949
|
+
}
|
|
24950
|
+
function generateCrud(name, flags) {
|
|
24951
|
+
const table2 = toTableName(name);
|
|
24952
|
+
const routeName = toPlural(table2);
|
|
24953
|
+
const isPublic = Boolean(flags.public);
|
|
24954
|
+
if (!__resolution.jsonMode) console.log(`
|
|
24955
|
+
Generating CRUD for ${name}...
|
|
24956
|
+
`);
|
|
24957
|
+
generateModel(name, flags, false);
|
|
24958
|
+
generateRoute(routeName, { ...flags, model: name }, false);
|
|
24959
|
+
generateForm(name, flags);
|
|
24960
|
+
generateView(name, flags);
|
|
24961
|
+
generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
|
|
24962
|
+
if (!__resolution.jsonMode) {
|
|
24963
|
+
console.log(`
|
|
24964
|
+
CRUD generation complete for ${name}.`);
|
|
24965
|
+
console.log(" Run: tina4nodejs migrate");
|
|
24966
|
+
console.log(" Visit: /swagger to see the API docs");
|
|
24967
|
+
}
|
|
24968
|
+
}
|
|
24969
|
+
function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
|
|
24970
|
+
const ts = timestamp();
|
|
24971
|
+
const dir = resolve10("migrations");
|
|
24972
|
+
ensureDir(dir);
|
|
24973
|
+
let table2;
|
|
24974
|
+
if (tableOverride) {
|
|
24975
|
+
table2 = tableOverride;
|
|
24976
|
+
} else {
|
|
24977
|
+
const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
|
|
24978
|
+
table2 = toTableName(raw);
|
|
24979
|
+
}
|
|
24980
|
+
if (__resolution.target === "migration") {
|
|
24981
|
+
setResolutionField("table_name", table2);
|
|
24982
|
+
}
|
|
24983
|
+
const fields = fieldsOverride || parseFields(flags.fields || "");
|
|
24984
|
+
const isCreate = name.startsWith("create_") || fieldsOverride !== void 0;
|
|
24985
|
+
const fileName = `${ts}_${name}.sql`;
|
|
24986
|
+
const path8 = join20(dir, fileName);
|
|
24987
|
+
setResolutionField("migration_path", `migrations/${fileName}`);
|
|
24988
|
+
if (__resolution.target === "migration") {
|
|
24989
|
+
setResolutionField("file_path", `migrations/${fileName}`);
|
|
24990
|
+
pushTestPath(`tests/${table2}_migration.test.ts`);
|
|
24991
|
+
}
|
|
24992
|
+
let upSql;
|
|
24993
|
+
let downSql;
|
|
24994
|
+
if (isCreate) {
|
|
24995
|
+
const colLines = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
|
|
24996
|
+
for (const [fname, ftype] of fields) {
|
|
24997
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
24998
|
+
const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
|
|
24999
|
+
colLines.push(` ${fname} ${info.sql}${defaultClause}`);
|
|
25000
|
+
}
|
|
25001
|
+
colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
|
|
25002
|
+
upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
|
|
25003
|
+
-- tina4:edit add columns beyond id + created_at
|
|
25004
|
+
${colLines.join(",\n")}
|
|
25005
|
+
);`;
|
|
25006
|
+
downSql = `-- tina4:edit mirror the CREATE's added columns in the rollback
|
|
25007
|
+
DROP TABLE IF EXISTS ${table2};`;
|
|
25008
|
+
} else {
|
|
25009
|
+
upSql = `-- tina4:edit write your UP migration SQL here
|
|
25010
|
+
-- Example: ALTER TABLE ${table2} ADD COLUMN new_col TEXT DEFAULT '';`;
|
|
25011
|
+
downSql = `-- tina4:edit write your DOWN rollback SQL here
|
|
25012
|
+
-- Example: ALTER TABLE ${table2} DROP COLUMN new_col;`;
|
|
25013
|
+
}
|
|
25014
|
+
const now = isoNow();
|
|
25015
|
+
const content = `-- Migration: ${name}
|
|
25016
|
+
-- Created: ${now}
|
|
25017
|
+
|
|
25018
|
+
-- UP
|
|
25019
|
+
${upSql}
|
|
25020
|
+
|
|
25021
|
+
-- DOWN
|
|
25022
|
+
${downSql}
|
|
25023
|
+
`;
|
|
25024
|
+
writeFileSafe(path8, content);
|
|
25025
|
+
const downPath = join20(dir, `${ts}_${name}.down.sql`);
|
|
25026
|
+
const downContent = `-- Rollback: ${name}
|
|
25027
|
+
-- Created: ${now}
|
|
25028
|
+
|
|
25029
|
+
${downSql}
|
|
25030
|
+
`;
|
|
25031
|
+
writeFileSafe(downPath, downContent);
|
|
25032
|
+
if (emitTest && isCreate) emitMigrationTest(name, table2);
|
|
25033
|
+
}
|
|
25034
|
+
function generateMiddleware(name, _flags) {
|
|
25035
|
+
const snake = toSnake(name);
|
|
25036
|
+
const dir = resolve10("src/middleware");
|
|
25037
|
+
ensureDir(dir);
|
|
25038
|
+
const path8 = join20(dir, `${snake}.ts`);
|
|
25039
|
+
if (__resolution.target === "middleware") {
|
|
25040
|
+
setResolutionField("class_name", name);
|
|
25041
|
+
setResolutionField("file_path", `src/middleware/${snake}.ts`);
|
|
25042
|
+
pushTestPath(`tests/${snake}.test.ts`);
|
|
25043
|
+
}
|
|
25044
|
+
const content = `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25045
|
+
|
|
25046
|
+
/**
|
|
25047
|
+
* ${name} middleware \u2014 runs before and after route handlers.
|
|
25048
|
+
*
|
|
25049
|
+
* Usage:
|
|
25050
|
+
* import { before${name}, after${name} } from "../middleware/${snake}.js";
|
|
25051
|
+
*/
|
|
25052
|
+
|
|
25053
|
+
export async function before${name}(
|
|
25054
|
+
req: Tina4Request,
|
|
25055
|
+
res: Tina4Response,
|
|
25056
|
+
next: () => Promise<void>,
|
|
25057
|
+
): Promise<void> {
|
|
25058
|
+
// tina4:edit replace the Authorization check with the real pre-request rule
|
|
25059
|
+
const auth = req.headers["authorization"];
|
|
25060
|
+
if (!auth) {
|
|
25061
|
+
res.json({ error: "Unauthorized" }, 401);
|
|
25062
|
+
return;
|
|
25063
|
+
}
|
|
25064
|
+
await next();
|
|
25065
|
+
}
|
|
25066
|
+
|
|
25067
|
+
export async function after${name}(
|
|
25068
|
+
req: Tina4Request,
|
|
25069
|
+
res: Tina4Response,
|
|
25070
|
+
next: () => Promise<void>,
|
|
25071
|
+
): Promise<void> {
|
|
25072
|
+
// tina4:edit add post-processing (logging, header injection, telemetry)
|
|
25073
|
+
await next();
|
|
25074
|
+
}
|
|
25075
|
+
`;
|
|
25076
|
+
writeFileSafe(path8, content);
|
|
25077
|
+
emitMiddlewareTest(name, snake);
|
|
25078
|
+
}
|
|
25079
|
+
function generateTest(name, flags) {
|
|
25080
|
+
const snake = toSnake(name);
|
|
25081
|
+
const singular = snake.endsWith("s") ? snake.slice(0, -1) : snake;
|
|
25082
|
+
const model = flags.model;
|
|
25083
|
+
const dir = resolve10("tests");
|
|
25084
|
+
ensureDir(dir);
|
|
25085
|
+
const path8 = join20(dir, `${snake}.test.ts`);
|
|
25086
|
+
if (model && flags["secure-writes"]) {
|
|
25087
|
+
const isPublic = Boolean(flags.public);
|
|
25088
|
+
const posture = isPublic ? "open (--public)" : "gated";
|
|
25089
|
+
const writeCase = isPublic ? ` // --public opened the write: an anonymous POST creates -> 201.
|
|
25090
|
+
assert("anonymous POST is public -> 201",
|
|
25091
|
+
(await client.post("/api/${snake}", { json: { name: "test" } })).status === 201);` : ` // Secure by default: a tokenless POST is rejected with 401.
|
|
25092
|
+
assert("anonymous POST is gated -> 401",
|
|
25093
|
+
(await client.post("/api/${snake}", { json: { name: "test" } })).status === 401);
|
|
25094
|
+
// A valid Bearer token passes the gate and creates -> 201.
|
|
25095
|
+
const token = getToken({ userId: 1 });
|
|
25096
|
+
assert("authenticated POST creates -> 201",
|
|
25097
|
+
(await client.post("/api/${snake}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status === 201);`;
|
|
25098
|
+
const content2 = `/**
|
|
25099
|
+
* ${name} CRUD \u2014 reads public, writes ${posture} (secure by default).
|
|
25100
|
+
*
|
|
25101
|
+
* Real end-to-end via TestClient: no mocks \u2014 real Router, real auth gate, real
|
|
25102
|
+
* JWT, real SQLite DB + table. Run with: npx tsx tests/${snake}.test.ts
|
|
25103
|
+
*/
|
|
25104
|
+
import { dirname, resolve } from "node:path";
|
|
25105
|
+
import { fileURLToPath } from "node:url";
|
|
25106
|
+
import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
|
|
25107
|
+
import { initDatabase } from "tina4-nodejs/orm";
|
|
25108
|
+
import ${model} from "../src/models/${model}.js";
|
|
25109
|
+
|
|
25110
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
25111
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
25112
|
+
|
|
25113
|
+
let pass = 0;
|
|
25114
|
+
let fail = 0;
|
|
25115
|
+
function assert(label: string, ok: boolean): void {
|
|
25116
|
+
if (ok) { pass++; console.log(\` PASS \${label}\`); }
|
|
25117
|
+
else { fail++; console.log(\` FAIL \${label}\`); }
|
|
25118
|
+
}
|
|
25119
|
+
|
|
25120
|
+
await initDatabase({ url: "sqlite:///data/test_${snake}.db" });
|
|
25121
|
+
await ${model}.createTable();
|
|
25122
|
+
|
|
25123
|
+
const router = new Router();
|
|
25124
|
+
for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
|
|
25125
|
+
const client = new TestClient(router);
|
|
25126
|
+
|
|
25127
|
+
// Reads are public.
|
|
25128
|
+
assert("GET list is public -> 200", (await client.get("/api/${snake}")).status === 200);
|
|
25129
|
+
${writeCase}
|
|
25130
|
+
|
|
25131
|
+
console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
|
|
25132
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
25133
|
+
`;
|
|
25134
|
+
writeFileSafe(path8, content2);
|
|
25135
|
+
return;
|
|
25136
|
+
}
|
|
25137
|
+
let content;
|
|
25138
|
+
if (model) {
|
|
25139
|
+
content = `import { tests, assertTrue } from "tina4-nodejs";
|
|
25140
|
+
|
|
25141
|
+
/**
|
|
25142
|
+
* Tests for ${name} CRUD operations.
|
|
25143
|
+
*/
|
|
25144
|
+
|
|
25145
|
+
const list${model}s = tests(
|
|
25146
|
+
assertTrue([]),
|
|
25147
|
+
)(function list${model}s() {
|
|
25148
|
+
// tina4:edit assert against a real GET /api/${toSnake(name)} response (rows, count)
|
|
25149
|
+
return true;
|
|
25150
|
+
});
|
|
25151
|
+
|
|
25152
|
+
const get${model} = tests(
|
|
25153
|
+
assertTrue([]),
|
|
25154
|
+
)(function get${model}() {
|
|
25155
|
+
// tina4:edit assert against GET /api/${toSnake(name)}/{id} for one seeded row
|
|
25156
|
+
return true;
|
|
25157
|
+
});
|
|
25158
|
+
|
|
25159
|
+
const create${model} = tests(
|
|
25160
|
+
assertTrue([]),
|
|
25161
|
+
)(function create${model}() {
|
|
25162
|
+
// tina4:edit POST a valid + an invalid body, assert 201 vs 400
|
|
25163
|
+
return true;
|
|
25164
|
+
});
|
|
25165
|
+
|
|
25166
|
+
const update${model} = tests(
|
|
25167
|
+
assertTrue([]),
|
|
25168
|
+
)(function update${model}() {
|
|
25169
|
+
// tina4:edit PUT changed fields, assert the row was persisted
|
|
25170
|
+
return true;
|
|
25171
|
+
});
|
|
25172
|
+
|
|
25173
|
+
const delete${model} = tests(
|
|
25174
|
+
assertTrue([]),
|
|
25175
|
+
)(function delete${model}() {
|
|
25176
|
+
// tina4:edit DELETE the id, assert 200 then GET returns 404
|
|
25177
|
+
return true;
|
|
25178
|
+
});
|
|
25179
|
+
|
|
25180
|
+
void [list${model}s, get${model}, create${model}, update${model}, delete${model}];
|
|
25181
|
+
`;
|
|
25182
|
+
} else {
|
|
25183
|
+
const titleName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
25184
|
+
content = `import { tests, assertTrue } from "tina4-nodejs";
|
|
25185
|
+
|
|
25186
|
+
/**
|
|
25187
|
+
* Tests for ${name}.
|
|
25188
|
+
*/
|
|
25189
|
+
|
|
25190
|
+
const test${titleName} = tests(
|
|
25191
|
+
assertTrue([]),
|
|
25192
|
+
)(function test${titleName}() {
|
|
25193
|
+
// tina4:edit assert against the real behaviour under test (no mocks)
|
|
25194
|
+
return true;
|
|
25195
|
+
});
|
|
25196
|
+
|
|
25197
|
+
void test${titleName};
|
|
25198
|
+
`;
|
|
25199
|
+
}
|
|
25200
|
+
writeFileSafe(path8, content);
|
|
25201
|
+
}
|
|
25202
|
+
function generateForm(name, flags) {
|
|
25203
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
25204
|
+
const table2 = toTableName(name);
|
|
25205
|
+
const routeName = toPlural(table2);
|
|
25206
|
+
const inputTypes = {
|
|
25207
|
+
string: "text",
|
|
25208
|
+
str: "text",
|
|
25209
|
+
text: "textarea",
|
|
25210
|
+
int: "number",
|
|
25211
|
+
integer: "number",
|
|
25212
|
+
float: "number",
|
|
25213
|
+
numeric: "number",
|
|
25214
|
+
decimal: "number",
|
|
25215
|
+
bool: "checkbox",
|
|
25216
|
+
boolean: "checkbox",
|
|
25217
|
+
datetime: "datetime-local",
|
|
25218
|
+
blob: "file"
|
|
25219
|
+
};
|
|
25220
|
+
const dir = resolve10("src/templates/forms");
|
|
25221
|
+
ensureDir(dir);
|
|
25222
|
+
const path8 = join20(dir, `${table2}.twig`);
|
|
25223
|
+
let fieldHtml = "";
|
|
25224
|
+
for (const [fname, ftype] of fields) {
|
|
25225
|
+
const itype = inputTypes[ftype] || "text";
|
|
25226
|
+
const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
25227
|
+
const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
|
|
25228
|
+
if (itype === "textarea") {
|
|
25229
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
25230
|
+
<label for="${fname}">${label}</label>
|
|
25231
|
+
<textarea id="${fname}" name="${fname}" class="form-control" rows="4" placeholder="${label}">{{ item.${fname} }}</textarea>
|
|
25232
|
+
</div>
|
|
25233
|
+
`;
|
|
25234
|
+
} else if (itype === "checkbox") {
|
|
25235
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
25236
|
+
<label>
|
|
25237
|
+
<input type="checkbox" id="${fname}" name="${fname}" value="1" {% if item.${fname} %}checked{% endif %}>
|
|
25238
|
+
${label}
|
|
25239
|
+
</label>
|
|
25240
|
+
</div>
|
|
25241
|
+
`;
|
|
25242
|
+
} else {
|
|
25243
|
+
fieldHtml += ` <div class="form-group mb-3">
|
|
25244
|
+
<label for="${fname}">${label}</label>
|
|
25245
|
+
<input type="${itype}" id="${fname}" name="${fname}" class="form-control"${step} value="{{ item.${fname} }}" placeholder="${label}">
|
|
25246
|
+
</div>
|
|
25247
|
+
`;
|
|
25248
|
+
}
|
|
25249
|
+
}
|
|
25250
|
+
const content = `{% extends "base.twig" %}
|
|
25251
|
+
{% block title %}${name} {% if item.id %}Edit{% else %}Create{% endif %}{% endblock %}
|
|
25252
|
+
{% block content %}
|
|
25253
|
+
<div class="container mt-4">
|
|
25254
|
+
<h1>{% if item.id %}Edit ${name}{% else %}Create ${name}{% endif %}</h1>
|
|
25255
|
+
{# tina4:edit restyle the form beyond the scaffolded defaults #}
|
|
25256
|
+
<form method="post" action="/api/${routeName}{% if item.id %}/{{ item.id }}{% endif %}">
|
|
25257
|
+
{{ form_token() }}
|
|
25258
|
+
` + fieldHtml + ` <button type="submit" class="btn btn-primary">
|
|
25259
|
+
{% if item.id %}Update{% else %}Create{% endif %}
|
|
25260
|
+
</button>
|
|
25261
|
+
<a href="/api/${routeName}" class="btn btn-secondary">Cancel</a>
|
|
25262
|
+
</form>
|
|
25263
|
+
</div>
|
|
25264
|
+
{% endblock %}
|
|
25265
|
+
`;
|
|
25266
|
+
writeFileSafe(path8, content);
|
|
25267
|
+
}
|
|
25268
|
+
function generateView(name, flags) {
|
|
25269
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
25270
|
+
const table2 = toTableName(name);
|
|
25271
|
+
const routeName = toPlural(table2);
|
|
25272
|
+
const cols = fields.map(([f]) => f);
|
|
25273
|
+
const dir = resolve10("src/templates/pages");
|
|
25274
|
+
ensureDir(dir);
|
|
25275
|
+
const listPath = join20(dir, `${routeName}.twig`);
|
|
25276
|
+
const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
|
|
25277
|
+
const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
|
|
25278
|
+
const listContent = `{% extends "base.twig" %}
|
|
25279
|
+
{% block title %}${name}s{% endblock %}
|
|
25280
|
+
{% block content %}
|
|
25281
|
+
<div class="container mt-4">
|
|
25282
|
+
{# tina4:edit add sort / filter / pagination controls to the list #}
|
|
25283
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
25284
|
+
<h1>${name}s</h1>
|
|
25285
|
+
<a href="/${routeName}/create" class="btn btn-primary">Add ${name}</a>
|
|
25286
|
+
</div>
|
|
25287
|
+
<table class="table">
|
|
25288
|
+
<thead>
|
|
25289
|
+
<tr>
|
|
25290
|
+
<th>ID</th>
|
|
25291
|
+
${th}
|
|
25292
|
+
<th>Actions</th>
|
|
25293
|
+
</tr>
|
|
25294
|
+
</thead>
|
|
25295
|
+
<tbody>
|
|
25296
|
+
{% for item in items %}
|
|
25297
|
+
<tr>
|
|
25298
|
+
<td>{{ item.id }}</td>
|
|
25299
|
+
${td}
|
|
25300
|
+
<td>
|
|
25301
|
+
<a href="/${routeName}/{{ item.id }}" class="btn btn-sm btn-primary">View</a>
|
|
25302
|
+
<a href="/${routeName}/{{ item.id }}/edit" class="btn btn-sm btn-secondary">Edit</a>
|
|
25303
|
+
</td>
|
|
25304
|
+
</tr>
|
|
25305
|
+
{% endfor %}
|
|
25306
|
+
</tbody>
|
|
25307
|
+
</table>
|
|
25308
|
+
</div>
|
|
25309
|
+
{% endblock %}
|
|
25310
|
+
`;
|
|
25311
|
+
writeFileSafe(listPath, listContent);
|
|
25312
|
+
const detailPath = join20(dir, `${table2}.twig`);
|
|
25313
|
+
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");
|
|
25314
|
+
const detailContent = `{% extends "base.twig" %}
|
|
25315
|
+
{% block title %}${name} Detail{% endblock %}
|
|
25316
|
+
{% block content %}
|
|
25317
|
+
<div class="container mt-4">
|
|
25318
|
+
{# tina4:edit extend the detail view with related records or actions #}
|
|
25319
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
25320
|
+
<h1>${name} #{{ item.id }}</h1>
|
|
25321
|
+
<div>
|
|
25322
|
+
<a href="/${routeName}/{{ item.id }}/edit" class="btn btn-secondary">Edit</a>
|
|
25323
|
+
<a href="/${routeName}" class="btn btn-outline-secondary">Back</a>
|
|
25324
|
+
</div>
|
|
25325
|
+
</div>
|
|
25326
|
+
${detailFields}
|
|
25327
|
+
</div>
|
|
25328
|
+
{% endblock %}
|
|
25329
|
+
`;
|
|
25330
|
+
writeFileSafe(detailPath, detailContent);
|
|
25331
|
+
}
|
|
25332
|
+
function generateAuth(_flags) {
|
|
25333
|
+
if (!__resolution.jsonMode) console.log("\n Generating authentication scaffolding...\n");
|
|
25334
|
+
generateModel("User", { fields: "email:string,password:string,role:string" }, false);
|
|
25335
|
+
const registerDir = resolve10("src/routes/api/auth/register");
|
|
25336
|
+
const loginDir = resolve10("src/routes/api/auth/login");
|
|
25337
|
+
const meDir = resolve10("src/routes/api/auth/me");
|
|
25338
|
+
ensureDir(registerDir);
|
|
25339
|
+
ensureDir(loginDir);
|
|
25340
|
+
ensureDir(meDir);
|
|
25341
|
+
writeFileSafe(
|
|
25342
|
+
join20(registerDir, "post.ts"),
|
|
25343
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25344
|
+
import { hashPassword } from "tina4-nodejs";
|
|
25345
|
+
import User from "../../../../models/User.js";
|
|
25346
|
+
|
|
25347
|
+
// Public: registration mints an account for a user who has no token yet.
|
|
25348
|
+
export const secure = false;
|
|
25349
|
+
|
|
25350
|
+
export const meta = { summary: "Register a new user", tags: ["auth"] };
|
|
25351
|
+
|
|
25352
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
25353
|
+
// tina4:edit add password-strength / email-format / captcha rules before mint
|
|
25354
|
+
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
25355
|
+
|
|
25356
|
+
if (!email || !password) {
|
|
25357
|
+
res.json({ error: "Email and password required" }, 400);
|
|
25358
|
+
return;
|
|
25359
|
+
}
|
|
25360
|
+
|
|
25361
|
+
const existing = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
25362
|
+
if (existing) {
|
|
25363
|
+
res.json({ error: "Email already registered" }, 409);
|
|
25364
|
+
return;
|
|
25365
|
+
}
|
|
25366
|
+
|
|
25367
|
+
const user = new User({ email, password: hashPassword(password), role: "user" });
|
|
25368
|
+
await user.save();
|
|
25369
|
+
res.json({ message: "Registered", id: user.toObject().id }, 201);
|
|
25370
|
+
}
|
|
25371
|
+
`
|
|
25372
|
+
);
|
|
25373
|
+
writeFileSafe(
|
|
25374
|
+
join20(loginDir, "post.ts"),
|
|
25375
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25376
|
+
import { checkPassword, getToken } from "tina4-nodejs";
|
|
25377
|
+
import User from "../../../../models/User.js";
|
|
25378
|
+
|
|
25379
|
+
// Public: login authenticates by password and mints the token.
|
|
25380
|
+
export const secure = false;
|
|
25381
|
+
|
|
25382
|
+
export const meta = { summary: "Login and receive JWT token", tags: ["auth"] };
|
|
25383
|
+
|
|
25384
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
25385
|
+
// tina4:edit add rate-limit / lock-after-N-failures / 2FA before password check
|
|
25386
|
+
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
25387
|
+
|
|
25388
|
+
if (!email || !password) {
|
|
25389
|
+
res.json({ error: "Email and password required" }, 400);
|
|
25390
|
+
return;
|
|
25391
|
+
}
|
|
25392
|
+
|
|
25393
|
+
const user = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
|
|
25394
|
+
if (!user || !checkPassword(password, user.toObject().password as string)) {
|
|
25395
|
+
res.json({ error: "Invalid credentials" }, 401);
|
|
25396
|
+
return;
|
|
25397
|
+
}
|
|
25398
|
+
|
|
25399
|
+
const data = user.toObject();
|
|
25400
|
+
// tina4:edit set token TTL (getToken(payload, secret, expiresInMinutes)) and add scopes if needed
|
|
25401
|
+
const token = getToken({ userId: data.id, email: data.email, role: data.role });
|
|
25402
|
+
res.json({ token });
|
|
25403
|
+
}
|
|
25404
|
+
`
|
|
25405
|
+
);
|
|
25406
|
+
writeFileSafe(
|
|
25407
|
+
join20(meDir, "get.ts"),
|
|
25408
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25409
|
+
import { authenticateRequest } from "tina4-nodejs";
|
|
25410
|
+
|
|
25411
|
+
export const meta = { summary: "Get current authenticated user", tags: ["auth"] };
|
|
25412
|
+
|
|
25413
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
25414
|
+
const payload = authenticateRequest(req.headers as Record<string, string | string[] | undefined>);
|
|
25415
|
+
if (!payload) {
|
|
25416
|
+
res.json({ error: "Unauthorized" }, 401);
|
|
25417
|
+
return;
|
|
25418
|
+
}
|
|
25419
|
+
res.json({ user: payload });
|
|
25420
|
+
}
|
|
25421
|
+
`
|
|
25422
|
+
);
|
|
25423
|
+
const formsDir = resolve10("src/templates/forms");
|
|
25424
|
+
ensureDir(formsDir);
|
|
25425
|
+
writeFileSafe(
|
|
25426
|
+
join20(formsDir, "login.twig"),
|
|
25427
|
+
`{% extends "base.twig" %}
|
|
25428
|
+
{% block title %}Login{% endblock %}
|
|
25429
|
+
{% block content %}
|
|
25430
|
+
<div class="container mt-4" style="max-width:400px">
|
|
25431
|
+
<h1>Login</h1>
|
|
25432
|
+
<form method="post" action="/api/auth/login">
|
|
25433
|
+
{{ form_token() }}
|
|
25434
|
+
<div class="form-group mb-3">
|
|
25435
|
+
<label for="email">Email</label>
|
|
25436
|
+
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
|
25437
|
+
</div>
|
|
25438
|
+
<div class="form-group mb-3">
|
|
25439
|
+
<label for="password">Password</label>
|
|
25440
|
+
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
|
|
25441
|
+
</div>
|
|
25442
|
+
<button type="submit" class="btn btn-primary w-100">Login</button>
|
|
25443
|
+
<p class="mt-3 text-center"><a href="/register">Create an account</a></p>
|
|
25444
|
+
</form>
|
|
25445
|
+
</div>
|
|
25446
|
+
{% endblock %}
|
|
25447
|
+
`
|
|
25448
|
+
);
|
|
25449
|
+
writeFileSafe(
|
|
25450
|
+
join20(formsDir, "register.twig"),
|
|
25451
|
+
`{% extends "base.twig" %}
|
|
25452
|
+
{% block title %}Register{% endblock %}
|
|
25453
|
+
{% block content %}
|
|
25454
|
+
<div class="container mt-4" style="max-width:400px">
|
|
25455
|
+
<h1>Register</h1>
|
|
25456
|
+
<form method="post" action="/api/auth/register">
|
|
25457
|
+
{{ form_token() }}
|
|
25458
|
+
<div class="form-group mb-3">
|
|
25459
|
+
<label for="email">Email</label>
|
|
25460
|
+
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
|
25461
|
+
</div>
|
|
25462
|
+
<div class="form-group mb-3">
|
|
25463
|
+
<label for="password">Password</label>
|
|
25464
|
+
<input type="password" id="password" name="password" class="form-control" placeholder="Password" minlength="8" required>
|
|
25465
|
+
</div>
|
|
25466
|
+
<button type="submit" class="btn btn-primary w-100">Register</button>
|
|
25467
|
+
<p class="mt-3 text-center"><a href="/login">Already have an account?</a></p>
|
|
25468
|
+
</form>
|
|
25469
|
+
</div>
|
|
25470
|
+
{% endblock %}
|
|
25471
|
+
`
|
|
25472
|
+
);
|
|
25473
|
+
emitAuthTest();
|
|
25474
|
+
if (!__resolution.jsonMode) {
|
|
25475
|
+
console.log("\n Authentication scaffolding complete.");
|
|
25476
|
+
console.log(" Run: tina4nodejs migrate");
|
|
25477
|
+
console.log(" POST /api/auth/register \u2014 create account (public)");
|
|
25478
|
+
console.log(" POST /api/auth/login \u2014 get JWT token (public)");
|
|
25479
|
+
console.log(" GET /api/auth/me \u2014 get profile (requires token)");
|
|
25480
|
+
}
|
|
25481
|
+
}
|
|
25482
|
+
function generateService(name, flags) {
|
|
25483
|
+
const snake = toSnake(name);
|
|
25484
|
+
const camel = toCamel(toPascal(name)) || snake;
|
|
25485
|
+
const cron = flags.cron;
|
|
25486
|
+
const dir = resolve10("src/services");
|
|
25487
|
+
ensureDir(dir);
|
|
25488
|
+
const path8 = join20(dir, `${snake}.ts`);
|
|
25489
|
+
let scheduleField;
|
|
25490
|
+
let note;
|
|
25491
|
+
if (cron && cron !== true) {
|
|
25492
|
+
scheduleField = ` timing: ${JSON.stringify(String(cron))},`;
|
|
25493
|
+
note = `cron '${cron}'`;
|
|
25494
|
+
} else {
|
|
25495
|
+
const seconds = parseEvery(flags.every);
|
|
25496
|
+
scheduleField = ` interval: ${seconds},`;
|
|
25497
|
+
note = `every ${seconds}s`;
|
|
25498
|
+
}
|
|
25499
|
+
const body = aiFill(`${camel}Task`, {
|
|
25500
|
+
intent: "do the scheduled work for this service",
|
|
25501
|
+
given: "context -> ServiceContext (.name, .running, .lastRun)",
|
|
25502
|
+
use: "your ORM / Api / Messenger code (re-run on schedule)",
|
|
25503
|
+
ground: `tina4_context("background service scheduled task", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
25504
|
+
raise: `service ${snake} not implemented`
|
|
25505
|
+
});
|
|
25506
|
+
const content = `import type { ServiceContext } from "tina4-nodejs";
|
|
25507
|
+
|
|
25508
|
+
/**
|
|
25509
|
+
* ${name} background service \u2014 runs ${note} via ServiceRunner.
|
|
25510
|
+
*
|
|
25511
|
+
* Wire a runner once (e.g. in app.ts) to actually run it \u2014 \`tina4nodejs serve\`
|
|
25512
|
+
* does NOT auto-start services:
|
|
25513
|
+
*
|
|
25514
|
+
* import { ServiceRunner } from "tina4-nodejs";
|
|
25515
|
+
* await ServiceRunner.discover("src/services"); // registers this default export
|
|
25516
|
+
* ServiceRunner.start();
|
|
25517
|
+
*/
|
|
25518
|
+
|
|
25519
|
+
export async function ${camel}Task(context: ServiceContext): Promise<void> {
|
|
25520
|
+
// tina4:edit replace the AI-FILL stub below with the scheduled work
|
|
25521
|
+
${body}}
|
|
25522
|
+
|
|
25523
|
+
// Discovered by ServiceRunner.discover("src/services") \u2014 it reads name/handler
|
|
25524
|
+
// (+ interval or timing) off this default export.
|
|
25525
|
+
export default {
|
|
25526
|
+
name: "${snake}",
|
|
25527
|
+
handler: ${camel}Task,
|
|
25528
|
+
${scheduleField}
|
|
25529
|
+
};
|
|
25530
|
+
`;
|
|
25531
|
+
writeFileSafe(path8, content);
|
|
25532
|
+
emitServiceTest(name, snake, camel);
|
|
25533
|
+
}
|
|
25534
|
+
function generateQueue(name, _flags) {
|
|
25535
|
+
const topic = name.replace(/^\//, "");
|
|
25536
|
+
const slug = toSnake(topic.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
|
|
25537
|
+
const pascal = toPascal(topic) || "Topic";
|
|
25538
|
+
const dir = resolve10("src/services");
|
|
25539
|
+
ensureDir(dir);
|
|
25540
|
+
const path8 = join20(dir, `${slug}_consumer.ts`);
|
|
25541
|
+
const body = aiFill(`handle${pascal}`, {
|
|
25542
|
+
intent: `process ONE ${topic} job payload`,
|
|
25543
|
+
given: "payload -> the produced job data (job.payload)",
|
|
25544
|
+
use: "your ORM / Messenger code; return to ack (job.complete), throw to nack (job.fail)",
|
|
25545
|
+
ground: `tina4_context("process a queue job", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
25546
|
+
raise: `queue ${topic} handler not implemented`
|
|
25547
|
+
});
|
|
25548
|
+
const content = `import { Queue } from "tina4-nodejs";
|
|
25549
|
+
import type { ServiceContext } from "tina4-nodejs";
|
|
25550
|
+
|
|
25551
|
+
/**
|
|
25552
|
+
* ${topic} queue \u2014 producer + consumer worker.
|
|
25553
|
+
*
|
|
25554
|
+
* Produce from anywhere: publish${pascal}({ ... })
|
|
25555
|
+
* The consumer is a long-running worker wired as a ServiceRunner daemon:
|
|
25556
|
+
* await ServiceRunner.discover("src/services"); ServiceRunner.start();
|
|
25557
|
+
*/
|
|
25558
|
+
|
|
25559
|
+
/** Enqueue a ${topic} job for the worker below to process. Returns the job id. */
|
|
25560
|
+
export function publish${pascal}(payload: Record<string, unknown>): string {
|
|
25561
|
+
return new Queue({ topic: "${topic}" }).produce("${topic}", payload);
|
|
25562
|
+
}
|
|
25563
|
+
|
|
25564
|
+
/** Process ONE ${topic} job payload. */
|
|
25565
|
+
export async function handle${pascal}(payload: unknown): Promise<void> {
|
|
25566
|
+
// tina4:edit implement the per-job handler; return to ack, throw to nack
|
|
25567
|
+
${body}}
|
|
25568
|
+
|
|
25569
|
+
/** Long-running ${topic} worker \u2014 consume() yields jobs; ack/nack each. */
|
|
25570
|
+
export async function consume${pascal}(_context?: ServiceContext): Promise<void> {
|
|
25571
|
+
const queue = new Queue({ topic: "${topic}" });
|
|
25572
|
+
for await (const job of queue.consume("${topic}")) {
|
|
25573
|
+
const one = Array.isArray(job) ? job[0] : job;
|
|
25574
|
+
try {
|
|
25575
|
+
await handle${pascal}(one.payload);
|
|
25576
|
+
one.complete(); // ack \u2014 remove from the queue
|
|
25577
|
+
} catch (err) {
|
|
25578
|
+
one.fail(String(err)); // nack \u2014 retry / dead-letter
|
|
25579
|
+
}
|
|
25580
|
+
}
|
|
25581
|
+
}
|
|
25582
|
+
|
|
25583
|
+
// Discovered by ServiceRunner.discover("src/services"); daemon:true because
|
|
25584
|
+
// consume${pascal} owns its own loop. The topic + per-job handle keys let
|
|
25585
|
+
// \`tina4nodejs queue work ${topic}\` drive this consumer directly (own the poll
|
|
25586
|
+
// loop / bounded --once drain) without wiring a ServiceRunner.
|
|
25587
|
+
export default {
|
|
25588
|
+
name: "${topic}-consumer",
|
|
25589
|
+
topic: "${topic}",
|
|
25590
|
+
handler: consume${pascal},
|
|
25591
|
+
handle: handle${pascal},
|
|
25592
|
+
daemon: true,
|
|
25593
|
+
};
|
|
25594
|
+
`;
|
|
25595
|
+
writeFileSafe(path8, content);
|
|
25596
|
+
emitQueueTest(topic, slug, pascal);
|
|
25597
|
+
}
|
|
25598
|
+
function generateValidator(name, _flags) {
|
|
25599
|
+
const dir = resolve10("src/validators");
|
|
25600
|
+
ensureDir(dir);
|
|
25601
|
+
const path8 = join20(dir, `${toSnake(name)}.ts`);
|
|
25602
|
+
const rules = extend(
|
|
25603
|
+
"add / adjust the validation rules for this payload",
|
|
25604
|
+
`e.g. .email("email").minLength("name", 2).integer("age"); ground: tina4_context("validate request body with Validator", "nodejs")`
|
|
25605
|
+
);
|
|
25606
|
+
const content = `import { Validator } from "tina4-nodejs";
|
|
25607
|
+
|
|
25608
|
+
/**
|
|
25609
|
+
* Validate a ${name} payload. Returns a Validator (chainable rules).
|
|
25610
|
+
*
|
|
25611
|
+
* Usage in a route:
|
|
25612
|
+
* const v = validate${toPascal(name)}(req.body as Record<string, unknown>);
|
|
25613
|
+
* if (!v.isValid()) return res.json({ error: v.errors()[0]?.message }, 400);
|
|
25614
|
+
*/
|
|
25615
|
+
export function validate${toPascal(name)}(data: Record<string, unknown>): Validator {
|
|
25616
|
+
const validator = new Validator(data);
|
|
25617
|
+
// tina4:edit add rules for this payload (.email/.minLength/.integer/.inList/.pattern)
|
|
25618
|
+
${rules} validator.required("name"); // starter rule (matches the model's default field)
|
|
25619
|
+
return validator;
|
|
25620
|
+
}
|
|
25621
|
+
`;
|
|
25622
|
+
writeFileSafe(path8, content);
|
|
25623
|
+
emitValidatorTest(name, toSnake(name), toPascal(name));
|
|
25624
|
+
}
|
|
25625
|
+
function generateSeeder(name, _flags) {
|
|
25626
|
+
const table2 = toTableName(name);
|
|
25627
|
+
const dir = resolve10("src/seeds");
|
|
25628
|
+
ensureDir(dir);
|
|
25629
|
+
const path8 = join20(dir, `${table2}_seeder.ts`);
|
|
25630
|
+
const overrides = extend(
|
|
25631
|
+
"override fields that need a specific shape (seedOrm auto-fills the rest)",
|
|
25632
|
+
`e.g. return { email: (f) => f.email(), status: "active" }; ground: tina4_context("seed ORM model with FakeData", "nodejs")`
|
|
25633
|
+
);
|
|
25634
|
+
const content = `import { pathToFileURL } from "node:url";
|
|
25635
|
+
import { FakeData, seedOrm, initDatabase } from "tina4-nodejs/orm";
|
|
25636
|
+
import ${name} from "../models/${name}.js";
|
|
25637
|
+
|
|
25638
|
+
/**
|
|
25639
|
+
* Seeder for ${name} \u2014 run with: tina4nodejs seed
|
|
25640
|
+
*
|
|
25641
|
+
* seedOrm auto-fills every field by type/name; override the ones that need a
|
|
25642
|
+
* specific shape below. Each callable receives a FakeData instance.
|
|
25643
|
+
*/
|
|
25644
|
+
export function fieldOverrides(fake: FakeData): Record<string, unknown> {
|
|
25645
|
+
// tina4:edit override any fields that need a specific shape (seedOrm auto-fills the rest)
|
|
25646
|
+
${overrides} void fake; // available for overrides above
|
|
25647
|
+
return {};
|
|
25648
|
+
}
|
|
25649
|
+
|
|
25650
|
+
/** Seed rows. Invoked when this file is run directly by \`tina4nodejs seed\`. */
|
|
25651
|
+
export async function run(): Promise<void> {
|
|
25652
|
+
await initDatabase({ url: process.env.TINA4_DATABASE_URL ?? "sqlite:///data/app.db" });
|
|
25653
|
+
const summary = await seedOrm(${name} as never, 20, fieldOverrides(new FakeData()));
|
|
25654
|
+
console.log(\`Seeded \${summary.seeded} ${name} row(s), \${summary.failed} failed\`);
|
|
25655
|
+
}
|
|
25656
|
+
|
|
25657
|
+
// Only seed when executed as a script (\`tina4nodejs seed\` runs it via tsx) \u2014
|
|
25658
|
+
// importing this module (e.g. in a test) must NOT trigger seeding.
|
|
25659
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
25660
|
+
await run();
|
|
25661
|
+
}
|
|
25662
|
+
`;
|
|
25663
|
+
writeFileSafe(path8, content);
|
|
25664
|
+
emitSeederTest(name, table2);
|
|
25665
|
+
}
|
|
25666
|
+
function generateWebsocket(name, _flags) {
|
|
25667
|
+
const raw = name.trim();
|
|
25668
|
+
const wsPath = raw.startsWith("/") ? raw : "/ws/" + raw.replace(/^\/+/, "");
|
|
25669
|
+
let slug = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
|
|
25670
|
+
const base = slug.startsWith("ws_") ? slug.slice(3) : slug;
|
|
25671
|
+
const handlerName = `${toCamel(toPascal(base))}Ws`;
|
|
25672
|
+
const dir = resolve10("src/routes");
|
|
25673
|
+
ensureDir(dir);
|
|
25674
|
+
const path8 = join20(dir, `ws_${base}.ts`);
|
|
25675
|
+
const body = aiFill(handlerName, {
|
|
25676
|
+
intent: `handle an inbound "message" frame on ${wsPath}`,
|
|
25677
|
+
given: "data -> the message payload (string); connection -> WebSocketConnection",
|
|
25678
|
+
use: "connection.broadcast(data) or connection.sendJson({ ... })",
|
|
25679
|
+
ground: `tina4_context("websocket broadcast message", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
25680
|
+
raise: `websocket ${wsPath} not implemented`
|
|
25681
|
+
});
|
|
25682
|
+
const content = `import { websocket } from "tina4-nodejs";
|
|
25683
|
+
import type { WebSocketConnection } from "tina4-nodejs";
|
|
25684
|
+
|
|
25685
|
+
/**
|
|
25686
|
+
* ${wsPath} WebSocket route.
|
|
25687
|
+
*
|
|
25688
|
+
* Registered on import by websocket(). Node has NO file-based WS
|
|
25689
|
+
* auto-discovery, so IMPORT this module once from app.ts to activate it (add
|
|
25690
|
+
* \`.secure()\` to require a JWT on the upgrade):
|
|
25691
|
+
*
|
|
25692
|
+
* import "./src/routes/ws_${base}.js";
|
|
25693
|
+
*
|
|
25694
|
+
* The server invokes the handler as (connection, event, data) for each event:
|
|
25695
|
+
* "open" (connect), "message" (inbound frame), "close" (disconnect).
|
|
25696
|
+
*/
|
|
25697
|
+
export async function ${handlerName}(
|
|
25698
|
+
connection: WebSocketConnection,
|
|
25699
|
+
event: "open" | "message" | "close",
|
|
25700
|
+
data: string,
|
|
25701
|
+
): Promise<void> {
|
|
25702
|
+
if (event === "open") {
|
|
25703
|
+
// tina4:edit customize the welcome frame (or drop it)
|
|
25704
|
+
connection.sendJson({ type: "welcome" });
|
|
25705
|
+
return;
|
|
25706
|
+
}
|
|
25707
|
+
if (event === "close") {
|
|
25708
|
+
return;
|
|
25709
|
+
}
|
|
25710
|
+
// event === "message"
|
|
25711
|
+
// tina4:edit handle the inbound "message" frame (broadcast, echo, route, etc.)
|
|
25712
|
+
${body}}
|
|
25713
|
+
|
|
25714
|
+
websocket("${wsPath}", ${handlerName});
|
|
25715
|
+
`;
|
|
25716
|
+
writeFileSafe(path8, content);
|
|
25717
|
+
emitWebsocketTest(wsPath, base, handlerName);
|
|
25718
|
+
}
|
|
25719
|
+
function generateListener(name, _flags) {
|
|
25720
|
+
const event = name.trim();
|
|
25721
|
+
const slug = toSnake(event.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
|
|
25722
|
+
const handlerName = `on${toPascal(slug)}`;
|
|
25723
|
+
const dir = resolve10("src/listeners");
|
|
25724
|
+
ensureDir(dir);
|
|
25725
|
+
const path8 = join20(dir, `${slug}.ts`);
|
|
25726
|
+
const body = aiFill(handlerName, {
|
|
25727
|
+
intent: `react to the '${event}' event`,
|
|
25728
|
+
given: `args -> whatever Events.emit("${event}", ...args) passed`,
|
|
25729
|
+
use: "your app code \u2014 Messenger().send(...), an ORM write, or Events.emit(...) a follow-up",
|
|
25730
|
+
ground: `tina4_context("event listener reaction", "nodejs") \xB7 skill tina4-developer-nodejs`,
|
|
25731
|
+
raise: `listener ${event} not implemented`
|
|
25732
|
+
});
|
|
25733
|
+
const content = `import { Events } from "tina4-nodejs";
|
|
25734
|
+
|
|
25735
|
+
/**
|
|
25736
|
+
* Listener for the '${event}' event.
|
|
25737
|
+
*
|
|
25738
|
+
* Registered on import by Events.on(). Node has NO src/listeners/
|
|
25739
|
+
* auto-discovery, so IMPORT this module once from app.ts to activate it:
|
|
25740
|
+
*
|
|
25741
|
+
* import "./src/listeners/${slug}.js";
|
|
25742
|
+
*
|
|
25743
|
+
* Fires when something calls Events.emit("${event}", ...args).
|
|
25744
|
+
*/
|
|
25745
|
+
export function ${handlerName}(...args: unknown[]): void {
|
|
25746
|
+
// tina4:edit implement the reaction to '${event}' (email, ORM write, follow-up emit)
|
|
25747
|
+
${body}}
|
|
25748
|
+
|
|
25749
|
+
Events.on("${event}", ${handlerName});
|
|
25750
|
+
`;
|
|
25751
|
+
writeFileSafe(path8, content);
|
|
25752
|
+
emitListenerTest(event, slug);
|
|
25753
|
+
}
|
|
25754
|
+
function writeTest(testName, content) {
|
|
25755
|
+
const dir = resolve10("tests");
|
|
25756
|
+
ensureDir(dir);
|
|
25757
|
+
writeFileSafe(join20(dir, `${testName}.test.ts`), content);
|
|
25758
|
+
}
|
|
25759
|
+
function standaloneTest(doc, body) {
|
|
25760
|
+
return `${doc}
|
|
25761
|
+
|
|
25762
|
+
let pass = 0;
|
|
25763
|
+
let fail = 0;
|
|
25764
|
+
function assert(label: string, ok: boolean): void {
|
|
25765
|
+
if (ok) { pass++; console.log(\` PASS \${label}\`); }
|
|
25766
|
+
else { fail++; console.log(\` FAIL \${label}\`); }
|
|
25767
|
+
}
|
|
25768
|
+
async function assertThrows(label: string, fn: () => unknown | Promise<unknown>): Promise<void> {
|
|
25769
|
+
try { await fn(); assert(label, false); }
|
|
25770
|
+
catch { assert(label, true); }
|
|
25771
|
+
}
|
|
25772
|
+
|
|
25773
|
+
${body}
|
|
25774
|
+
|
|
25775
|
+
console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
|
|
25776
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
25777
|
+
`;
|
|
25778
|
+
}
|
|
25779
|
+
function sampleLiteral(fieldType) {
|
|
25780
|
+
switch ((fieldType || "string").toLowerCase()) {
|
|
25781
|
+
case "int":
|
|
25782
|
+
case "integer":
|
|
25783
|
+
return "1";
|
|
25784
|
+
case "float":
|
|
25785
|
+
case "number":
|
|
25786
|
+
case "numeric":
|
|
25787
|
+
case "decimal":
|
|
25788
|
+
return "1.5";
|
|
25789
|
+
case "bool":
|
|
25790
|
+
case "boolean":
|
|
25791
|
+
return "true";
|
|
25792
|
+
case "datetime":
|
|
25793
|
+
return '"2020-01-01 00:00:00"';
|
|
25794
|
+
case "blob":
|
|
25795
|
+
return '"x"';
|
|
25796
|
+
default:
|
|
25797
|
+
return '"sample"';
|
|
25798
|
+
}
|
|
25799
|
+
}
|
|
25800
|
+
function emitModelTest(model, table2, fields) {
|
|
25801
|
+
const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
25802
|
+
const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
|
|
25803
|
+
const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
|
|
25804
|
+
const valueAssert = stringField ? `
|
|
25805
|
+
assert("string field round-trips", fetched !== null && (fetched.toObject() as Record<string, unknown>).${stringField} === "sample");` : "";
|
|
25806
|
+
const doc = `/**
|
|
25807
|
+
* Real ORM roundtrip for ${model} \u2014 no mocks, real SQLite.
|
|
25808
|
+
*
|
|
25809
|
+
* Generated with src/models/${model}.ts by \`tina4nodejs generate model
|
|
25810
|
+
* ${model}\`. The model scaffold is working code, so this passes on generation:
|
|
25811
|
+
* binds a real in-memory SQLite DB, creates the table, saves a row, reads it
|
|
25812
|
+
* back. Run with: npx tsx tests/${table2}_model.test.ts
|
|
25813
|
+
*/
|
|
25814
|
+
import ${model} from "../src/models/${model}.js";
|
|
25815
|
+
import { initDatabase } from "tina4-nodejs/orm";`;
|
|
25816
|
+
const body = `await initDatabase({ url: "sqlite:///:memory:" });
|
|
25817
|
+
await ${model}.createTable();
|
|
25818
|
+
|
|
25819
|
+
const row = new ${model}({ ${payload} });
|
|
25820
|
+
const saved = await row.save();
|
|
25821
|
+
assert("create() persists and returns the row", saved !== false && Boolean(row.toObject().id));
|
|
25822
|
+
|
|
25823
|
+
const id = row.toObject().id;
|
|
25824
|
+
const fetched = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
|
|
25825
|
+
assert("row reads back by id", fetched !== null);
|
|
25826
|
+
assert("read-back id matches", fetched !== null && (fetched.toObject() as Record<string, unknown>).id === id);${valueAssert}
|
|
25827
|
+
|
|
25828
|
+
const missing = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [999999]);
|
|
25829
|
+
assert("find missing returns null", missing === null);`;
|
|
25830
|
+
writeTest(`${table2}_model`, standaloneTest(doc, body));
|
|
25831
|
+
}
|
|
25832
|
+
function emitRouteStubTest(route) {
|
|
25833
|
+
const doc = `/**
|
|
25834
|
+
* Routing test for ${route} \u2014 no mocks, real Router + route discovery.
|
|
25835
|
+
*
|
|
25836
|
+
* Generated with src/routes/api/${route}/ by \`tina4nodejs generate route
|
|
25837
|
+
* ${route}\` (no --model). The handlers are AI-FILL stubs that throw until you
|
|
25838
|
+
* implement them, so this tests what IS live on generation: all five routes
|
|
25839
|
+
* register on the REAL Router, and the list handler fails loud until filled.
|
|
25840
|
+
* Run with: npx tsx tests/${route}.test.ts
|
|
25841
|
+
*/
|
|
25842
|
+
import { dirname, resolve } from "node:path";
|
|
25843
|
+
import { fileURLToPath } from "node:url";
|
|
25844
|
+
import { Router, discoverRoutes } from "tina4-nodejs";
|
|
25845
|
+
import listHandler from "../src/routes/api/${route}/get.js";`;
|
|
25846
|
+
const body = `const here = dirname(fileURLToPath(import.meta.url));
|
|
25847
|
+
const router = new Router();
|
|
25848
|
+
const defs = await discoverRoutes(resolve(here, "../src/routes"));
|
|
25849
|
+
for (const def of defs) router.addRoute(def);
|
|
25850
|
+
|
|
25851
|
+
const sigs = defs.map((d) => \`\${d.method} \${d.pattern}\`);
|
|
25852
|
+
for (const sig of ["GET /api/${route}", "POST /api/${route}", "GET /api/${route}/{id}", "PUT /api/${route}/{id}", "DELETE /api/${route}/{id}"]) {
|
|
25853
|
+
assert(\`route registered: \${sig}\`, sigs.includes(sig));
|
|
25854
|
+
}
|
|
25855
|
+
|
|
25856
|
+
// The scaffolded list handler is a loud AI-FILL stub \u2014 it throws until filled.
|
|
25857
|
+
await assertThrows("list handler is a live stub (throws until filled)",
|
|
25858
|
+
() => listHandler({} as never, {} as never));`;
|
|
25859
|
+
writeTest(route, standaloneTest(doc, body));
|
|
25860
|
+
}
|
|
25861
|
+
function emitMiddlewareTest(name, snake) {
|
|
25862
|
+
const doc = `/**
|
|
25863
|
+
* Real dispatch test for the ${name} middleware \u2014 no mocks.
|
|
25864
|
+
*
|
|
25865
|
+
* Generated with src/middleware/${snake}.ts by \`tina4nodejs generate middleware
|
|
25866
|
+
* ${name}\`. Drives the scaffolded before/after functions through the REAL
|
|
25867
|
+
* MiddlewareChain with a real Tina4Request/Response (built from real node http
|
|
25868
|
+
* objects) \u2014 the same continuation dispatch the live server runs.
|
|
25869
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
25870
|
+
*/
|
|
25871
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
25872
|
+
import { Socket } from "node:net";
|
|
25873
|
+
import { MiddlewareChain, createRequest, createResponse } from "tina4-nodejs";
|
|
25874
|
+
import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
25875
|
+
import { before${name}, after${name} } from "../src/middleware/${snake}.js";`;
|
|
25876
|
+
const body = `function realPair(headers: Record<string, string>): { req: Tina4Request; res: Tina4Response; raw: ServerResponse } {
|
|
25877
|
+
const socket = new Socket();
|
|
25878
|
+
const rawReq = new IncomingMessage(socket);
|
|
25879
|
+
rawReq.method = "GET";
|
|
25880
|
+
rawReq.url = "/";
|
|
25881
|
+
rawReq.headers = { ...headers, host: "localhost" };
|
|
25882
|
+
rawReq.push(null);
|
|
25883
|
+
const rawRes = new ServerResponse(rawReq);
|
|
25884
|
+
rawRes.write = (() => true) as typeof rawRes.write;
|
|
25885
|
+
rawRes.end = (function (this: ServerResponse) { return this; }) as typeof rawRes.end;
|
|
25886
|
+
return { req: createRequest(rawReq), res: createResponse(rawRes), raw: rawRes };
|
|
25887
|
+
}
|
|
25888
|
+
|
|
25889
|
+
// before(): blocks an unauthenticated request (401, does not call next()).
|
|
25890
|
+
{
|
|
25891
|
+
const chain = new MiddlewareChain();
|
|
25892
|
+
chain.use(before${name});
|
|
25893
|
+
let reached = false;
|
|
25894
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
25895
|
+
const { req, res, raw } = realPair({});
|
|
25896
|
+
await chain.run(req, res);
|
|
25897
|
+
assert("before blocks unauthenticated (401, chain short-circuits)", raw.statusCode === 401 && reached === false);
|
|
25898
|
+
}
|
|
25899
|
+
|
|
25900
|
+
// before(): lets an authenticated request through to the next middleware.
|
|
25901
|
+
{
|
|
25902
|
+
const chain = new MiddlewareChain();
|
|
25903
|
+
chain.use(before${name});
|
|
25904
|
+
let reached = false;
|
|
25905
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
25906
|
+
const { req, res } = realPair({ authorization: "Bearer test" });
|
|
25907
|
+
await chain.run(req, res);
|
|
25908
|
+
assert("before passes an authenticated request through", reached === true);
|
|
25909
|
+
}
|
|
25910
|
+
|
|
25911
|
+
// after(): always continues the chain.
|
|
25912
|
+
{
|
|
25913
|
+
const chain = new MiddlewareChain();
|
|
25914
|
+
chain.use(after${name});
|
|
25915
|
+
let reached = false;
|
|
25916
|
+
chain.use(async (_r, _s, next) => { reached = true; next(); });
|
|
25917
|
+
const { req, res } = realPair({});
|
|
25918
|
+
await chain.run(req, res);
|
|
25919
|
+
assert("after runs and continues the chain", reached === true);
|
|
25920
|
+
}`;
|
|
25921
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
25922
|
+
}
|
|
25923
|
+
function emitServiceTest(name, snake, camel) {
|
|
25924
|
+
const doc = `/**
|
|
25925
|
+
* Real ServiceRunner test for the ${name} service \u2014 no mocks.
|
|
25926
|
+
*
|
|
25927
|
+
* Generated with src/services/${snake}.ts by \`tina4nodejs generate service
|
|
25928
|
+
* ${name}\`. Registers the scaffold on a REAL ServiceRunner and confirms the
|
|
25929
|
+
* descriptor; the task body is an AI-FILL stub that throws until filled.
|
|
25930
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
25931
|
+
*/
|
|
25932
|
+
import { ServiceRunner } from "tina4-nodejs";
|
|
25933
|
+
import service, { ${camel}Task } from "../src/services/${snake}.js";`;
|
|
25934
|
+
const body = `assert("descriptor has a name + callable handler",
|
|
25935
|
+
service.name === "${snake}" && typeof service.handler === "function");
|
|
25936
|
+
|
|
25937
|
+
// Register on a REAL ServiceRunner and confirm it is listed.
|
|
25938
|
+
ServiceRunner.register(service.name, service.handler, { interval: (service as { interval?: number }).interval });
|
|
25939
|
+
assert("registers on a real ServiceRunner", ServiceRunner.list().some((s) => s.name === "${snake}"));
|
|
25940
|
+
ServiceRunner.remove("${snake}");
|
|
25941
|
+
|
|
25942
|
+
// The scaffolded task body is an AI-FILL stub \u2014 it throws until filled.
|
|
25943
|
+
await assertThrows("task is a live stub (throws until filled)", () => ${camel}Task({} as never));`;
|
|
25944
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
25945
|
+
}
|
|
25946
|
+
function emitQueueTest(topic, slug, pascal) {
|
|
25947
|
+
const doc = `/**
|
|
25948
|
+
* Real file-backed Queue test for the ${topic} worker \u2014 no mocks.
|
|
25949
|
+
*
|
|
25950
|
+
* Generated with src/services/${slug}_consumer.ts by \`tina4nodejs generate
|
|
25951
|
+
* queue ${topic}\`. Pushes a REAL job onto the real file-backed Queue and
|
|
25952
|
+
* asserts it is enqueued, and that the consumer is wired as a daemon. The
|
|
25953
|
+
* per-job handle is an AI-FILL stub that throws until filled.
|
|
25954
|
+
* Run with: npx tsx tests/${slug}.test.ts
|
|
25955
|
+
*/
|
|
25956
|
+
import { Queue } from "tina4-nodejs";
|
|
25957
|
+
import worker, { publish${pascal}, handle${pascal} } from "../src/services/${slug}_consumer.js";`;
|
|
25958
|
+
const body = `const jobId = publish${pascal}({ hello: "world" });
|
|
25959
|
+
assert("publish enqueues a real job (returns an id)", typeof jobId === "string" && jobId.length > 0);
|
|
25960
|
+
assert("the job is really on the queue", new Queue({ topic: "${topic}" }).size() >= 1);
|
|
25961
|
+
|
|
25962
|
+
assert("consumer default export is a daemon", worker.daemon === true);
|
|
25963
|
+
assert("consumer handler is wired", typeof worker.handler === "function");
|
|
25964
|
+
|
|
25965
|
+
// The per-job handler is an AI-FILL stub \u2014 it throws until filled.
|
|
25966
|
+
await assertThrows("handle is a live stub (throws until filled)", () => handle${pascal}({}));`;
|
|
25967
|
+
writeTest(slug, standaloneTest(doc, body));
|
|
25968
|
+
}
|
|
25969
|
+
function emitValidatorTest(name, snake, pascal) {
|
|
25970
|
+
const doc = `/**
|
|
25971
|
+
* Real validation test for validate${pascal} \u2014 no mocks.
|
|
25972
|
+
*
|
|
25973
|
+
* Generated with src/validators/${snake}.ts by \`tina4nodejs generate validator
|
|
25974
|
+
* ${name}\`. The scaffold ships a starter rule (required "name"), so this passes
|
|
25975
|
+
* on generation \u2014 adjust the rules for your payload and update these cases.
|
|
25976
|
+
* Run with: npx tsx tests/${snake}.test.ts
|
|
25977
|
+
*/
|
|
25978
|
+
import { validate${pascal} } from "../src/validators/${snake}.js";`;
|
|
25979
|
+
const body = `assert("valid input passes", validate${pascal}({ name: "Ada" }).isValid());
|
|
25980
|
+
|
|
25981
|
+
const bad = validate${pascal}({});
|
|
25982
|
+
assert("invalid input fails", bad.isValid() === false);
|
|
25983
|
+
assert("invalid input reports errors", bad.errors().length > 0);`;
|
|
25984
|
+
writeTest(snake, standaloneTest(doc, body));
|
|
25985
|
+
}
|
|
25986
|
+
function emitSeederTest(model, table2) {
|
|
25987
|
+
const doc = `/**
|
|
25988
|
+
* Real seeding test for the ${model} seeder \u2014 no mocks, real SQLite.
|
|
25989
|
+
*
|
|
25990
|
+
* Generated with src/seeds/${table2}_seeder.ts by \`tina4nodejs generate seeder
|
|
25991
|
+
* ${model}\`. Binds a real SQLite DB, creates the table, runs the scaffolded
|
|
25992
|
+
* seeder (auto-fills every field via FakeData) and asserts rows were created.
|
|
25993
|
+
* Run with: npx tsx tests/${table2}_seeder.test.ts
|
|
25994
|
+
*/
|
|
25995
|
+
import { initDatabase, FakeData } from "tina4-nodejs/orm";
|
|
25996
|
+
import ${model} from "../src/models/${model}.js";
|
|
25997
|
+
import { fieldOverrides, run } from "../src/seeds/${table2}_seeder.js";`;
|
|
25998
|
+
const body = `process.env.TINA4_DATABASE_URL = "sqlite:///test_${table2}_seeder.db";
|
|
25999
|
+
await initDatabase({ url: process.env.TINA4_DATABASE_URL });
|
|
26000
|
+
await ${model}.createTable();
|
|
26001
|
+
|
|
26002
|
+
assert("fieldOverrides returns an object", typeof fieldOverrides(new FakeData()) === "object");
|
|
26003
|
+
|
|
26004
|
+
await run(); // run() re-binds the same DB URL and seeds via seedOrm
|
|
26005
|
+
const rows = await ${model}.all();
|
|
26006
|
+
assert("run() seeds real rows", rows.length >= 1);`;
|
|
26007
|
+
writeTest(`${table2}_seeder`, standaloneTest(doc, body));
|
|
26008
|
+
}
|
|
26009
|
+
function emitWebsocketTest(wsPath, base, handler) {
|
|
26010
|
+
const doc = `/**
|
|
26011
|
+
* Real handler test for the ${wsPath} WebSocket route \u2014 no mocks.
|
|
26012
|
+
*
|
|
26013
|
+
* Generated with src/routes/ws_${base}.ts by \`tina4nodejs generate websocket
|
|
26014
|
+
* ...\`. Confirms the handler registers on the REAL Router (importing runs the
|
|
26015
|
+
* module-level websocket() call) and drives the real handler for the "close"
|
|
26016
|
+
* event (no socket needed). The "message" branch is an AI-FILL stub that throws
|
|
26017
|
+
* until filled. Run with: npx tsx tests/ws_${base}.test.ts
|
|
26018
|
+
*/
|
|
26019
|
+
import { Router } from "tina4-nodejs";
|
|
26020
|
+
import { ${handler} } from "../src/routes/ws_${base}.js"; // importing registers via websocket()`;
|
|
26021
|
+
const body = `assert("handler registered on the real router",
|
|
26022
|
+
Router.getWebSocketRoutes().some((r) => r.pattern === "${wsPath}"));
|
|
26023
|
+
|
|
26024
|
+
// The "close" branch returns cleanly without a live connection.
|
|
26025
|
+
const closed = await ${handler}(null as never, "close", "");
|
|
26026
|
+
assert("close event handled cleanly", closed === undefined);
|
|
26027
|
+
|
|
26028
|
+
// The "message" branch is an AI-FILL stub \u2014 it throws until filled.
|
|
26029
|
+
await assertThrows("message branch is a live stub (throws until filled)",
|
|
26030
|
+
() => ${handler}(null as never, "message", "hi"));`;
|
|
26031
|
+
writeTest(`ws_${base}`, standaloneTest(doc, body));
|
|
26032
|
+
}
|
|
26033
|
+
function emitListenerTest(event, slug) {
|
|
26034
|
+
const doc = `/**
|
|
26035
|
+
* Real event-bus test for the '${event}' listener \u2014 no mocks.
|
|
26036
|
+
*
|
|
26037
|
+
* Generated with src/listeners/${slug}.ts by \`tina4nodejs generate listener
|
|
26038
|
+
* ${event}\`. Confirms the listener binds on the REAL event bus (importing runs
|
|
26039
|
+
* the module-level Events.on) and that emitting the event reaches it. The
|
|
26040
|
+
* reaction body is an AI-FILL stub, so a strict emit re-raises here (proving it
|
|
26041
|
+
* ran). Run with: npx tsx tests/${slug}.test.ts
|
|
26042
|
+
*/
|
|
26043
|
+
import { Events } from "tina4-nodejs";
|
|
26044
|
+
import "../src/listeners/${slug}.js"; // importing registers the listener via Events.on()`;
|
|
26045
|
+
const body = `assert("listener registered on the real event bus", Events.listeners("${event}").length >= 1);
|
|
26046
|
+
|
|
26047
|
+
// strict emit re-raises the stub error, proving the listener actually ran.
|
|
26048
|
+
await assertThrows("emitting the event reaches the (stub) listener",
|
|
26049
|
+
() => Events.emit("${event}", { strict: true }, { id: 1 }));`;
|
|
26050
|
+
writeTest(slug, standaloneTest(doc, body));
|
|
26051
|
+
}
|
|
26052
|
+
function emitAuthTest() {
|
|
26053
|
+
const doc = `/**
|
|
26054
|
+
* Real auth test \u2014 register / login / me via the real TestClient.
|
|
26055
|
+
*
|
|
26056
|
+
* Generated with the auth scaffold by \`tina4nodejs generate auth\`. No mocks:
|
|
26057
|
+
* real Router + route discovery, real Auth (PBKDF2 + JWT), real SQLite. register
|
|
26058
|
+
* + login are public; the token from login authenticates GET /api/auth/me.
|
|
26059
|
+
* Run with: npx tsx tests/auth.test.ts
|
|
26060
|
+
*/
|
|
26061
|
+
import { dirname, resolve } from "node:path";
|
|
26062
|
+
import { fileURLToPath } from "node:url";
|
|
26063
|
+
import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
|
|
26064
|
+
import { initDatabase } from "tina4-nodejs/orm";
|
|
26065
|
+
import User from "../src/models/User.js";
|
|
26066
|
+
|
|
26067
|
+
process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
|
|
26068
|
+
delete process.env.TINA4_API_KEY;
|
|
26069
|
+
const here = dirname(fileURLToPath(import.meta.url));`;
|
|
26070
|
+
const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
|
|
26071
|
+
await User.createTable();
|
|
26072
|
+
for (const existing of await User.all()) await existing.delete(); // start from an empty table
|
|
26073
|
+
|
|
26074
|
+
const router = new Router();
|
|
26075
|
+
for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
|
|
26076
|
+
const client = new TestClient(router);
|
|
26077
|
+
|
|
26078
|
+
const registered = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
|
|
26079
|
+
assert("register a new user -> 201", registered.status === 201);
|
|
26080
|
+
|
|
26081
|
+
const duplicate = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
|
|
26082
|
+
assert("duplicate register -> 409", duplicate.status === 409);
|
|
26083
|
+
|
|
26084
|
+
const login = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "secret12" } });
|
|
26085
|
+
assert("login -> 200", login.status === 200);
|
|
26086
|
+
const token = (login.json() as { token?: string }).token;
|
|
26087
|
+
assert("login returns a token", typeof token === "string" && token.length > 0);
|
|
26088
|
+
|
|
26089
|
+
const me = await client.get("/api/auth/me", { headers: { authorization: \`Bearer \${token}\` } });
|
|
26090
|
+
assert("authenticated GET /api/auth/me -> 200 (token accepted)", me.status === 200);
|
|
26091
|
+
assert("me returns the authenticated user's email",
|
|
26092
|
+
((me.json() as { user?: { email?: string } }).user?.email) === "a@b.c");
|
|
26093
|
+
|
|
26094
|
+
const anon = await client.get("/api/auth/me");
|
|
26095
|
+
assert("anonymous GET /api/auth/me -> 401", anon.status === 401);
|
|
26096
|
+
|
|
26097
|
+
const bad = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "WRONG" } });
|
|
26098
|
+
assert("wrong password -> 401", bad.status === 401);`;
|
|
26099
|
+
writeTest("auth", standaloneTest(doc, body));
|
|
26100
|
+
}
|
|
26101
|
+
function emitMigrationTest(migrationName, table2) {
|
|
26102
|
+
const doc = `/**
|
|
26103
|
+
* Real migration test for ${migrationName} \u2014 no mocks, real SQLite.
|
|
26104
|
+
*
|
|
26105
|
+
* Generated with the migration by \`tina4nodejs generate migration
|
|
26106
|
+
* ${migrationName}\`. Applies the generated UP SQL against a fresh real
|
|
26107
|
+
* in-memory SQLite database and asserts the table exists, then applies the DOWN
|
|
26108
|
+
* SQL and asserts it is gone \u2014 the raw SQL the migration runner executes.
|
|
26109
|
+
* Run with: npx tsx tests/${table2}_migration.test.ts
|
|
26110
|
+
*/
|
|
26111
|
+
import { dirname, join, resolve } from "node:path";
|
|
26112
|
+
import { fileURLToPath } from "node:url";
|
|
26113
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
26114
|
+
import { SQLiteAdapter } from "tina4-nodejs/orm";`;
|
|
26115
|
+
const body = `const here = dirname(fileURLToPath(import.meta.url));
|
|
26116
|
+
const migrationsDir = resolve(here, "../migrations");
|
|
26117
|
+
|
|
26118
|
+
const upFile = readdirSync(migrationsDir).find((f) => f.endsWith("_${migrationName}.sql") && !f.endsWith(".down.sql"));
|
|
26119
|
+
assert("generated UP migration file exists", Boolean(upFile));
|
|
26120
|
+
const downFile = upFile!.replace(/\\.sql$/, ".down.sql");
|
|
26121
|
+
|
|
26122
|
+
function statements(sql: string): string[] {
|
|
26123
|
+
const noComments = sql.split("\\n").filter((l) => !l.trim().startsWith("--")).join("\\n");
|
|
26124
|
+
return noComments.split(";").map((s) => s.trim()).filter(Boolean);
|
|
26125
|
+
}
|
|
26126
|
+
|
|
26127
|
+
const upText = readFileSync(join(migrationsDir, upFile!), "utf-8");
|
|
26128
|
+
const upSql = upText.split("-- UP")[1].split("-- DOWN")[0];
|
|
26129
|
+
const downSql = readFileSync(join(migrationsDir, downFile), "utf-8");
|
|
26130
|
+
|
|
26131
|
+
const db = new SQLiteAdapter(":memory:");
|
|
26132
|
+
for (const stmt of statements(upSql)) db.execute(stmt);
|
|
26133
|
+
assert("UP creates the ${table2} table", db.tableExists("${table2}"));
|
|
26134
|
+
|
|
26135
|
+
for (const stmt of statements(downSql)) db.execute(stmt);
|
|
26136
|
+
assert("DOWN drops the ${table2} table", db.tableExists("${table2}") === false);`;
|
|
26137
|
+
writeTest(`${table2}_migration`, standaloneTest(doc, body));
|
|
26138
|
+
}
|
|
26139
|
+
var FIELD_TYPE_MAP, SQL_RESERVED_TABLE_NAMES, RESOLUTION_ENVELOPE_VERSION, __resolution, TINA4_EDIT_MARKER, DEFAULT_FIELDS, GENERATORS, GENERATOR_LIST, NEXT_STEPS;
|
|
26140
|
+
var init_generate = __esm({
|
|
26141
|
+
"../cli/src/commands/generate.ts"() {
|
|
26142
|
+
"use strict";
|
|
26143
|
+
FIELD_TYPE_MAP = {
|
|
26144
|
+
string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
26145
|
+
str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
26146
|
+
int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
26147
|
+
integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
26148
|
+
float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
26149
|
+
number: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
26150
|
+
numeric: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
26151
|
+
decimal: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
26152
|
+
bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
26153
|
+
boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
26154
|
+
text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
26155
|
+
datetime: { orm: '"datetime"', sql: "TEXT", defaultVal: "NULL" },
|
|
26156
|
+
blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
|
|
26157
|
+
};
|
|
26158
|
+
SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
|
|
26159
|
+
"order",
|
|
26160
|
+
"group",
|
|
26161
|
+
"user",
|
|
26162
|
+
"table",
|
|
26163
|
+
"select",
|
|
26164
|
+
"from",
|
|
26165
|
+
"where",
|
|
26166
|
+
"index",
|
|
26167
|
+
"key",
|
|
26168
|
+
"values",
|
|
26169
|
+
"column",
|
|
26170
|
+
"constraint",
|
|
26171
|
+
"check",
|
|
26172
|
+
"default",
|
|
26173
|
+
"primary",
|
|
26174
|
+
"foreign",
|
|
26175
|
+
"references",
|
|
26176
|
+
"unique",
|
|
26177
|
+
"join",
|
|
26178
|
+
"union",
|
|
26179
|
+
"having",
|
|
26180
|
+
"limit",
|
|
26181
|
+
"offset",
|
|
26182
|
+
"desc",
|
|
26183
|
+
"asc",
|
|
26184
|
+
"case",
|
|
26185
|
+
"when",
|
|
26186
|
+
"then",
|
|
26187
|
+
"else",
|
|
26188
|
+
"end",
|
|
26189
|
+
"and",
|
|
26190
|
+
"or",
|
|
26191
|
+
"not",
|
|
26192
|
+
"null",
|
|
26193
|
+
"insert",
|
|
26194
|
+
"update",
|
|
26195
|
+
"delete",
|
|
26196
|
+
"create",
|
|
26197
|
+
"drop",
|
|
26198
|
+
"alter",
|
|
26199
|
+
"grant",
|
|
26200
|
+
"revoke",
|
|
26201
|
+
"commit",
|
|
26202
|
+
"rollback",
|
|
26203
|
+
"view",
|
|
26204
|
+
"trigger",
|
|
26205
|
+
"procedure",
|
|
26206
|
+
"function",
|
|
26207
|
+
"database",
|
|
26208
|
+
"schema",
|
|
26209
|
+
"session",
|
|
26210
|
+
"set",
|
|
26211
|
+
"into",
|
|
26212
|
+
"as",
|
|
26213
|
+
"on",
|
|
26214
|
+
"by",
|
|
26215
|
+
"inner",
|
|
26216
|
+
"outer",
|
|
26217
|
+
"left",
|
|
26218
|
+
"right",
|
|
26219
|
+
"full",
|
|
26220
|
+
"natural",
|
|
26221
|
+
"using",
|
|
26222
|
+
"with",
|
|
26223
|
+
"distinct",
|
|
26224
|
+
"between",
|
|
26225
|
+
"exists",
|
|
26226
|
+
"like",
|
|
26227
|
+
"in",
|
|
26228
|
+
"is",
|
|
26229
|
+
"all",
|
|
26230
|
+
"any",
|
|
26231
|
+
"cross",
|
|
26232
|
+
"add",
|
|
26233
|
+
"row",
|
|
26234
|
+
"rows",
|
|
26235
|
+
"range",
|
|
26236
|
+
"current",
|
|
26237
|
+
"to"
|
|
26238
|
+
]);
|
|
26239
|
+
RESOLUTION_ENVELOPE_VERSION = "generate_v1_1";
|
|
26240
|
+
__resolution = {
|
|
26241
|
+
target: "",
|
|
26242
|
+
input: { name: "", fields: null },
|
|
26243
|
+
body: { transformations: [] },
|
|
26244
|
+
actionsTaken: [],
|
|
26245
|
+
dryRun: false,
|
|
26246
|
+
jsonMode: false
|
|
26247
|
+
};
|
|
26248
|
+
TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
|
|
26249
|
+
DEFAULT_FIELDS = [["name", "string"]];
|
|
26250
|
+
GENERATORS = {
|
|
26251
|
+
model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
|
|
26252
|
+
route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
26253
|
+
crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
|
|
26254
|
+
migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
26255
|
+
middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
|
|
26256
|
+
test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
|
|
26257
|
+
form: { handler: generateForm, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" },
|
|
26258
|
+
view: { handler: generateView, usage: '<Name> [--fields "..."]', summary: "List + detail view templates" },
|
|
26259
|
+
auth: { handler: (_n, f) => generateAuth(f), usage: "", summary: "Login/register routes (public) + User model + templates" },
|
|
26260
|
+
service: { handler: generateService, usage: '<Name> [--every 5m | --cron "..."]', summary: "Scheduled ServiceRunner task (src/services/)" },
|
|
26261
|
+
queue: { handler: generateQueue, usage: "<topic>", summary: "Producer + consumer daemon worker (src/services/)" },
|
|
26262
|
+
validator: { handler: generateValidator, usage: "<Name>", summary: "Request-body Validator (src/validators/)" },
|
|
26263
|
+
seeder: { handler: generateSeeder, usage: "<Model>", summary: "FakeData + seedOrm seeder (src/seeds/)" },
|
|
26264
|
+
websocket: { handler: generateWebsocket, usage: "<path>", summary: "websocket() handler (src/routes/)" },
|
|
26265
|
+
listener: { handler: generateListener, usage: "<event>", summary: "Events.on(event) listener (src/listeners/)" }
|
|
26266
|
+
};
|
|
26267
|
+
GENERATOR_LIST = Object.keys(GENERATORS).join(", ");
|
|
26268
|
+
NEXT_STEPS = {
|
|
26269
|
+
model: ({ name, table: table2 }) => [
|
|
26270
|
+
`Edit src/models/${name}.ts to add fields beyond the default 'name'`,
|
|
26271
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
26272
|
+
`Run its test: npx tsx tests/${table2}_model.test.ts`,
|
|
26273
|
+
`Add CRUD scaffolding: npx tina4nodejs generate crud ${name}`
|
|
26274
|
+
],
|
|
26275
|
+
route: ({ name, table: table2 }) => [
|
|
26276
|
+
`Fill the AI-FILL stubs in src/routes/api/${name.replace(/^\//, "")}/`,
|
|
26277
|
+
`Run its test: npx tsx tests/${table2}.test.ts`,
|
|
26278
|
+
`Serve and try: npx tina4nodejs serve -> curl http://localhost:7148/api/${name.replace(/^\//, "")}`
|
|
26279
|
+
],
|
|
26280
|
+
crud: ({ name, table: table2 }) => [
|
|
26281
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
26282
|
+
`Serve and try: npx tina4nodejs serve -> visit /swagger`,
|
|
26283
|
+
`Run the gate test: npx tsx tests/${toPlural(table2)}.test.ts`,
|
|
26284
|
+
`Change fields: edit src/models/${name}.ts then re-run generate crud`
|
|
26285
|
+
],
|
|
26286
|
+
migration: () => [
|
|
26287
|
+
`Apply pending migrations: npx tina4nodejs migrate`,
|
|
26288
|
+
`Check status: npx tina4nodejs migrate:status`,
|
|
26289
|
+
`Roll back the batch: npx tina4nodejs migrate:rollback`
|
|
26290
|
+
],
|
|
26291
|
+
middleware: ({ name }) => [
|
|
26292
|
+
`Wire it: router.middleware(before${name}, after${name}) \u2014 or bind per-route`,
|
|
26293
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
26294
|
+
],
|
|
26295
|
+
test: ({ name }) => [
|
|
26296
|
+
`Fill the TODOs in tests/${toSnake(name)}.test.ts`,
|
|
26297
|
+
`Run it: npx tsx tests/${toSnake(name)}.test.ts`
|
|
26298
|
+
],
|
|
26299
|
+
form: ({ name, table: table2 }) => [
|
|
26300
|
+
`Render from a route: res.render("forms/${table2}.twig", { item })`,
|
|
26301
|
+
`Add the POST route: npx tina4nodejs generate route ${toPlural(table2)} --model ${name}`
|
|
26302
|
+
],
|
|
26303
|
+
view: ({ table: table2 }) => [
|
|
26304
|
+
`Wire routes to render list -> ${toPlural(table2)}.twig, detail -> ${table2}.twig`,
|
|
26305
|
+
`Customize the templates in src/templates/pages/`
|
|
26306
|
+
],
|
|
26307
|
+
auth: () => [
|
|
26308
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
26309
|
+
`Run the auth test: npx tsx tests/auth.test.ts`,
|
|
26310
|
+
`Try register: curl -X POST http://localhost:7148/api/auth/register -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`,
|
|
26311
|
+
`Login: curl -X POST http://localhost:7148/api/auth/login -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`
|
|
26312
|
+
],
|
|
26313
|
+
service: ({ name }) => [
|
|
26314
|
+
`Wire ServiceRunner in app.ts: await ServiceRunner.discover("src/services"); ServiceRunner.start();`,
|
|
26315
|
+
`Fill the task body in src/services/${toSnake(name)}.ts`,
|
|
26316
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
26317
|
+
],
|
|
26318
|
+
queue: ({ name }) => {
|
|
26319
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
|
|
26320
|
+
return [
|
|
26321
|
+
`Fill handle${toPascal(name)}() in src/services/${slug}_consumer.ts`,
|
|
26322
|
+
`Produce a job: publish${toPascal(name)}({ ... })`,
|
|
26323
|
+
`Run the worker: npx tina4nodejs queue work ${name}`,
|
|
26324
|
+
`Run its test: npx tsx tests/${slug}.test.ts`
|
|
26325
|
+
];
|
|
26326
|
+
},
|
|
26327
|
+
validator: ({ name }) => [
|
|
26328
|
+
`Add rules in src/validators/${toSnake(name)}.ts (.email/.minLength/.integer/.inList/.pattern)`,
|
|
26329
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`
|
|
26330
|
+
],
|
|
26331
|
+
seeder: ({ name, table: table2 }) => [
|
|
26332
|
+
`Override any fields that need a specific shape in src/seeds/${table2}_seeder.ts`,
|
|
26333
|
+
`Seed the table: npx tina4nodejs seed`,
|
|
26334
|
+
`Run its test: npx tsx tests/${table2}_seeder.test.ts`
|
|
26335
|
+
],
|
|
26336
|
+
websocket: ({ name }) => {
|
|
26337
|
+
const raw = name.trim();
|
|
26338
|
+
const slugRaw = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
|
|
26339
|
+
const base = slugRaw.startsWith("ws_") ? slugRaw.slice(3) : slugRaw;
|
|
26340
|
+
return [
|
|
26341
|
+
`Import once in app.ts to register: import "./src/routes/ws_${base}.js";`,
|
|
26342
|
+
`Fill the "message" branch in src/routes/ws_${base}.ts`,
|
|
26343
|
+
`Run its test: npx tsx tests/ws_${base}.test.ts`
|
|
26344
|
+
];
|
|
26345
|
+
},
|
|
26346
|
+
listener: ({ name }) => {
|
|
26347
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
|
|
26348
|
+
return [
|
|
26349
|
+
`Import once in app.ts to register: import "./src/listeners/${slug}.js";`,
|
|
26350
|
+
`Fill the reaction in src/listeners/${slug}.ts`,
|
|
26351
|
+
`Run its test: npx tsx tests/${slug}.test.ts`
|
|
26352
|
+
];
|
|
26353
|
+
}
|
|
26354
|
+
};
|
|
26355
|
+
}
|
|
26356
|
+
});
|
|
26357
|
+
|
|
24326
26358
|
// src/mcp.ts
|
|
24327
26359
|
var mcp_exports = {};
|
|
24328
26360
|
__export(mcp_exports, {
|
|
@@ -24823,10 +26855,10 @@ function registerDevTools(server) {
|
|
|
24823
26855
|
"swagger_spec",
|
|
24824
26856
|
(_args) => {
|
|
24825
26857
|
try {
|
|
24826
|
-
const { generate:
|
|
26858
|
+
const { generate: generate3 } = reqSibling("swagger");
|
|
24827
26859
|
const { defaultRouter: defaultRouter2 } = req("./router.js");
|
|
24828
26860
|
const routes = defaultRouter2?.getRoutes?.() ?? [];
|
|
24829
|
-
return
|
|
26861
|
+
return generate3?.(routes, []) ?? { info: "Swagger not available" };
|
|
24830
26862
|
} catch (e) {
|
|
24831
26863
|
return { error: e.message };
|
|
24832
26864
|
}
|
|
@@ -24985,20 +27017,43 @@ function registerDevTools(server) {
|
|
|
24985
27017
|
);
|
|
24986
27018
|
server.registerTool(
|
|
24987
27019
|
"migration_create",
|
|
24988
|
-
(args) => {
|
|
24989
|
-
|
|
24990
|
-
|
|
24991
|
-
|
|
24992
|
-
|
|
24993
|
-
|
|
24994
|
-
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
|
|
24998
|
-
|
|
24999
|
-
|
|
27020
|
+
async (args) => {
|
|
27021
|
+
try {
|
|
27022
|
+
const rawDesc = String(args.description ?? "").trim();
|
|
27023
|
+
if (!rawDesc) return { ok: false, error: "description is required" };
|
|
27024
|
+
const slug = rawDesc.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
27025
|
+
if (!slug) return { ok: false, error: "description sanitised to an empty slug" };
|
|
27026
|
+
const migrationsDir = path3.join(projectRoot3, "migrations");
|
|
27027
|
+
if (fs4.existsSync(migrationsDir)) {
|
|
27028
|
+
const upSuffix = `_${slug}.sql`;
|
|
27029
|
+
const downSuffix = `_${slug}.down.sql`;
|
|
27030
|
+
const existing = fs4.readdirSync(migrationsDir).filter(
|
|
27031
|
+
(f) => f.endsWith(upSuffix) && !f.endsWith(downSuffix) || f.endsWith(downSuffix)
|
|
27032
|
+
);
|
|
27033
|
+
if (existing.length > 0) {
|
|
27034
|
+
return {
|
|
27035
|
+
ok: false,
|
|
27036
|
+
error: `A migration with slug "${slug}" already exists`,
|
|
27037
|
+
existing
|
|
27038
|
+
};
|
|
27039
|
+
}
|
|
27040
|
+
}
|
|
27041
|
+
const originalCwd = process.cwd();
|
|
27042
|
+
try {
|
|
27043
|
+
process.chdir(projectRoot3);
|
|
27044
|
+
const gen = await Promise.resolve().then(() => (init_generate(), generate_exports));
|
|
27045
|
+
const envelope = await gen.generateProgrammatic("migration", slug, ["--no-test"]);
|
|
27046
|
+
const migrationPath = envelope.resolution?.migration_path;
|
|
27047
|
+
const created = migrationPath ? path3.basename(migrationPath) : "";
|
|
27048
|
+
return { ok: true, created, resolution: envelope };
|
|
27049
|
+
} finally {
|
|
27050
|
+
process.chdir(originalCwd);
|
|
27051
|
+
}
|
|
27052
|
+
} catch (e) {
|
|
27053
|
+
return { ok: false, error: e.message };
|
|
27054
|
+
}
|
|
25000
27055
|
},
|
|
25001
|
-
"Create a new migration file",
|
|
27056
|
+
"Create a new migration file (delegates to `generate migration` \u2014 emits the ADR-0063 generate_v1_1 envelope + timestamped filename)",
|
|
25002
27057
|
schemaFromParams([{ name: "description", type: "string" }])
|
|
25003
27058
|
);
|
|
25004
27059
|
server.registerTool(
|
|
@@ -25766,14 +27821,14 @@ data: ${channel.buffer.shift()}
|
|
|
25766
27821
|
`;
|
|
25767
27822
|
continue;
|
|
25768
27823
|
}
|
|
25769
|
-
const gotMessage = await new Promise((
|
|
27824
|
+
const gotMessage = await new Promise((resolve21) => {
|
|
25770
27825
|
const timer = setTimeout(() => {
|
|
25771
27826
|
channel.wake = null;
|
|
25772
|
-
|
|
27827
|
+
resolve21(false);
|
|
25773
27828
|
}, keepaliveMs);
|
|
25774
27829
|
channel.wake = () => {
|
|
25775
27830
|
clearTimeout(timer);
|
|
25776
|
-
|
|
27831
|
+
resolve21(true);
|
|
25777
27832
|
};
|
|
25778
27833
|
});
|
|
25779
27834
|
if (!gotMessage) yield `: keep-alive
|
|
@@ -27092,8 +29147,8 @@ __export(context_exports, {
|
|
|
27092
29147
|
fts5Supported: () => fts5Supported
|
|
27093
29148
|
});
|
|
27094
29149
|
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
|
|
29150
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync15, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
|
|
29151
|
+
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
29152
|
function fts5Supported() {
|
|
27098
29153
|
try {
|
|
27099
29154
|
const conn = new DatabaseSync4(":memory:");
|
|
@@ -27116,13 +29171,13 @@ function realResolve(abs) {
|
|
|
27116
29171
|
} catch {
|
|
27117
29172
|
}
|
|
27118
29173
|
try {
|
|
27119
|
-
return
|
|
29174
|
+
return join22(realpathSync5(dirname10(abs)), basename4(abs));
|
|
27120
29175
|
} catch {
|
|
27121
29176
|
return abs;
|
|
27122
29177
|
}
|
|
27123
29178
|
}
|
|
27124
29179
|
function dbKey(db) {
|
|
27125
|
-
return
|
|
29180
|
+
return resolve12(db ? String(db) : join22(process.cwd(), ".tina4", "context.db"));
|
|
27126
29181
|
}
|
|
27127
29182
|
function defaultContext(root, db) {
|
|
27128
29183
|
const key = dbKey(db);
|
|
@@ -27206,7 +29261,7 @@ var init_context = __esm({
|
|
|
27206
29261
|
if (!this.available) return;
|
|
27207
29262
|
const parent = dirname10(this.path);
|
|
27208
29263
|
if (parent !== "" && parent !== ".") {
|
|
27209
|
-
|
|
29264
|
+
mkdirSync14(parent, { recursive: true });
|
|
27210
29265
|
}
|
|
27211
29266
|
this.conn = new DatabaseSync4(this.path);
|
|
27212
29267
|
this.ensureTable();
|
|
@@ -27278,7 +29333,7 @@ var init_context = __esm({
|
|
|
27278
29333
|
*/
|
|
27279
29334
|
indexRoot(root) {
|
|
27280
29335
|
if (!this.available) return 0;
|
|
27281
|
-
const rootAbs = realResolve(
|
|
29336
|
+
const rootAbs = realResolve(resolve12(String(root)));
|
|
27282
29337
|
this.root = rootAbs;
|
|
27283
29338
|
let total = 0;
|
|
27284
29339
|
const walk2 = (dir) => {
|
|
@@ -27295,11 +29350,11 @@ var init_context = __esm({
|
|
|
27295
29350
|
files.sort();
|
|
27296
29351
|
for (const fn of files) {
|
|
27297
29352
|
if (!_Context.eligible(fn)) continue;
|
|
27298
|
-
const full =
|
|
27299
|
-
const rel =
|
|
29353
|
+
const full = join22(dir, fn);
|
|
29354
|
+
const rel = relative4(rootAbs, full);
|
|
27300
29355
|
total += this.indexPath(full, rel);
|
|
27301
29356
|
}
|
|
27302
|
-
for (const d of subdirs) walk2(
|
|
29357
|
+
for (const d of subdirs) walk2(join22(dir, d));
|
|
27303
29358
|
};
|
|
27304
29359
|
walk2(rootAbs);
|
|
27305
29360
|
return total;
|
|
@@ -27315,9 +29370,9 @@ var init_context = __esm({
|
|
|
27315
29370
|
reindexFile(changedPath) {
|
|
27316
29371
|
if (!this.available || this.root === null) return -1;
|
|
27317
29372
|
const raw = String(changedPath);
|
|
27318
|
-
const abs = isAbsolute6(raw) ? raw :
|
|
27319
|
-
const resolved = realResolve(
|
|
27320
|
-
const rel =
|
|
29373
|
+
const abs = isAbsolute6(raw) ? raw : join22(process.cwd(), raw);
|
|
29374
|
+
const resolved = realResolve(resolve12(abs));
|
|
29375
|
+
const rel = relative4(this.root, resolved);
|
|
27321
29376
|
if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) {
|
|
27322
29377
|
return -1;
|
|
27323
29378
|
}
|
|
@@ -27328,7 +29383,7 @@ var init_context = __esm({
|
|
|
27328
29383
|
}
|
|
27329
29384
|
if (!_Context.eligible(basename4(rel))) return -1;
|
|
27330
29385
|
const stored = rel;
|
|
27331
|
-
if (!
|
|
29386
|
+
if (!existsSync17(abs)) {
|
|
27332
29387
|
this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
|
|
27333
29388
|
return 0;
|
|
27334
29389
|
}
|
|
@@ -28074,7 +30129,7 @@ var init_websocket = __esm({
|
|
|
28074
30129
|
* Start the WebSocket server.
|
|
28075
30130
|
*/
|
|
28076
30131
|
async start() {
|
|
28077
|
-
return new Promise((
|
|
30132
|
+
return new Promise((resolve21, reject) => {
|
|
28078
30133
|
this.server = createServer((req2, res) => {
|
|
28079
30134
|
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
28080
30135
|
res.end("Upgrade Required");
|
|
@@ -28084,7 +30139,7 @@ var init_websocket = __esm({
|
|
|
28084
30139
|
});
|
|
28085
30140
|
this.server.listen(this.port, () => {
|
|
28086
30141
|
this.startIdleReaper();
|
|
28087
|
-
|
|
30142
|
+
resolve21();
|
|
28088
30143
|
});
|
|
28089
30144
|
this.server.on("error", (err) => {
|
|
28090
30145
|
this.emit("error", err);
|
|
@@ -29305,8 +31360,8 @@ var init_job = __esm({
|
|
|
29305
31360
|
});
|
|
29306
31361
|
|
|
29307
31362
|
// src/queueBackends/liteBackend.ts
|
|
29308
|
-
import { mkdirSync as
|
|
29309
|
-
import { join as
|
|
31363
|
+
import { mkdirSync as mkdirSync15, readdirSync as readdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7, existsSync as existsSync18 } from "node:fs";
|
|
31364
|
+
import { join as join23 } from "node:path";
|
|
29310
31365
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
29311
31366
|
var LiteBackend;
|
|
29312
31367
|
var init_liteBackend = __esm({
|
|
@@ -29330,22 +31385,22 @@ var init_liteBackend = __esm({
|
|
|
29330
31385
|
this.visibilityTimeout = visibilityTimeout;
|
|
29331
31386
|
}
|
|
29332
31387
|
ensureDir(queue) {
|
|
29333
|
-
const dir =
|
|
29334
|
-
|
|
31388
|
+
const dir = join23(this.basePath, queue);
|
|
31389
|
+
mkdirSync15(dir, { recursive: true });
|
|
29335
31390
|
return dir;
|
|
29336
31391
|
}
|
|
29337
31392
|
ensureFailedDir(queue) {
|
|
29338
|
-
const dir =
|
|
29339
|
-
|
|
31393
|
+
const dir = join23(this.basePath, queue, "failed");
|
|
31394
|
+
mkdirSync15(dir, { recursive: true });
|
|
29340
31395
|
return dir;
|
|
29341
31396
|
}
|
|
29342
31397
|
ensureReservedDir(queue) {
|
|
29343
|
-
const dir =
|
|
29344
|
-
|
|
31398
|
+
const dir = join23(this.basePath, queue, "reserved");
|
|
31399
|
+
mkdirSync15(dir, { recursive: true });
|
|
29345
31400
|
return dir;
|
|
29346
31401
|
}
|
|
29347
31402
|
reservedPath(queue, jobId) {
|
|
29348
|
-
return
|
|
31403
|
+
return join23(this.ensureReservedDir(queue), `${jobId}.queue-data`);
|
|
29349
31404
|
}
|
|
29350
31405
|
nowIso() {
|
|
29351
31406
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -29383,7 +31438,7 @@ var init_liteBackend = __esm({
|
|
|
29383
31438
|
error: void 0
|
|
29384
31439
|
};
|
|
29385
31440
|
const prefix = this.nextPrefix();
|
|
29386
|
-
|
|
31441
|
+
writeFileSync11(join23(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
|
|
29387
31442
|
return id;
|
|
29388
31443
|
}
|
|
29389
31444
|
/**
|
|
@@ -29402,7 +31457,7 @@ var init_liteBackend = __esm({
|
|
|
29402
31457
|
}
|
|
29403
31458
|
const candidates = [];
|
|
29404
31459
|
for (const filename of filenames) {
|
|
29405
|
-
const filePath =
|
|
31460
|
+
const filePath = join23(dir, filename);
|
|
29406
31461
|
let job;
|
|
29407
31462
|
try {
|
|
29408
31463
|
job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
@@ -29445,7 +31500,7 @@ var init_liteBackend = __esm({
|
|
|
29445
31500
|
createdAt: job.createdAt ?? now,
|
|
29446
31501
|
topic: job.topic ?? queue
|
|
29447
31502
|
};
|
|
29448
|
-
|
|
31503
|
+
writeFileSync11(this.reservedPath(queue, record.id), JSON.stringify(record, null, 2));
|
|
29449
31504
|
}
|
|
29450
31505
|
/**
|
|
29451
31506
|
* Return expired reservations to the queue (at-least-once delivery).
|
|
@@ -29466,7 +31521,7 @@ var init_liteBackend = __esm({
|
|
|
29466
31521
|
return;
|
|
29467
31522
|
}
|
|
29468
31523
|
for (const filename of filenames) {
|
|
29469
|
-
const filePath =
|
|
31524
|
+
const filePath = join23(reservedDir, filename);
|
|
29470
31525
|
let record;
|
|
29471
31526
|
try {
|
|
29472
31527
|
record = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
@@ -29504,7 +31559,7 @@ var init_liteBackend = __esm({
|
|
|
29504
31559
|
this.reclaimExpired(queue, bridge.getMaxRetries(), this.nowIso());
|
|
29505
31560
|
const now = this.nowIso();
|
|
29506
31561
|
for (const [filename, job] of this.availableCandidates(queue, now)) {
|
|
29507
|
-
const filePath =
|
|
31562
|
+
const filePath = join23(dir, filename);
|
|
29508
31563
|
job.topic = queue;
|
|
29509
31564
|
job.priority = job.priority ?? 0;
|
|
29510
31565
|
this.writeReserved(queue, job);
|
|
@@ -29529,7 +31584,7 @@ var init_liteBackend = __esm({
|
|
|
29529
31584
|
const results = [];
|
|
29530
31585
|
for (const [filename, job] of this.availableCandidates(queue, now)) {
|
|
29531
31586
|
if (results.length >= count) break;
|
|
29532
|
-
const filePath =
|
|
31587
|
+
const filePath = join23(dir, filename);
|
|
29533
31588
|
job.topic = queue;
|
|
29534
31589
|
job.priority = job.priority ?? 0;
|
|
29535
31590
|
this.writeReserved(queue, job);
|
|
@@ -29582,7 +31637,7 @@ var init_liteBackend = __esm({
|
|
|
29582
31637
|
let count = 0;
|
|
29583
31638
|
for (const file of files) {
|
|
29584
31639
|
try {
|
|
29585
|
-
const job = JSON.parse(readFileSync16(
|
|
31640
|
+
const job = JSON.parse(readFileSync16(join23(scanDir, file), "utf-8"));
|
|
29586
31641
|
if (job.status === status2) count++;
|
|
29587
31642
|
} catch {
|
|
29588
31643
|
}
|
|
@@ -29595,28 +31650,28 @@ var init_liteBackend = __esm({
|
|
|
29595
31650
|
try {
|
|
29596
31651
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
|
|
29597
31652
|
for (const file of files) {
|
|
29598
|
-
unlinkSync7(
|
|
31653
|
+
unlinkSync7(join23(dir, file));
|
|
29599
31654
|
count++;
|
|
29600
31655
|
}
|
|
29601
31656
|
} catch {
|
|
29602
31657
|
}
|
|
29603
|
-
const failedDir =
|
|
31658
|
+
const failedDir = join23(dir, "failed");
|
|
29604
31659
|
try {
|
|
29605
|
-
if (
|
|
31660
|
+
if (existsSync18(failedDir)) {
|
|
29606
31661
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
29607
31662
|
for (const file of files) {
|
|
29608
|
-
unlinkSync7(
|
|
31663
|
+
unlinkSync7(join23(failedDir, file));
|
|
29609
31664
|
count++;
|
|
29610
31665
|
}
|
|
29611
31666
|
}
|
|
29612
31667
|
} catch {
|
|
29613
31668
|
}
|
|
29614
|
-
const reservedDir =
|
|
31669
|
+
const reservedDir = join23(dir, "reserved");
|
|
29615
31670
|
try {
|
|
29616
|
-
if (
|
|
31671
|
+
if (existsSync18(reservedDir)) {
|
|
29617
31672
|
const files = readdirSync11(reservedDir).filter((f) => f.endsWith(".queue-data"));
|
|
29618
31673
|
for (const file of files) {
|
|
29619
|
-
unlinkSync7(
|
|
31674
|
+
unlinkSync7(join23(reservedDir, file));
|
|
29620
31675
|
count++;
|
|
29621
31676
|
}
|
|
29622
31677
|
}
|
|
@@ -29639,7 +31694,7 @@ var init_liteBackend = __esm({
|
|
|
29639
31694
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
29640
31695
|
for (const file of files) {
|
|
29641
31696
|
try {
|
|
29642
|
-
const job = JSON.parse(readFileSync16(
|
|
31697
|
+
const job = JSON.parse(readFileSync16(join23(dir, file), "utf-8"));
|
|
29643
31698
|
const attempts = job.attempts || 0;
|
|
29644
31699
|
if (attempts > 0 && attempts < maxRetries) {
|
|
29645
31700
|
results.push(job);
|
|
@@ -29662,9 +31717,9 @@ var init_liteBackend = __esm({
|
|
|
29662
31717
|
try {
|
|
29663
31718
|
const queues = readdirSync11(this.basePath);
|
|
29664
31719
|
for (const q of queues) {
|
|
29665
|
-
const failedDir =
|
|
29666
|
-
const filePath =
|
|
29667
|
-
if (
|
|
31720
|
+
const failedDir = join23(this.basePath, q, "failed");
|
|
31721
|
+
const filePath = join23(failedDir, `${jobId}.queue-data`);
|
|
31722
|
+
if (existsSync18(filePath)) {
|
|
29668
31723
|
const job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
29669
31724
|
job.status = "pending";
|
|
29670
31725
|
job.attempts = (job.attempts || 0) + 1;
|
|
@@ -29672,8 +31727,8 @@ var init_liteBackend = __esm({
|
|
|
29672
31727
|
job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29673
31728
|
job.delayUntil = delaySeconds ? new Date(Date.now() + delaySeconds * 1e3).toISOString() : null;
|
|
29674
31729
|
const prefix = this.nextPrefix();
|
|
29675
|
-
const queueDir =
|
|
29676
|
-
|
|
31730
|
+
const queueDir = join23(this.basePath, q);
|
|
31731
|
+
writeFileSync11(join23(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
|
|
29677
31732
|
unlinkSync7(filePath);
|
|
29678
31733
|
return true;
|
|
29679
31734
|
}
|
|
@@ -29689,7 +31744,7 @@ var init_liteBackend = __esm({
|
|
|
29689
31744
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
29690
31745
|
for (const file of files) {
|
|
29691
31746
|
try {
|
|
29692
|
-
const job = JSON.parse(readFileSync16(
|
|
31747
|
+
const job = JSON.parse(readFileSync16(join23(failedDir, file), "utf-8"));
|
|
29693
31748
|
if ((job.attempts || 0) >= maxRetries) {
|
|
29694
31749
|
job.status = "dead";
|
|
29695
31750
|
results.push(job);
|
|
@@ -29710,7 +31765,7 @@ var init_liteBackend = __esm({
|
|
|
29710
31765
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
29711
31766
|
for (const file of files) {
|
|
29712
31767
|
try {
|
|
29713
|
-
unlinkSync7(
|
|
31768
|
+
unlinkSync7(join23(failedDir, file));
|
|
29714
31769
|
count++;
|
|
29715
31770
|
} catch {
|
|
29716
31771
|
}
|
|
@@ -29723,9 +31778,9 @@ var init_liteBackend = __esm({
|
|
|
29723
31778
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
|
|
29724
31779
|
for (const file of files) {
|
|
29725
31780
|
try {
|
|
29726
|
-
const job = JSON.parse(readFileSync16(
|
|
31781
|
+
const job = JSON.parse(readFileSync16(join23(dir, file), "utf-8"));
|
|
29727
31782
|
if (job.status === status2) {
|
|
29728
|
-
unlinkSync7(
|
|
31783
|
+
unlinkSync7(join23(dir, file));
|
|
29729
31784
|
count++;
|
|
29730
31785
|
}
|
|
29731
31786
|
} catch {
|
|
@@ -29749,7 +31804,7 @@ var init_liteBackend = __esm({
|
|
|
29749
31804
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data"));
|
|
29750
31805
|
for (const file of files) {
|
|
29751
31806
|
try {
|
|
29752
|
-
const filePath =
|
|
31807
|
+
const filePath = join23(failedDir, file);
|
|
29753
31808
|
const job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
29754
31809
|
if ((job.attempts || 0) >= maxRetries) {
|
|
29755
31810
|
continue;
|
|
@@ -29759,7 +31814,7 @@ var init_liteBackend = __esm({
|
|
|
29759
31814
|
job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29760
31815
|
job.delayUntil = null;
|
|
29761
31816
|
const prefix = this.nextPrefix();
|
|
29762
|
-
|
|
31817
|
+
writeFileSync11(join23(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
|
|
29763
31818
|
unlinkSync7(filePath);
|
|
29764
31819
|
count++;
|
|
29765
31820
|
} catch {
|
|
@@ -29779,7 +31834,7 @@ var init_liteBackend = __esm({
|
|
|
29779
31834
|
}
|
|
29780
31835
|
for (const file of files) {
|
|
29781
31836
|
if (!file.includes(id)) continue;
|
|
29782
|
-
const filePath =
|
|
31837
|
+
const filePath = join23(dir, file);
|
|
29783
31838
|
let job;
|
|
29784
31839
|
try {
|
|
29785
31840
|
job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
@@ -29827,7 +31882,7 @@ var init_liteBackend = __esm({
|
|
|
29827
31882
|
error
|
|
29828
31883
|
};
|
|
29829
31884
|
const prefix = this.nextPrefix();
|
|
29830
|
-
|
|
31885
|
+
writeFileSync11(join23(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
|
|
29831
31886
|
}
|
|
29832
31887
|
/**
|
|
29833
31888
|
* Move the job to the dead-letter (failed/) directory. Terminal until a
|
|
@@ -29847,7 +31902,7 @@ var init_liteBackend = __esm({
|
|
|
29847
31902
|
error,
|
|
29848
31903
|
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
29849
31904
|
};
|
|
29850
|
-
|
|
31905
|
+
writeFileSync11(join23(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
|
|
29851
31906
|
}
|
|
29852
31907
|
/**
|
|
29853
31908
|
* Record a failed attempt.
|
|
@@ -29883,7 +31938,7 @@ var init_liteBackend = __esm({
|
|
|
29883
31938
|
retryJob(queue, job, delaySeconds) {
|
|
29884
31939
|
this.clearReservation(queue, job.id);
|
|
29885
31940
|
try {
|
|
29886
|
-
unlinkSync7(
|
|
31941
|
+
unlinkSync7(join23(this.ensureFailedDir(queue), `${job.id}.queue-data`));
|
|
29887
31942
|
} catch {
|
|
29888
31943
|
}
|
|
29889
31944
|
job.attempts = (job.attempts || 0) + 1;
|
|
@@ -30302,7 +32357,7 @@ var init_queue = __esm({
|
|
|
30302
32357
|
const jobs = this.popBatch(resolvedBatchSize);
|
|
30303
32358
|
if (jobs.length === 0) {
|
|
30304
32359
|
if (resolvedPollInterval <= 0) break;
|
|
30305
|
-
await new Promise((
|
|
32360
|
+
await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
|
|
30306
32361
|
continue;
|
|
30307
32362
|
}
|
|
30308
32363
|
yield jobs;
|
|
@@ -30312,7 +32367,7 @@ var init_queue = __esm({
|
|
|
30312
32367
|
const raw = this.pop();
|
|
30313
32368
|
if (raw === null) {
|
|
30314
32369
|
if (resolvedPollInterval <= 0) break;
|
|
30315
|
-
await new Promise((
|
|
32370
|
+
await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
|
|
30316
32371
|
continue;
|
|
30317
32372
|
}
|
|
30318
32373
|
yield createJob(raw, this);
|
|
@@ -32387,8 +34442,8 @@ ${end}
|
|
|
32387
34442
|
|
|
32388
34443
|
// src/devAdmin.ts
|
|
32389
34444
|
import { cpus as osCpus } from "node:os";
|
|
32390
|
-
import { readFileSync as readFileSync20, writeFileSync as
|
|
32391
|
-
import { join as
|
|
34445
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync15, existsSync as existsSync22, readdirSync as readdirSync15, mkdirSync as mkdirSync18, copyFileSync, statSync as statSync16 } from "node:fs";
|
|
34446
|
+
import { join as join27, dirname as dirname12, resolve as resolve16, relative as relative8 } from "node:path";
|
|
32392
34447
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
32393
34448
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
32394
34449
|
function escapeHtml(value) {
|
|
@@ -32502,12 +34557,12 @@ function mapQueueJob(job, topic, status2) {
|
|
|
32502
34557
|
};
|
|
32503
34558
|
}
|
|
32504
34559
|
function readQueueDir(dir, topic, status2) {
|
|
32505
|
-
if (!
|
|
34560
|
+
if (!existsSync22(dir)) return [];
|
|
32506
34561
|
const jobs = [];
|
|
32507
34562
|
for (const filename of readdirSync15(dir).sort()) {
|
|
32508
34563
|
if (!filename.endsWith(".queue-data")) continue;
|
|
32509
34564
|
try {
|
|
32510
|
-
jobs.push(mapQueueJob(JSON.parse(readFileSync20(
|
|
34565
|
+
jobs.push(mapQueueJob(JSON.parse(readFileSync20(join27(dir, filename), "utf-8")), topic, status2));
|
|
32511
34566
|
} catch {
|
|
32512
34567
|
}
|
|
32513
34568
|
}
|
|
@@ -32624,8 +34679,8 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
|
|
|
32624
34679
|
function resolveDevEnvVar(key) {
|
|
32625
34680
|
const live = process.env[key];
|
|
32626
34681
|
if (live !== void 0 && live !== "") return live;
|
|
32627
|
-
const envPath =
|
|
32628
|
-
if (!
|
|
34682
|
+
const envPath = join27(process.cwd(), ".env");
|
|
34683
|
+
if (!existsSync22(envPath)) return "";
|
|
32629
34684
|
for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
|
|
32630
34685
|
const t = line.trim();
|
|
32631
34686
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
@@ -32635,8 +34690,8 @@ function resolveDevEnvVar(key) {
|
|
|
32635
34690
|
return "";
|
|
32636
34691
|
}
|
|
32637
34692
|
function upsertDevEnvVar(key, value) {
|
|
32638
|
-
const envPath =
|
|
32639
|
-
const lines =
|
|
34693
|
+
const envPath = join27(process.cwd(), ".env");
|
|
34694
|
+
const lines = existsSync22(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
|
|
32640
34695
|
let found = false;
|
|
32641
34696
|
const out = [];
|
|
32642
34697
|
for (const line of lines) {
|
|
@@ -32651,7 +34706,7 @@ function upsertDevEnvVar(key, value) {
|
|
|
32651
34706
|
} else out.push(line);
|
|
32652
34707
|
}
|
|
32653
34708
|
if (!found) out.push(`${key}=${value}`);
|
|
32654
|
-
|
|
34709
|
+
writeFileSync15(envPath, out.join("\n").replace(/\n+$/, "") + "\n");
|
|
32655
34710
|
}
|
|
32656
34711
|
function formatUptime(seconds) {
|
|
32657
34712
|
const d = Math.floor(seconds / 86400);
|
|
@@ -32666,9 +34721,9 @@ function formatUptime(seconds) {
|
|
|
32666
34721
|
return parts.join(" ");
|
|
32667
34722
|
}
|
|
32668
34723
|
function parseEnvFile() {
|
|
32669
|
-
const envPath =
|
|
34724
|
+
const envPath = join27(process.cwd(), ".env");
|
|
32670
34725
|
const result = {};
|
|
32671
|
-
if (!
|
|
34726
|
+
if (!existsSync22(envPath)) return result;
|
|
32672
34727
|
const lines = readFileSync20(envPath, "utf-8").split("\n");
|
|
32673
34728
|
for (const line of lines) {
|
|
32674
34729
|
const trimmed = line.trim();
|
|
@@ -32680,9 +34735,9 @@ function parseEnvFile() {
|
|
|
32680
34735
|
}
|
|
32681
34736
|
function walkDirRecursive(dir) {
|
|
32682
34737
|
const results = [];
|
|
32683
|
-
if (!
|
|
34738
|
+
if (!existsSync22(dir)) return results;
|
|
32684
34739
|
for (const entry of readdirSync15(dir)) {
|
|
32685
|
-
const full =
|
|
34740
|
+
const full = join27(dir, entry);
|
|
32686
34741
|
if (statSync16(full).isDirectory()) {
|
|
32687
34742
|
results.push(...walkDirRecursive(full));
|
|
32688
34743
|
} else {
|
|
@@ -32699,25 +34754,25 @@ function handleGalleryDeploy(router) {
|
|
|
32699
34754
|
res.json({ error: "No gallery item specified" }, 400);
|
|
32700
34755
|
return;
|
|
32701
34756
|
}
|
|
32702
|
-
const galleryDir =
|
|
32703
|
-
const gallerySrc =
|
|
32704
|
-
if (!
|
|
34757
|
+
const galleryDir = resolve16(__devAdminDirname, "..", "gallery");
|
|
34758
|
+
const gallerySrc = join27(galleryDir, name, "src");
|
|
34759
|
+
if (!existsSync22(gallerySrc)) {
|
|
32705
34760
|
res.json({ error: `Gallery item '${name}' not found` }, 404);
|
|
32706
34761
|
return;
|
|
32707
34762
|
}
|
|
32708
|
-
const projectSrc =
|
|
34763
|
+
const projectSrc = resolve16(process.cwd(), "src");
|
|
32709
34764
|
const copied = [];
|
|
32710
34765
|
const allFiles = walkDirRecursive(gallerySrc);
|
|
32711
34766
|
for (const srcFile of allFiles) {
|
|
32712
|
-
const rel =
|
|
32713
|
-
const dest =
|
|
32714
|
-
|
|
34767
|
+
const rel = relative8(gallerySrc, srcFile);
|
|
34768
|
+
const dest = join27(projectSrc, rel);
|
|
34769
|
+
mkdirSync18(dirname12(dest), { recursive: true });
|
|
32715
34770
|
copyFileSync(srcFile, dest);
|
|
32716
34771
|
copied.push(rel);
|
|
32717
34772
|
}
|
|
32718
34773
|
try {
|
|
32719
|
-
const routesDir =
|
|
32720
|
-
if (
|
|
34774
|
+
const routesDir = resolve16(process.cwd(), "src", "routes");
|
|
34775
|
+
if (existsSync22(routesDir)) {
|
|
32721
34776
|
const { discoverRoutes: discoverRoutes2 } = await Promise.resolve().then(() => (init_routeDiscovery(), routeDiscovery_exports));
|
|
32722
34777
|
const routes = await discoverRoutes2(routesDir);
|
|
32723
34778
|
for (const route of routes) {
|
|
@@ -32733,7 +34788,7 @@ function handleGalleryDeploy(router) {
|
|
|
32733
34788
|
};
|
|
32734
34789
|
}
|
|
32735
34790
|
function safeJoin(projectRoot3, rel) {
|
|
32736
|
-
const resolved =
|
|
34791
|
+
const resolved = resolve16(projectRoot3, rel);
|
|
32737
34792
|
if (!resolved.startsWith(projectRoot3)) return null;
|
|
32738
34793
|
return resolved;
|
|
32739
34794
|
}
|
|
@@ -33696,16 +35751,16 @@ var init_devAdmin = __esm({
|
|
|
33696
35751
|
failed: queue.size("failed"),
|
|
33697
35752
|
reserved: queue.size("reserved")
|
|
33698
35753
|
};
|
|
33699
|
-
const topicDir =
|
|
35754
|
+
const topicDir = join27(queueBasePath2(), topic);
|
|
33700
35755
|
const jobs = [];
|
|
33701
35756
|
if (!statusFilter || statusFilter === "pending") {
|
|
33702
35757
|
jobs.push(...readQueueDir(topicDir, topic, "pending"));
|
|
33703
35758
|
}
|
|
33704
35759
|
if (!statusFilter || statusFilter === "reserved") {
|
|
33705
|
-
jobs.push(...readQueueDir(
|
|
35760
|
+
jobs.push(...readQueueDir(join27(topicDir, "reserved"), topic, "reserved"));
|
|
33706
35761
|
}
|
|
33707
35762
|
if (!statusFilter || statusFilter === "failed" || statusFilter === "dead") {
|
|
33708
|
-
jobs.push(...readQueueDir(
|
|
35763
|
+
jobs.push(...readQueueDir(join27(topicDir, "failed"), topic, "dead_letter"));
|
|
33709
35764
|
}
|
|
33710
35765
|
res.json({ stats, jobs });
|
|
33711
35766
|
} catch (e) {
|
|
@@ -33721,10 +35776,10 @@ var init_devAdmin = __esm({
|
|
|
33721
35776
|
const { queueBasePath: queueBasePath2 } = await Promise.resolve().then(() => (init_queue(), queue_exports));
|
|
33722
35777
|
const queueDir = queueBasePath2();
|
|
33723
35778
|
let topics = [];
|
|
33724
|
-
if (
|
|
35779
|
+
if (existsSync22(queueDir)) {
|
|
33725
35780
|
topics = readdirSync15(queueDir).filter((d) => {
|
|
33726
35781
|
try {
|
|
33727
|
-
return statSync16(
|
|
35782
|
+
return statSync16(join27(queueDir, d)).isDirectory();
|
|
33728
35783
|
} catch {
|
|
33729
35784
|
return false;
|
|
33730
35785
|
}
|
|
@@ -34039,7 +36094,7 @@ var init_devAdmin = __esm({
|
|
|
34039
36094
|
const count = parseInt(String(body.count ?? "10"), 10) || 10;
|
|
34040
36095
|
try {
|
|
34041
36096
|
const orm = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
34042
|
-
const dirs = ["src/orm", "src/models"].map((d) =>
|
|
36097
|
+
const dirs = ["src/orm", "src/models"].map((d) => resolve16(process.cwd(), d)).filter((d) => existsSync22(d));
|
|
34043
36098
|
const classes = [];
|
|
34044
36099
|
for (const dir of dirs) {
|
|
34045
36100
|
for (const m of await orm.discoverModels(dir)) classes.push(m.modelClass);
|
|
@@ -34066,7 +36121,7 @@ var init_devAdmin = __esm({
|
|
|
34066
36121
|
const run = promisify(execFile);
|
|
34067
36122
|
try {
|
|
34068
36123
|
const { stdout, stderr } = await run("npm", ["test"], {
|
|
34069
|
-
cwd:
|
|
36124
|
+
cwd: resolve16(process.cwd()),
|
|
34070
36125
|
timeout: 18e4,
|
|
34071
36126
|
encoding: "utf-8",
|
|
34072
36127
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -34180,8 +36235,8 @@ var init_devAdmin = __esm({
|
|
|
34180
36235
|
return;
|
|
34181
36236
|
}
|
|
34182
36237
|
try {
|
|
34183
|
-
const envPath =
|
|
34184
|
-
const lines =
|
|
36238
|
+
const envPath = join27(process.cwd(), ".env");
|
|
36239
|
+
const lines = existsSync22(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
|
|
34185
36240
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
34186
36241
|
const newLines = [];
|
|
34187
36242
|
for (const line of lines) {
|
|
@@ -34208,7 +36263,7 @@ var init_devAdmin = __esm({
|
|
|
34208
36263
|
for (const [key, found] of Object.entries(keysFound)) {
|
|
34209
36264
|
if (!found) newLines.push(`${key}=${values[key]}`);
|
|
34210
36265
|
}
|
|
34211
|
-
|
|
36266
|
+
writeFileSync15(envPath, newLines.join("\n") + "\n");
|
|
34212
36267
|
res.json({ success: true });
|
|
34213
36268
|
} catch (e) {
|
|
34214
36269
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -34218,26 +36273,26 @@ var init_devAdmin = __esm({
|
|
|
34218
36273
|
__devAdminFilename = fileURLToPath5(import.meta.url);
|
|
34219
36274
|
__devAdminDirname = dirname12(__devAdminFilename);
|
|
34220
36275
|
handleGalleryList = (_req, res) => {
|
|
34221
|
-
const galleryDir =
|
|
36276
|
+
const galleryDir = resolve16(__devAdminDirname, "..", "gallery");
|
|
34222
36277
|
const items = [];
|
|
34223
|
-
if (
|
|
36278
|
+
if (existsSync22(galleryDir)) {
|
|
34224
36279
|
const entries = readdirSync15(galleryDir).sort();
|
|
34225
36280
|
for (const entry of entries) {
|
|
34226
|
-
const entryPath =
|
|
34227
|
-
const metaFile =
|
|
34228
|
-
if (statSync16(entryPath).isDirectory() &&
|
|
36281
|
+
const entryPath = join27(galleryDir, entry);
|
|
36282
|
+
const metaFile = join27(entryPath, "meta.json");
|
|
36283
|
+
if (statSync16(entryPath).isDirectory() && existsSync22(metaFile)) {
|
|
34229
36284
|
try {
|
|
34230
36285
|
const meta = JSON.parse(readFileSync20(metaFile, "utf-8"));
|
|
34231
36286
|
meta.id = entry;
|
|
34232
|
-
const srcDir =
|
|
34233
|
-
if (
|
|
36287
|
+
const srcDir = join27(entryPath, "src");
|
|
36288
|
+
if (existsSync22(srcDir)) {
|
|
34234
36289
|
const allFiles = walkDirRecursive(srcDir);
|
|
34235
|
-
meta.files = allFiles.map((f) =>
|
|
36290
|
+
meta.files = allFiles.map((f) => relative8(srcDir, f));
|
|
34236
36291
|
}
|
|
34237
|
-
const projectSrc =
|
|
34238
|
-
if (
|
|
36292
|
+
const projectSrc = resolve16(process.cwd(), "src");
|
|
36293
|
+
if (existsSync22(srcDir) && meta.files) {
|
|
34239
36294
|
meta.deployed = meta.files.every(
|
|
34240
|
-
(f) =>
|
|
36295
|
+
(f) => existsSync22(join27(projectSrc, f))
|
|
34241
36296
|
);
|
|
34242
36297
|
} else {
|
|
34243
36298
|
meta.deployed = false;
|
|
@@ -34333,10 +36388,10 @@ var init_devAdmin = __esm({
|
|
|
34333
36388
|
handleFiles = async (req2, res) => {
|
|
34334
36389
|
const url = new URL(req2.url ?? "/", "http://localhost");
|
|
34335
36390
|
const rel = url.searchParams.get("path") ?? ".";
|
|
34336
|
-
const root =
|
|
36391
|
+
const root = resolve16(process.cwd());
|
|
34337
36392
|
const target = safeJoin(root, rel);
|
|
34338
36393
|
const { branch, gitRoot, status: gitStatus } = await devGitInfo(root);
|
|
34339
|
-
if (!target || !
|
|
36394
|
+
if (!target || !existsSync22(target) || !statSync16(target).isDirectory()) {
|
|
34340
36395
|
res.json({ path: rel, branch, entries: [], error: "not a directory" });
|
|
34341
36396
|
return;
|
|
34342
36397
|
}
|
|
@@ -34349,8 +36404,8 @@ var init_devAdmin = __esm({
|
|
|
34349
36404
|
const entries = [];
|
|
34350
36405
|
for (const name of readdirSync15(target).sort()) {
|
|
34351
36406
|
if (devFilesHidden(name)) continue;
|
|
34352
|
-
const full =
|
|
34353
|
-
const entryRel =
|
|
36407
|
+
const full = join27(target, name);
|
|
36408
|
+
const entryRel = relative8(root, full).replace(/\\/g, "/");
|
|
34354
36409
|
if (isSecretPath(entryRel)) continue;
|
|
34355
36410
|
let isDir = false;
|
|
34356
36411
|
let size = null;
|
|
@@ -34395,7 +36450,7 @@ var init_devAdmin = __esm({
|
|
|
34395
36450
|
size
|
|
34396
36451
|
});
|
|
34397
36452
|
}
|
|
34398
|
-
res.json({ path:
|
|
36453
|
+
res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
|
|
34399
36454
|
};
|
|
34400
36455
|
DEV_ADMIN_LANG_MAP = {
|
|
34401
36456
|
".py": "python",
|
|
@@ -34440,15 +36495,15 @@ var init_devAdmin = __esm({
|
|
|
34440
36495
|
res.json({ error: "Refused: secret file", path: rel, content: "", language: "text", bytes: 0 }, 403);
|
|
34441
36496
|
return;
|
|
34442
36497
|
}
|
|
34443
|
-
const root =
|
|
36498
|
+
const root = resolve16(process.cwd());
|
|
34444
36499
|
const target = safeJoin(root, rel);
|
|
34445
|
-
if (!target || !
|
|
36500
|
+
if (!target || !existsSync22(target) || !statSync16(target).isFile()) {
|
|
34446
36501
|
res.json({ error: `File not found: ${rel}` }, 404);
|
|
34447
36502
|
return;
|
|
34448
36503
|
}
|
|
34449
36504
|
try {
|
|
34450
36505
|
const content = readFileSync20(target, "utf-8");
|
|
34451
|
-
const path8 =
|
|
36506
|
+
const path8 = relative8(root, target);
|
|
34452
36507
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
34453
36508
|
} catch (e) {
|
|
34454
36509
|
res.json({ error: e.message }, 500);
|
|
@@ -34458,22 +36513,22 @@ var init_devAdmin = __esm({
|
|
|
34458
36513
|
const body = req2.body || {};
|
|
34459
36514
|
const rel = body.path || "";
|
|
34460
36515
|
const content = body.content ?? "";
|
|
34461
|
-
const root =
|
|
36516
|
+
const root = resolve16(process.cwd());
|
|
34462
36517
|
const target = safeJoin(root, rel);
|
|
34463
36518
|
if (!target) {
|
|
34464
36519
|
res.json({ error: `Path escapes project directory: ${rel}` }, 400);
|
|
34465
36520
|
return;
|
|
34466
36521
|
}
|
|
34467
36522
|
try {
|
|
34468
|
-
|
|
34469
|
-
const existed =
|
|
34470
|
-
|
|
36523
|
+
mkdirSync18(dirname12(target), { recursive: true });
|
|
36524
|
+
const existed = existsSync22(target);
|
|
36525
|
+
writeFileSync15(target, content, "utf-8");
|
|
34471
36526
|
try {
|
|
34472
36527
|
const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
34473
|
-
Plan2.recordAction(existed ? "patched" : "created",
|
|
36528
|
+
Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
|
|
34474
36529
|
} catch {
|
|
34475
36530
|
}
|
|
34476
|
-
res.json({ ok: true, path:
|
|
36531
|
+
res.json({ ok: true, path: relative8(root, target), bytes: Buffer.byteLength(content, "utf-8") });
|
|
34477
36532
|
} catch (e) {
|
|
34478
36533
|
res.json({ error: e.message }, 500);
|
|
34479
36534
|
}
|
|
@@ -34485,9 +36540,9 @@ var init_devAdmin = __esm({
|
|
|
34485
36540
|
res.json({ error: "Refused: secret file" }, 403);
|
|
34486
36541
|
return;
|
|
34487
36542
|
}
|
|
34488
|
-
const root =
|
|
36543
|
+
const root = resolve16(process.cwd());
|
|
34489
36544
|
const target = safeJoin(root, rel);
|
|
34490
|
-
if (!target || !
|
|
36545
|
+
if (!target || !existsSync22(target) || !statSync16(target).isFile()) {
|
|
34491
36546
|
res.raw.writeHead(404);
|
|
34492
36547
|
res.raw.end("Not found");
|
|
34493
36548
|
return;
|
|
@@ -34520,22 +36575,22 @@ var init_devAdmin = __esm({
|
|
|
34520
36575
|
const body = req2.body || {};
|
|
34521
36576
|
const from = body.from || "";
|
|
34522
36577
|
const to = body.to || "";
|
|
34523
|
-
const root =
|
|
36578
|
+
const root = resolve16(process.cwd());
|
|
34524
36579
|
const src = safeJoin(root, from);
|
|
34525
36580
|
const dst = safeJoin(root, to);
|
|
34526
36581
|
if (!src || !dst) {
|
|
34527
36582
|
res.json({ error: "Invalid path" }, 400);
|
|
34528
36583
|
return;
|
|
34529
36584
|
}
|
|
34530
|
-
if (!
|
|
36585
|
+
if (!existsSync22(src)) {
|
|
34531
36586
|
res.json({ error: `Source not found: ${from}` }, 404);
|
|
34532
36587
|
return;
|
|
34533
36588
|
}
|
|
34534
36589
|
try {
|
|
34535
36590
|
const { renameSync: renameSync3 } = await import("node:fs");
|
|
34536
|
-
|
|
36591
|
+
mkdirSync18(dirname12(dst), { recursive: true });
|
|
34537
36592
|
renameSync3(src, dst);
|
|
34538
|
-
res.json({ ok: true, from:
|
|
36593
|
+
res.json({ ok: true, from: relative8(root, src), to: relative8(root, dst) });
|
|
34539
36594
|
} catch (e) {
|
|
34540
36595
|
res.json({ error: e.message }, 500);
|
|
34541
36596
|
}
|
|
@@ -34543,20 +36598,20 @@ var init_devAdmin = __esm({
|
|
|
34543
36598
|
handleFileDelete = async (req2, res) => {
|
|
34544
36599
|
const body = req2.body || {};
|
|
34545
36600
|
const rel = body.path || "";
|
|
34546
|
-
const root =
|
|
36601
|
+
const root = resolve16(process.cwd());
|
|
34547
36602
|
const target = safeJoin(root, rel);
|
|
34548
36603
|
if (!target) {
|
|
34549
36604
|
res.json({ error: "Invalid path" }, 400);
|
|
34550
36605
|
return;
|
|
34551
36606
|
}
|
|
34552
|
-
if (!
|
|
36607
|
+
if (!existsSync22(target)) {
|
|
34553
36608
|
res.json({ error: `Not found: ${rel}` }, 404);
|
|
34554
36609
|
return;
|
|
34555
36610
|
}
|
|
34556
36611
|
try {
|
|
34557
36612
|
const { rmSync } = await import("node:fs");
|
|
34558
36613
|
rmSync(target, { recursive: true, force: true });
|
|
34559
|
-
res.json({ ok: true, deleted:
|
|
36614
|
+
res.json({ ok: true, deleted: relative8(root, target) });
|
|
34560
36615
|
} catch (e) {
|
|
34561
36616
|
res.json({ error: e.message }, 500);
|
|
34562
36617
|
}
|
|
@@ -34594,7 +36649,7 @@ var init_devAdmin = __esm({
|
|
|
34594
36649
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
34595
36650
|
const args = ["install", dev ? "--save-dev" : "--save", pkg];
|
|
34596
36651
|
const output = execFileSync7("npm", args, {
|
|
34597
|
-
cwd:
|
|
36652
|
+
cwd: resolve16(process.cwd()),
|
|
34598
36653
|
timeout: 12e4,
|
|
34599
36654
|
encoding: "utf-8"
|
|
34600
36655
|
}).toString();
|
|
@@ -34606,7 +36661,7 @@ var init_devAdmin = __esm({
|
|
|
34606
36661
|
handleGitStatus = async (_req, res) => {
|
|
34607
36662
|
try {
|
|
34608
36663
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
34609
|
-
const cwd =
|
|
36664
|
+
const cwd = resolve16(process.cwd());
|
|
34610
36665
|
try {
|
|
34611
36666
|
execFileSync7("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 3e3 });
|
|
34612
36667
|
} catch {
|
|
@@ -34745,7 +36800,7 @@ var init_devAdmin = __esm({
|
|
|
34745
36800
|
try {
|
|
34746
36801
|
const { execFileSync: execFileSync7 } = await import("node:child_process");
|
|
34747
36802
|
const output = execFileSync7("npx", ["tina4nodejs", "generate", kind, name], {
|
|
34748
|
-
cwd:
|
|
36803
|
+
cwd: resolve16(process.cwd()),
|
|
34749
36804
|
timeout: 3e4,
|
|
34750
36805
|
encoding: "utf-8"
|
|
34751
36806
|
}).toString();
|
|
@@ -34890,21 +36945,21 @@ var init_devAdmin = __esm({
|
|
|
34890
36945
|
});
|
|
34891
36946
|
};
|
|
34892
36947
|
handleDevAdminJs = async (_req, res) => {
|
|
34893
|
-
const { readFileSync: readFileSync27, existsSync:
|
|
34894
|
-
const { dirname: dirname15, join:
|
|
36948
|
+
const { readFileSync: readFileSync27, existsSync: existsSync28 } = await import("node:fs");
|
|
36949
|
+
const { dirname: dirname15, join: join33, resolve: resolve21 } = await import("node:path");
|
|
34895
36950
|
const { fileURLToPath: fileURLToPath8 } = await import("node:url");
|
|
34896
36951
|
const dir = dirname15(fileURLToPath8(import.meta.url));
|
|
34897
36952
|
const candidates = [
|
|
34898
|
-
|
|
36953
|
+
join33(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
|
|
34899
36954
|
// src/../public/js/
|
|
34900
|
-
|
|
36955
|
+
join33(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
|
|
34901
36956
|
// deeper nesting
|
|
34902
|
-
|
|
34903
|
-
|
|
36957
|
+
resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
|
|
36958
|
+
resolve21(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
|
|
34904
36959
|
// project public/
|
|
34905
36960
|
];
|
|
34906
36961
|
for (const jsPath of candidates) {
|
|
34907
|
-
if (
|
|
36962
|
+
if (existsSync28(jsPath)) {
|
|
34908
36963
|
try {
|
|
34909
36964
|
const content = readFileSync27(jsPath, "utf-8");
|
|
34910
36965
|
res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
@@ -34929,8 +36984,8 @@ var init_devAdmin = __esm({
|
|
|
34929
36984
|
});
|
|
34930
36985
|
|
|
34931
36986
|
// src/i18n.ts
|
|
34932
|
-
import { readFileSync as readFileSync21, readdirSync as readdirSync16, existsSync as
|
|
34933
|
-
import { join as
|
|
36987
|
+
import { readFileSync as readFileSync21, readdirSync as readdirSync16, existsSync as existsSync23 } from "node:fs";
|
|
36988
|
+
import { join as join28, resolve as resolve17 } from "node:path";
|
|
34934
36989
|
var I18n;
|
|
34935
36990
|
var init_i18n = __esm({
|
|
34936
36991
|
"src/i18n.ts"() {
|
|
@@ -34950,7 +37005,7 @@ var init_i18n = __esm({
|
|
|
34950
37005
|
* (BUG-7, BREAKING in 3.13.x — was previously (localeDir, defaultLocale)).
|
|
34951
37006
|
*/
|
|
34952
37007
|
constructor(locale, path8) {
|
|
34953
|
-
this._localeDir =
|
|
37008
|
+
this._localeDir = resolve17(
|
|
34954
37009
|
path8 ?? process.env.TINA4_LOCALE_DIR ?? "src/locales"
|
|
34955
37010
|
);
|
|
34956
37011
|
this._defaultLocale = locale ?? process.env.TINA4_LOCALE ?? "en";
|
|
@@ -35007,7 +37062,7 @@ var init_i18n = __esm({
|
|
|
35007
37062
|
}
|
|
35008
37063
|
/** List available locale codes based on JSON files in the locale directory. */
|
|
35009
37064
|
availableLocales() {
|
|
35010
|
-
if (!
|
|
37065
|
+
if (!existsSync23(this._localeDir)) {
|
|
35011
37066
|
return [this._defaultLocale];
|
|
35012
37067
|
}
|
|
35013
37068
|
try {
|
|
@@ -35023,8 +37078,8 @@ var init_i18n = __esm({
|
|
|
35023
37078
|
if (this._translations.has(locale)) {
|
|
35024
37079
|
return;
|
|
35025
37080
|
}
|
|
35026
|
-
const filePath =
|
|
35027
|
-
if (
|
|
37081
|
+
const filePath = join28(this._localeDir, `${locale}.json`);
|
|
37082
|
+
if (existsSync23(filePath)) {
|
|
35028
37083
|
try {
|
|
35029
37084
|
const raw = readFileSync21(filePath, "utf-8");
|
|
35030
37085
|
const data = JSON.parse(raw);
|
|
@@ -35036,8 +37091,8 @@ var init_i18n = __esm({
|
|
|
35036
37091
|
}
|
|
35037
37092
|
}
|
|
35038
37093
|
for (const ext of [".yml", ".yaml"]) {
|
|
35039
|
-
const yamlPath =
|
|
35040
|
-
if (
|
|
37094
|
+
const yamlPath = join28(this._localeDir, `${locale}${ext}`);
|
|
37095
|
+
if (existsSync23(yamlPath)) {
|
|
35041
37096
|
try {
|
|
35042
37097
|
const raw = readFileSync21(yamlPath, "utf-8");
|
|
35043
37098
|
const data = _I18n._parseSimpleYaml(raw);
|
|
@@ -35382,7 +37437,7 @@ function sanitizeSecurity(reqs, schemes) {
|
|
|
35382
37437
|
return clean;
|
|
35383
37438
|
});
|
|
35384
37439
|
}
|
|
35385
|
-
function
|
|
37440
|
+
function generate2(routes, models = []) {
|
|
35386
37441
|
const info = {
|
|
35387
37442
|
title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
|
|
35388
37443
|
// The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
|
|
@@ -35830,7 +37885,7 @@ __export(src_exports2, {
|
|
|
35830
37885
|
addSchema: () => addSchema,
|
|
35831
37886
|
addSecurityScheme: () => addSecurityScheme,
|
|
35832
37887
|
createSwaggerRoutes: () => createSwaggerRoutes,
|
|
35833
|
-
generate: () =>
|
|
37888
|
+
generate: () => generate2,
|
|
35834
37889
|
resetRegistry: () => resetRegistry,
|
|
35835
37890
|
swaggerEnabled: () => swaggerEnabled
|
|
35836
37891
|
});
|
|
@@ -36184,8 +38239,8 @@ function writeMcpDiscovery(projectRoot3, port) {
|
|
|
36184
38239
|
const lines = contents.split(/\r?\n/);
|
|
36185
38240
|
const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
|
|
36186
38241
|
if (!already) {
|
|
36187
|
-
const
|
|
36188
|
-
fs8.writeFileSync(gitignorePath, `${contents}${
|
|
38242
|
+
const sep7 = contents.endsWith("\n") || contents === "" ? "" : "\n";
|
|
38243
|
+
fs8.writeFileSync(gitignorePath, `${contents}${sep7}${GITIGNORE_LINE}
|
|
36189
38244
|
`, "utf-8");
|
|
36190
38245
|
}
|
|
36191
38246
|
}
|
|
@@ -36204,8 +38259,8 @@ var init_docsAutoDiscovery = __esm({
|
|
|
36204
38259
|
// src/server.ts
|
|
36205
38260
|
import { createServer as createServer2 } from "node:http";
|
|
36206
38261
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
36207
|
-
import { resolve as
|
|
36208
|
-
import { existsSync as
|
|
38262
|
+
import { resolve as resolve19, dirname as dirname13, join as join30, relative as relative9 } from "node:path";
|
|
38263
|
+
import { existsSync as existsSync25, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
|
|
36209
38264
|
import { isatty } from "node:tty";
|
|
36210
38265
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
36211
38266
|
import { execFileSync as execFileSync3, exec } from "node:child_process";
|
|
@@ -36243,8 +38298,8 @@ function swaggerAdvertised() {
|
|
|
36243
38298
|
return TRUTHY2.includes(raw);
|
|
36244
38299
|
}
|
|
36245
38300
|
async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
|
|
36246
|
-
const dir =
|
|
36247
|
-
if (!
|
|
38301
|
+
const dir = resolve19(base, migrationDir);
|
|
38302
|
+
if (!existsSync25(dir)) return;
|
|
36248
38303
|
let hasSql = false;
|
|
36249
38304
|
try {
|
|
36250
38305
|
hasSql = readdirSync17(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
|
|
@@ -36385,12 +38440,12 @@ async function renderErrorPage(code, data, templatesDir) {
|
|
|
36385
38440
|
}
|
|
36386
38441
|
return instance;
|
|
36387
38442
|
};
|
|
36388
|
-
const userTemplatePath =
|
|
36389
|
-
if (
|
|
38443
|
+
const userTemplatePath = join30(templatesDir, templateFile);
|
|
38444
|
+
if (existsSync25(userTemplatePath)) {
|
|
36390
38445
|
return getCachedFrond(templatesDir).render(templateFile, data);
|
|
36391
38446
|
}
|
|
36392
|
-
const builtinTemplatePath =
|
|
36393
|
-
if (
|
|
38447
|
+
const builtinTemplatePath = join30(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
|
|
38448
|
+
if (existsSync25(builtinTemplatePath)) {
|
|
36394
38449
|
return getCachedFrond(BUILTIN_ERROR_TEMPLATES_DIR).render(templateFile, data);
|
|
36395
38450
|
}
|
|
36396
38451
|
return null;
|
|
@@ -36407,29 +38462,29 @@ function injectDevToolbar(html, ctx) {
|
|
|
36407
38462
|
}
|
|
36408
38463
|
function walkGalleryFiles(dir) {
|
|
36409
38464
|
const results = [];
|
|
36410
|
-
if (!
|
|
38465
|
+
if (!existsSync25(dir)) return results;
|
|
36411
38466
|
for (const f of readdirSync17(dir)) {
|
|
36412
|
-
const full =
|
|
38467
|
+
const full = join30(dir, f);
|
|
36413
38468
|
if (statSync17(full).isDirectory()) results.push(...walkGalleryFiles(full));
|
|
36414
38469
|
else results.push(full);
|
|
36415
38470
|
}
|
|
36416
38471
|
return results;
|
|
36417
38472
|
}
|
|
36418
38473
|
function getGalleryDeployedState() {
|
|
36419
|
-
const galleryDir =
|
|
38474
|
+
const galleryDir = resolve19(__dirname, "..", "gallery");
|
|
36420
38475
|
const state = {};
|
|
36421
|
-
if (!
|
|
38476
|
+
if (!existsSync25(galleryDir)) return state;
|
|
36422
38477
|
try {
|
|
36423
38478
|
const entries = readdirSync17(galleryDir).sort();
|
|
36424
38479
|
for (const entry of entries) {
|
|
36425
|
-
const entryPath =
|
|
36426
|
-
const metaFile =
|
|
36427
|
-
if (statSync17(entryPath).isDirectory() &&
|
|
36428
|
-
const srcDir =
|
|
36429
|
-
if (
|
|
38480
|
+
const entryPath = join30(galleryDir, entry);
|
|
38481
|
+
const metaFile = join30(entryPath, "meta.json");
|
|
38482
|
+
if (statSync17(entryPath).isDirectory() && existsSync25(metaFile)) {
|
|
38483
|
+
const srcDir = join30(entryPath, "src");
|
|
38484
|
+
if (existsSync25(srcDir)) {
|
|
36430
38485
|
const files = walkGalleryFiles(srcDir);
|
|
36431
|
-
const projectSrc =
|
|
36432
|
-
state[entry] = files.every((f) =>
|
|
38486
|
+
const projectSrc = resolve19(process.cwd(), "src");
|
|
38487
|
+
state[entry] = files.every((f) => existsSync25(join30(projectSrc, relative9(srcDir, f))));
|
|
36433
38488
|
} else {
|
|
36434
38489
|
state[entry] = false;
|
|
36435
38490
|
}
|
|
@@ -36457,9 +38512,9 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
36457
38512
|
const isDev2 = (process.env.TINA4_DEBUG ?? "false").toLowerCase() === "true";
|
|
36458
38513
|
if (isDev2) {
|
|
36459
38514
|
if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
|
|
36460
|
-
const pagesDir =
|
|
38515
|
+
const pagesDir = resolve19(templatesDir, TEMPLATE_PAGES_DIR);
|
|
36461
38516
|
for (const ext of [".twig", ".html"]) {
|
|
36462
|
-
if (
|
|
38517
|
+
if (existsSync25(resolve19(pagesDir, cleanPath + ext))) {
|
|
36463
38518
|
return `${TEMPLATE_PAGES_DIR}/${cleanPath}${ext}`;
|
|
36464
38519
|
}
|
|
36465
38520
|
}
|
|
@@ -36467,14 +38522,14 @@ function resolveTemplate(pathname, templatesDir) {
|
|
|
36467
38522
|
}
|
|
36468
38523
|
if (!templateCache) {
|
|
36469
38524
|
templateCache = /* @__PURE__ */ new Map();
|
|
36470
|
-
const pagesDir =
|
|
36471
|
-
if (
|
|
38525
|
+
const pagesDir = resolve19(templatesDir, TEMPLATE_PAGES_DIR);
|
|
38526
|
+
if (existsSync25(pagesDir)) {
|
|
36472
38527
|
const scan = (dir, prefix) => {
|
|
36473
38528
|
for (const entry of readdirSync17(dir, { withFileTypes: true })) {
|
|
36474
38529
|
if (entry.name.startsWith("_")) continue;
|
|
36475
38530
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
36476
38531
|
if (entry.isDirectory()) {
|
|
36477
|
-
scan(
|
|
38532
|
+
scan(resolve19(dir, entry.name), rel);
|
|
36478
38533
|
} else if (entry.name.endsWith(".twig") || entry.name.endsWith(".html")) {
|
|
36479
38534
|
const urlPath = rel.replace(/\.(twig|html)$/, "");
|
|
36480
38535
|
if (!templateCache.has(urlPath)) {
|
|
@@ -36879,7 +38934,7 @@ function serveTemplateFallback(ctx) {
|
|
|
36879
38934
|
if ((ctx.req.method ?? "GET") !== "GET") return false;
|
|
36880
38935
|
const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
|
|
36881
38936
|
if (!tplFile) return false;
|
|
36882
|
-
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync23(
|
|
38937
|
+
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync23(resolve19(ctx.templatesDir, tplFile), "utf-8");
|
|
36883
38938
|
ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
36884
38939
|
ctx.res.raw.end(html);
|
|
36885
38940
|
return true;
|
|
@@ -36918,9 +38973,9 @@ function serveMethodNotAllowed(ctx) {
|
|
|
36918
38973
|
}
|
|
36919
38974
|
function serveStaticAsset(ctx) {
|
|
36920
38975
|
const custom = process.env.TINA4_PUBLIC_DIR;
|
|
36921
|
-
if (custom &&
|
|
36922
|
-
if (
|
|
36923
|
-
if (
|
|
38976
|
+
if (custom && existsSync25(custom) && tryServeStatic(custom, ctx.req, ctx.res)) return true;
|
|
38977
|
+
if (existsSync25(ctx.staticDir) && tryServeStatic(ctx.staticDir, ctx.req, ctx.res)) return true;
|
|
38978
|
+
if (existsSync25(ctx.srcPublicDir) && tryServeStatic(ctx.srcPublicDir, ctx.req, ctx.res)) return true;
|
|
36924
38979
|
if (ctx.swaggerAssetsEnabled || !isSwaggerAssetPath(ctx.pathname)) {
|
|
36925
38980
|
if (tryServeStatic(BUILTIN_PUBLIC_DIR, ctx.req, ctx.res)) return true;
|
|
36926
38981
|
}
|
|
@@ -36944,10 +38999,10 @@ async function serveNotFound(ctx) {
|
|
|
36944
38999
|
return true;
|
|
36945
39000
|
}
|
|
36946
39001
|
async function buildDispatchContext(router, base) {
|
|
36947
|
-
const root = base ?
|
|
36948
|
-
const staticDir =
|
|
36949
|
-
const srcPublicDir =
|
|
36950
|
-
const templatesDir =
|
|
39002
|
+
const root = base ? resolve19(base) : process.cwd();
|
|
39003
|
+
const staticDir = resolve19(root, "public");
|
|
39004
|
+
const srcPublicDir = resolve19(root, "src/public");
|
|
39005
|
+
const templatesDir = resolve19(root, "src/templates");
|
|
36951
39006
|
let frondEngine = null;
|
|
36952
39007
|
try {
|
|
36953
39008
|
const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
|
|
@@ -37111,13 +39166,13 @@ ${reset2}
|
|
|
37111
39166
|
};
|
|
37112
39167
|
}
|
|
37113
39168
|
}
|
|
37114
|
-
const base = config?.basePath ?
|
|
37115
|
-
const routesDir =
|
|
37116
|
-
const modelsDir =
|
|
37117
|
-
const ormDir =
|
|
37118
|
-
const staticDir =
|
|
37119
|
-
const srcPublicDir =
|
|
37120
|
-
const templatesDir =
|
|
39169
|
+
const base = config?.basePath ? resolve19(config.basePath) : process.cwd();
|
|
39170
|
+
const routesDir = resolve19(base, config?.routesDir ?? "src/routes");
|
|
39171
|
+
const modelsDir = resolve19(base, config?.modelsDir ?? "src/models");
|
|
39172
|
+
const ormDir = resolve19(base, "src/orm");
|
|
39173
|
+
const staticDir = resolve19(base, config?.staticDir ?? "public");
|
|
39174
|
+
const srcPublicDir = resolve19(base, "src/public");
|
|
39175
|
+
const templatesDir = resolve19(base, config?.templatesDir ?? "src/templates");
|
|
37121
39176
|
const router = new Router();
|
|
37122
39177
|
const middleware = new MiddlewareChain();
|
|
37123
39178
|
globalThis.__tina4_router = router;
|
|
@@ -37151,8 +39206,8 @@ ${reset2}
|
|
|
37151
39206
|
} catch {
|
|
37152
39207
|
}
|
|
37153
39208
|
if (frondEngine) {
|
|
37154
|
-
const localeDir =
|
|
37155
|
-
if (
|
|
39209
|
+
const localeDir = resolve19(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
|
|
39210
|
+
if (existsSync25(localeDir)) {
|
|
37156
39211
|
try {
|
|
37157
39212
|
const localeFiles = readdirSync17(localeDir).filter((f) => f.endsWith(".json"));
|
|
37158
39213
|
if (localeFiles.length > 0 && !frondEngine.globals?.t) {
|
|
@@ -37167,7 +39222,7 @@ ${reset2}
|
|
|
37167
39222
|
middleware.use(requestLogger());
|
|
37168
39223
|
middleware.use(rateLimiter());
|
|
37169
39224
|
MiddlewareRunner.use(SecurityHeadersMiddleware);
|
|
37170
|
-
if (
|
|
39225
|
+
if (existsSync25(routesDir)) {
|
|
37171
39226
|
const routes = await discoverRoutes(routesDir);
|
|
37172
39227
|
for (const route of routes) {
|
|
37173
39228
|
router.addRoute(route);
|
|
@@ -37187,8 +39242,8 @@ ${reset2}
|
|
|
37187
39242
|
console.log(`
|
|
37188
39243
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
37189
39244
|
}
|
|
37190
|
-
const hasOrmDir =
|
|
37191
|
-
const hasModelsDir =
|
|
39245
|
+
const hasOrmDir = existsSync25(ormDir);
|
|
39246
|
+
const hasModelsDir = existsSync25(modelsDir);
|
|
37192
39247
|
if (hasOrmDir || hasModelsDir) {
|
|
37193
39248
|
try {
|
|
37194
39249
|
const orm = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
@@ -37245,7 +39300,7 @@ ${reset2}
|
|
|
37245
39300
|
let modelDefs = [];
|
|
37246
39301
|
try {
|
|
37247
39302
|
const orm = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
37248
|
-
const allModelDirs = [ormDir, modelsDir].filter((d) =>
|
|
39303
|
+
const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync25(d));
|
|
37249
39304
|
const seenTables = /* @__PURE__ */ new Set();
|
|
37250
39305
|
for (const dir of allModelDirs) {
|
|
37251
39306
|
const discovered = await orm.discoverModels(dir);
|
|
@@ -37480,8 +39535,8 @@ var init_server = __esm({
|
|
|
37480
39535
|
init_version();
|
|
37481
39536
|
__filename = fileURLToPath6(import.meta.url);
|
|
37482
39537
|
__dirname = dirname13(__filename);
|
|
37483
|
-
BUILTIN_ERROR_TEMPLATES_DIR =
|
|
37484
|
-
BUILTIN_PUBLIC_DIR =
|
|
39538
|
+
BUILTIN_ERROR_TEMPLATES_DIR = resolve19(__dirname, "..", "templates");
|
|
39539
|
+
BUILTIN_PUBLIC_DIR = resolve19(__dirname, "..", "public");
|
|
37485
39540
|
swaggerAssetsEnabled = false;
|
|
37486
39541
|
DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 30;
|
|
37487
39542
|
frondCache = /* @__PURE__ */ new Map();
|
|
@@ -37708,7 +39763,7 @@ var init_mqttMessage = __esm({
|
|
|
37708
39763
|
import net2 from "node:net";
|
|
37709
39764
|
import tls from "node:tls";
|
|
37710
39765
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
37711
|
-
import { existsSync as
|
|
39766
|
+
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
|
|
37712
39767
|
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
39768
|
var init_mqtt = __esm({
|
|
37714
39769
|
"src/mqtt.ts"() {
|
|
@@ -37911,7 +39966,7 @@ var init_mqtt = __esm({
|
|
|
37911
39966
|
*/
|
|
37912
39967
|
async connect() {
|
|
37913
39968
|
this.closeSocket();
|
|
37914
|
-
if (this.secure && this.tlsVerify && this.caFile && !
|
|
39969
|
+
if (this.secure && this.tlsVerify && this.caFile && !existsSync26(this.caFile)) {
|
|
37915
39970
|
throw new MqttError(
|
|
37916
39971
|
`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
39972
|
);
|
|
@@ -38148,7 +40203,7 @@ var init_mqtt = __esm({
|
|
|
38148
40203
|
* a later client.
|
|
38149
40204
|
*/
|
|
38150
40205
|
openSocket() {
|
|
38151
|
-
return new Promise((
|
|
40206
|
+
return new Promise((resolve21, reject) => {
|
|
38152
40207
|
let settled = false;
|
|
38153
40208
|
const settle = (fn) => {
|
|
38154
40209
|
if (settled) return;
|
|
@@ -38174,9 +40229,9 @@ var init_mqtt = __esm({
|
|
|
38174
40229
|
rejectUnauthorized: this.tlsVerify
|
|
38175
40230
|
};
|
|
38176
40231
|
if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
|
|
38177
|
-
sock = tls.connect(opts, () => settle(() =>
|
|
40232
|
+
sock = tls.connect(opts, () => settle(() => resolve21(sock)));
|
|
38178
40233
|
} else {
|
|
38179
|
-
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() =>
|
|
40234
|
+
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
|
|
38180
40235
|
}
|
|
38181
40236
|
sock.once("error", (err) => {
|
|
38182
40237
|
settle(() => {
|
|
@@ -38215,13 +40270,13 @@ var init_mqtt = __esm({
|
|
|
38215
40270
|
writePacket(header, body) {
|
|
38216
40271
|
if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
|
|
38217
40272
|
const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
|
|
38218
|
-
return new Promise((
|
|
40273
|
+
return new Promise((resolve21, reject) => {
|
|
38219
40274
|
this.socket.write(packet, (err) => {
|
|
38220
40275
|
if (err) {
|
|
38221
40276
|
reject(new MqttError(`MQTT write failed: ${err.message}`));
|
|
38222
40277
|
} else {
|
|
38223
40278
|
this.lastWriteAt = Date.now();
|
|
38224
|
-
|
|
40279
|
+
resolve21();
|
|
38225
40280
|
}
|
|
38226
40281
|
});
|
|
38227
40282
|
});
|
|
@@ -38254,7 +40309,7 @@ var init_mqtt = __esm({
|
|
|
38254
40309
|
if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
|
|
38255
40310
|
if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
|
|
38256
40311
|
if (this.socketError !== null) return Promise.reject(this.socketError);
|
|
38257
|
-
return new Promise((
|
|
40312
|
+
return new Promise((resolve21, reject) => {
|
|
38258
40313
|
let timer = null;
|
|
38259
40314
|
if (deadline !== null) {
|
|
38260
40315
|
const remaining = deadline - Date.now();
|
|
@@ -38269,7 +40324,7 @@ var init_mqtt = __esm({
|
|
|
38269
40324
|
}
|
|
38270
40325
|
}, remaining);
|
|
38271
40326
|
}
|
|
38272
|
-
this.waiter = { need, resolve:
|
|
40327
|
+
this.waiter = { need, resolve: resolve21, reject, timer };
|
|
38273
40328
|
this.serviceWaiter();
|
|
38274
40329
|
});
|
|
38275
40330
|
}
|
|
@@ -38394,7 +40449,7 @@ var init_mqtt = __esm({
|
|
|
38394
40449
|
|
|
38395
40450
|
// src/service.ts
|
|
38396
40451
|
import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
|
|
38397
|
-
import { join as
|
|
40452
|
+
import { join as join31, extname as extname7 } from "node:path";
|
|
38398
40453
|
import { pathToFileURL } from "node:url";
|
|
38399
40454
|
function matchCronField(field, value) {
|
|
38400
40455
|
if (field === "*") return true;
|
|
@@ -38568,7 +40623,7 @@ var init_service = __esm({
|
|
|
38568
40623
|
for (const entry of entries) {
|
|
38569
40624
|
const ext = extname7(entry);
|
|
38570
40625
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
38571
|
-
const fullPath =
|
|
40626
|
+
const fullPath = join31(dir, entry);
|
|
38572
40627
|
const stat = statSync18(fullPath);
|
|
38573
40628
|
if (!stat.isFile()) continue;
|
|
38574
40629
|
try {
|
|
@@ -38695,7 +40750,7 @@ var init_service = __esm({
|
|
|
38695
40750
|
for (const entry of entries) {
|
|
38696
40751
|
const ext = extname7(entry);
|
|
38697
40752
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
38698
|
-
const fullPath =
|
|
40753
|
+
const fullPath = join31(dir, entry);
|
|
38699
40754
|
if (watchedFiles.has(fullPath)) continue;
|
|
38700
40755
|
watchedFiles.add(fullPath);
|
|
38701
40756
|
watchFile(fullPath, { interval: 1e3 }, async () => {
|
|
@@ -39304,7 +41359,7 @@ var init_api = __esm({
|
|
|
39304
41359
|
* `res.destroy()`.
|
|
39305
41360
|
*/
|
|
39306
41361
|
openStreamRequest(method, url, headers, data, connectSec) {
|
|
39307
|
-
return new Promise((
|
|
41362
|
+
return new Promise((resolve21, reject) => {
|
|
39308
41363
|
let parsed;
|
|
39309
41364
|
try {
|
|
39310
41365
|
parsed = new URL2(url);
|
|
@@ -39326,7 +41381,7 @@ var init_api = __esm({
|
|
|
39326
41381
|
options.rejectUnauthorized = false;
|
|
39327
41382
|
}
|
|
39328
41383
|
const req2 = protocolModule.request(options, (res) => {
|
|
39329
|
-
|
|
41384
|
+
resolve21({ res });
|
|
39330
41385
|
});
|
|
39331
41386
|
req2.on("timeout", () => {
|
|
39332
41387
|
req2.destroy(new ApiStreamError(`stream connect timeout after ${connectSec}s`, null));
|
|
@@ -39464,12 +41519,12 @@ var init_api = __esm({
|
|
|
39464
41519
|
* authenticate to.
|
|
39465
41520
|
*/
|
|
39466
41521
|
performRequest(method, url, headers, data, redirectsLeft) {
|
|
39467
|
-
return new Promise((
|
|
41522
|
+
return new Promise((resolve21) => {
|
|
39468
41523
|
let parsed;
|
|
39469
41524
|
try {
|
|
39470
41525
|
parsed = new URL2(url);
|
|
39471
41526
|
} catch (err) {
|
|
39472
|
-
|
|
41527
|
+
resolve21({ kind: "error", error: err instanceof Error ? err.message : String(err) });
|
|
39473
41528
|
return;
|
|
39474
41529
|
}
|
|
39475
41530
|
const isHttps = parsed.protocol === "https:";
|
|
@@ -39494,7 +41549,7 @@ var init_api = __esm({
|
|
|
39494
41549
|
try {
|
|
39495
41550
|
nextUrl = new URL2(location, url).toString();
|
|
39496
41551
|
} catch {
|
|
39497
|
-
|
|
41552
|
+
resolve21({ kind: "response", res });
|
|
39498
41553
|
return;
|
|
39499
41554
|
}
|
|
39500
41555
|
const crossOrigin = !sameOrigin(url, nextUrl);
|
|
@@ -39512,17 +41567,17 @@ var init_api = __esm({
|
|
|
39512
41567
|
deleteHeaderCaseInsensitive(nextHeaders, name);
|
|
39513
41568
|
}
|
|
39514
41569
|
}
|
|
39515
|
-
this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(
|
|
41570
|
+
this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve21);
|
|
39516
41571
|
return;
|
|
39517
41572
|
}
|
|
39518
|
-
|
|
41573
|
+
resolve21({ kind: "response", res });
|
|
39519
41574
|
});
|
|
39520
41575
|
req2.on("timeout", () => {
|
|
39521
41576
|
req2.destroy();
|
|
39522
|
-
|
|
41577
|
+
resolve21({ kind: "error", error: `Request timed out after ${this.timeout}s` });
|
|
39523
41578
|
});
|
|
39524
41579
|
req2.on("error", (err) => {
|
|
39525
|
-
|
|
41580
|
+
resolve21({ kind: "error", error: err.message });
|
|
39526
41581
|
});
|
|
39527
41582
|
if (data) {
|
|
39528
41583
|
req2.write(data);
|
|
@@ -39532,7 +41587,7 @@ var init_api = __esm({
|
|
|
39532
41587
|
}
|
|
39533
41588
|
/** Buffer a response body, parse JSON if possible, and store cookies. */
|
|
39534
41589
|
readResponse(res) {
|
|
39535
|
-
return new Promise((
|
|
41590
|
+
return new Promise((resolve21) => {
|
|
39536
41591
|
const chunks = [];
|
|
39537
41592
|
res.on("data", (chunk) => {
|
|
39538
41593
|
chunks.push(chunk);
|
|
@@ -39547,7 +41602,7 @@ var init_api = __esm({
|
|
|
39547
41602
|
} catch {
|
|
39548
41603
|
parsed = raw;
|
|
39549
41604
|
}
|
|
39550
|
-
|
|
41605
|
+
resolve21({
|
|
39551
41606
|
http_code: res.statusCode ?? null,
|
|
39552
41607
|
body: parsed,
|
|
39553
41608
|
headers: respHeaders,
|
|
@@ -39555,7 +41610,7 @@ var init_api = __esm({
|
|
|
39555
41610
|
});
|
|
39556
41611
|
});
|
|
39557
41612
|
res.on("error", (err) => {
|
|
39558
|
-
|
|
41613
|
+
resolve21({ http_code: null, body: null, headers: {}, error: err.message });
|
|
39559
41614
|
});
|
|
39560
41615
|
});
|
|
39561
41616
|
}
|
|
@@ -39619,7 +41674,7 @@ function parseMailRedirectList(raw) {
|
|
|
39619
41674
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
39620
41675
|
}
|
|
39621
41676
|
function readResponse(socket) {
|
|
39622
|
-
return new Promise((
|
|
41677
|
+
return new Promise((resolve21, reject) => {
|
|
39623
41678
|
let buffer = "";
|
|
39624
41679
|
const onData = (chunk) => {
|
|
39625
41680
|
buffer += chunk.toString("utf-8");
|
|
@@ -39631,7 +41686,7 @@ function readResponse(socket) {
|
|
|
39631
41686
|
if (line.length >= 4 && line[3] === " ") {
|
|
39632
41687
|
socket.removeListener("data", onData);
|
|
39633
41688
|
socket.removeListener("error", onError);
|
|
39634
|
-
|
|
41689
|
+
resolve21({ code, text: buffer.trim() });
|
|
39635
41690
|
return;
|
|
39636
41691
|
}
|
|
39637
41692
|
}
|
|
@@ -39645,10 +41700,10 @@ function readResponse(socket) {
|
|
|
39645
41700
|
});
|
|
39646
41701
|
}
|
|
39647
41702
|
function sendCommand(socket, command) {
|
|
39648
|
-
return new Promise((
|
|
41703
|
+
return new Promise((resolve21, reject) => {
|
|
39649
41704
|
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
39650
41705
|
if (err) return reject(err);
|
|
39651
|
-
readResponse(socket).then(
|
|
41706
|
+
readResponse(socket).then(resolve21, reject);
|
|
39652
41707
|
});
|
|
39653
41708
|
});
|
|
39654
41709
|
}
|
|
@@ -39748,7 +41803,7 @@ function imapQuote(s) {
|
|
|
39748
41803
|
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
39749
41804
|
}
|
|
39750
41805
|
function imapReadLine(socket) {
|
|
39751
|
-
return new Promise((
|
|
41806
|
+
return new Promise((resolve21, reject) => {
|
|
39752
41807
|
let buffer = "";
|
|
39753
41808
|
const onData = (chunk) => {
|
|
39754
41809
|
buffer += chunk.toString("utf-8");
|
|
@@ -39756,7 +41811,7 @@ function imapReadLine(socket) {
|
|
|
39756
41811
|
if (nlIndex !== -1) {
|
|
39757
41812
|
socket.removeListener("data", onData);
|
|
39758
41813
|
socket.removeListener("error", onError);
|
|
39759
|
-
|
|
41814
|
+
resolve21(buffer);
|
|
39760
41815
|
}
|
|
39761
41816
|
};
|
|
39762
41817
|
const onError = (err) => {
|
|
@@ -39768,7 +41823,7 @@ function imapReadLine(socket) {
|
|
|
39768
41823
|
});
|
|
39769
41824
|
}
|
|
39770
41825
|
function imapCommand(socket, command) {
|
|
39771
|
-
return new Promise((
|
|
41826
|
+
return new Promise((resolve21, reject) => {
|
|
39772
41827
|
imapTagCounter++;
|
|
39773
41828
|
const tag = `T${imapTagCounter}`;
|
|
39774
41829
|
const fullCommand = `${tag} ${command}\r
|
|
@@ -39779,7 +41834,7 @@ function imapCommand(socket, command) {
|
|
|
39779
41834
|
if (buffer.includes(`${tag} OK`)) {
|
|
39780
41835
|
socket.removeListener("data", onData);
|
|
39781
41836
|
socket.removeListener("error", onError);
|
|
39782
|
-
|
|
41837
|
+
resolve21(buffer);
|
|
39783
41838
|
return;
|
|
39784
41839
|
}
|
|
39785
41840
|
if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
|
|
@@ -40084,14 +42139,14 @@ var init_messenger = __esm({
|
|
|
40084
42139
|
let socket;
|
|
40085
42140
|
if (this.port === 465) {
|
|
40086
42141
|
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
40087
|
-
await new Promise((
|
|
40088
|
-
socket.once("secureConnect",
|
|
42142
|
+
await new Promise((resolve21, reject) => {
|
|
42143
|
+
socket.once("secureConnect", resolve21);
|
|
40089
42144
|
socket.once("error", reject);
|
|
40090
42145
|
});
|
|
40091
42146
|
} else {
|
|
40092
42147
|
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
40093
|
-
await new Promise((
|
|
40094
|
-
socket.once("connect",
|
|
42148
|
+
await new Promise((resolve21, reject) => {
|
|
42149
|
+
socket.once("connect", resolve21);
|
|
40095
42150
|
socket.once("error", reject);
|
|
40096
42151
|
});
|
|
40097
42152
|
}
|
|
@@ -40115,8 +42170,8 @@ var init_messenger = __esm({
|
|
|
40115
42170
|
socket = tls2.connect(
|
|
40116
42171
|
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
40117
42172
|
);
|
|
40118
|
-
await new Promise((
|
|
40119
|
-
socket.once("secureConnect",
|
|
42173
|
+
await new Promise((resolve21, reject) => {
|
|
42174
|
+
socket.once("secureConnect", resolve21);
|
|
40120
42175
|
socket.once("error", reject);
|
|
40121
42176
|
});
|
|
40122
42177
|
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
@@ -40209,14 +42264,14 @@ var init_messenger = __esm({
|
|
|
40209
42264
|
let socket;
|
|
40210
42265
|
if (this.port === 465) {
|
|
40211
42266
|
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
40212
|
-
await new Promise((
|
|
40213
|
-
socket.once("secureConnect",
|
|
42267
|
+
await new Promise((resolve21, reject) => {
|
|
42268
|
+
socket.once("secureConnect", resolve21);
|
|
40214
42269
|
socket.once("error", reject);
|
|
40215
42270
|
});
|
|
40216
42271
|
} else {
|
|
40217
42272
|
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
40218
|
-
await new Promise((
|
|
40219
|
-
socket.once("connect",
|
|
42273
|
+
await new Promise((resolve21, reject) => {
|
|
42274
|
+
socket.once("connect", resolve21);
|
|
40220
42275
|
socket.once("error", reject);
|
|
40221
42276
|
});
|
|
40222
42277
|
}
|
|
@@ -40251,14 +42306,14 @@ var init_messenger = __esm({
|
|
|
40251
42306
|
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
40252
42307
|
if (useTls) {
|
|
40253
42308
|
socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
40254
|
-
await new Promise((
|
|
40255
|
-
socket.once("secureConnect",
|
|
42309
|
+
await new Promise((resolve21, reject) => {
|
|
42310
|
+
socket.once("secureConnect", resolve21);
|
|
40256
42311
|
socket.once("error", reject);
|
|
40257
42312
|
});
|
|
40258
42313
|
} else {
|
|
40259
42314
|
socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
40260
|
-
await new Promise((
|
|
40261
|
-
socket.once("connect",
|
|
42315
|
+
await new Promise((resolve21, reject) => {
|
|
42316
|
+
socket.once("connect", resolve21);
|
|
40262
42317
|
socket.once("error", reject);
|
|
40263
42318
|
});
|
|
40264
42319
|
}
|
|
@@ -41168,16 +43223,16 @@ var init_htmlElement = __esm({
|
|
|
41168
43223
|
});
|
|
41169
43224
|
|
|
41170
43225
|
// src/ai.ts
|
|
41171
|
-
import { existsSync as
|
|
43226
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync21, writeFileSync as writeFileSync18, readFileSync as readFileSync26 } from "node:fs";
|
|
41172
43227
|
import { homedir } from "node:os";
|
|
41173
|
-
import { join as
|
|
43228
|
+
import { join as join32, resolve as resolve20, relative as relative10, dirname as dirname14 } from "node:path";
|
|
41174
43229
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
41175
43230
|
import { execSync, execFileSync as execFileSync4 } from "node:child_process";
|
|
41176
43231
|
import { createInterface } from "node:readline";
|
|
41177
43232
|
function readVersion() {
|
|
41178
43233
|
try {
|
|
41179
43234
|
const thisDir = dirname14(fileURLToPath7(import.meta.url));
|
|
41180
|
-
const rootPkg =
|
|
43235
|
+
const rootPkg = resolve20(thisDir, "..", "..", "..", "package.json");
|
|
41181
43236
|
const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
|
|
41182
43237
|
return pkg.version ?? "0.0.0";
|
|
41183
43238
|
} catch {
|
|
@@ -41241,8 +43296,8 @@ function downloadSkillsSync(jobs) {
|
|
|
41241
43296
|
function installSkills(root = ".", targets) {
|
|
41242
43297
|
const ref = skillsRef();
|
|
41243
43298
|
const dests = targets ?? [
|
|
41244
|
-
|
|
41245
|
-
|
|
43299
|
+
join32(resolve20(root), ".claude", "skills"),
|
|
43300
|
+
join32(homedir(), ".claude", "skills")
|
|
41246
43301
|
];
|
|
41247
43302
|
const jobs = [];
|
|
41248
43303
|
const index = /* @__PURE__ */ new Map();
|
|
@@ -41260,9 +43315,9 @@ function installSkills(root = ".", targets) {
|
|
|
41260
43315
|
const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
|
|
41261
43316
|
skillMdUrl[skill] = `${base}/SKILL.md`;
|
|
41262
43317
|
for (const dest of dests) {
|
|
41263
|
-
add(`${base}/SKILL.md`,
|
|
43318
|
+
add(`${base}/SKILL.md`, join32(dest, skill, "SKILL.md"));
|
|
41264
43319
|
for (const r of spec.references) {
|
|
41265
|
-
add(`${base}/references/${r}`,
|
|
43320
|
+
add(`${base}/references/${r}`, join32(dest, skill, "references", r));
|
|
41266
43321
|
}
|
|
41267
43322
|
}
|
|
41268
43323
|
}
|
|
@@ -41274,10 +43329,10 @@ function installSkills(root = ".", targets) {
|
|
|
41274
43329
|
return installed;
|
|
41275
43330
|
}
|
|
41276
43331
|
function isInstalled(root, tool) {
|
|
41277
|
-
return
|
|
43332
|
+
return existsSync27(join32(resolve20(root), tool.contextFile));
|
|
41278
43333
|
}
|
|
41279
43334
|
function showMenu(root = ".") {
|
|
41280
|
-
const r =
|
|
43335
|
+
const r = resolve20(root);
|
|
41281
43336
|
console.log("\n Tina4 AI Context Installer\n");
|
|
41282
43337
|
for (let i = 0; i < AI_TOOLS.length; i++) {
|
|
41283
43338
|
const tool = AI_TOOLS[i];
|
|
@@ -41295,16 +43350,16 @@ function showMenu(root = ".") {
|
|
|
41295
43350
|
const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
|
|
41296
43351
|
console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
|
|
41297
43352
|
console.log();
|
|
41298
|
-
return new Promise((
|
|
43353
|
+
return new Promise((resolve21) => {
|
|
41299
43354
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
41300
43355
|
rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
|
|
41301
43356
|
rl.close();
|
|
41302
|
-
|
|
43357
|
+
resolve21(answer.trim());
|
|
41303
43358
|
});
|
|
41304
43359
|
});
|
|
41305
43360
|
}
|
|
41306
43361
|
function installSelected(root, selection) {
|
|
41307
|
-
const rootPath =
|
|
43362
|
+
const rootPath = resolve20(root);
|
|
41308
43363
|
const created = [];
|
|
41309
43364
|
let indices;
|
|
41310
43365
|
let doInstallTina4Ai = false;
|
|
@@ -41397,35 +43452,35 @@ function looksLikeOldFrameworkInstall(existing) {
|
|
|
41397
43452
|
function writeOrMerge(contextPath, contextFile, frameworkGuide) {
|
|
41398
43453
|
const block = skillBlock(contextFile);
|
|
41399
43454
|
const [start2, end] = markersFor(contextFile);
|
|
41400
|
-
if (!
|
|
41401
|
-
|
|
43455
|
+
if (!existsSync27(contextPath)) {
|
|
43456
|
+
writeFileSync18(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
41402
43457
|
return "Installed";
|
|
41403
43458
|
}
|
|
41404
43459
|
const existing = readFileSync26(contextPath, "utf-8");
|
|
41405
43460
|
if (hasMarkers(existing, start2, end)) {
|
|
41406
|
-
|
|
43461
|
+
writeFileSync18(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
|
|
41407
43462
|
return "Refreshed skill block in";
|
|
41408
43463
|
}
|
|
41409
43464
|
if (looksLikeOldFrameworkInstall(existing)) {
|
|
41410
43465
|
const head = existing.replace(/^\s+/, "");
|
|
41411
43466
|
const preamble = existing.slice(0, existing.length - head.length);
|
|
41412
43467
|
const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
|
|
41413
|
-
|
|
43468
|
+
writeFileSync18(contextPath, newContent, "utf-8");
|
|
41414
43469
|
return "Migrated (replaced old framework dump in)";
|
|
41415
43470
|
}
|
|
41416
|
-
|
|
43471
|
+
writeFileSync18(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
41417
43472
|
return "Appended skill block to";
|
|
41418
43473
|
}
|
|
41419
43474
|
function installForTool(root, tool, context) {
|
|
41420
43475
|
const created = [];
|
|
41421
|
-
const contextPath =
|
|
43476
|
+
const contextPath = join32(root, tool.contextFile);
|
|
41422
43477
|
if (tool.configDir) {
|
|
41423
|
-
|
|
43478
|
+
mkdirSync21(join32(root, tool.configDir), { recursive: true });
|
|
41424
43479
|
}
|
|
41425
43480
|
const parentDir = dirname14(contextPath);
|
|
41426
|
-
|
|
43481
|
+
mkdirSync21(parentDir, { recursive: true });
|
|
41427
43482
|
const action = writeOrMerge(contextPath, tool.contextFile, context);
|
|
41428
|
-
const rel =
|
|
43483
|
+
const rel = relative10(root, contextPath);
|
|
41429
43484
|
created.push(rel);
|
|
41430
43485
|
console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
|
|
41431
43486
|
if (tool.name === "claude-code") {
|
|
@@ -41458,7 +43513,7 @@ function installTina4Ai() {
|
|
|
41458
43513
|
function installClaudeSkills(root) {
|
|
41459
43514
|
const created = [];
|
|
41460
43515
|
for (const skill of installSkills(root)) {
|
|
41461
|
-
created.push(
|
|
43516
|
+
created.push(join32(".claude", "skills", skill));
|
|
41462
43517
|
console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
|
|
41463
43518
|
}
|
|
41464
43519
|
return created;
|
|
@@ -41800,9 +43855,9 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
|
|
|
41800
43855
|
function generateClaudeCodeContext() {
|
|
41801
43856
|
try {
|
|
41802
43857
|
const thisDir = dirname14(fileURLToPath7(import.meta.url));
|
|
41803
|
-
const repoRoot =
|
|
41804
|
-
const claudeMdPath =
|
|
41805
|
-
if (
|
|
43858
|
+
const repoRoot = resolve20(thisDir, "..", "..", "..");
|
|
43859
|
+
const claudeMdPath = join32(repoRoot, "CLAUDE.md");
|
|
43860
|
+
if (existsSync27(claudeMdPath)) {
|
|
41806
43861
|
return readFileSync26(claudeMdPath, "utf-8");
|
|
41807
43862
|
}
|
|
41808
43863
|
} catch {
|
|
@@ -42349,11 +44404,11 @@ var init_aiClient = __esm({
|
|
|
42349
44404
|
const payload = JSON.stringify(body);
|
|
42350
44405
|
const controller = new AbortController();
|
|
42351
44406
|
const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
|
|
42352
|
-
return new Promise((
|
|
44407
|
+
return new Promise((resolve21, reject) => {
|
|
42353
44408
|
const client = url.protocol === "https:" ? https2 : http2;
|
|
42354
44409
|
const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
|
|
42355
44410
|
clearTimeout(connectTimer);
|
|
42356
|
-
|
|
44411
|
+
resolve21({ response, cleanup: () => {
|
|
42357
44412
|
clearTimeout(totalTimer);
|
|
42358
44413
|
clearTimeout(connectTimer);
|
|
42359
44414
|
} });
|
|
@@ -42382,7 +44437,7 @@ var init_aiClient = __esm({
|
|
|
42382
44437
|
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
42383
44438
|
const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
|
|
42384
44439
|
const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
|
|
42385
|
-
return new Promise((
|
|
44440
|
+
return new Promise((resolve21) => setTimeout(resolve21, delay));
|
|
42386
44441
|
}
|
|
42387
44442
|
static async requestJson(config, headers, body) {
|
|
42388
44443
|
const deadline = performance.now() + config.totalTimeout * 1e3;
|