tina4-nodejs 3.13.120 → 3.13.122

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