tina4-nodejs 3.13.119 → 3.13.121

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