tina4-nodejs 3.13.95 → 3.13.97

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.
Files changed (36) hide show
  1. package/CLAUDE.md +3 -4
  2. package/package.json +2 -1
  3. package/packages/cli/dist/bin.js +708 -1012
  4. package/packages/core/dist/index.js +588 -893
  5. package/packages/core/public/css/tina4.min.css +1 -1
  6. package/packages/core/src/index.ts +1 -3
  7. package/packages/core/src/messenger.ts +288 -96
  8. package/packages/core/src/queueBackends/kafkaBackend.ts +23 -2
  9. package/packages/core/src/queueBackends/rabbitmqBackend.ts +29 -17
  10. package/packages/core/src/request.ts +28 -7
  11. package/packages/core/src/server.ts +135 -7
  12. package/packages/core/src/session.ts +8 -1
  13. package/packages/orm/dist/index.js +639 -944
  14. package/packages/orm/src/autoCrud.ts +12 -10
  15. package/packages/orm/src/database.ts +62 -58
  16. package/packages/orm/src/databaseResult.ts +44 -73
  17. package/packages/orm/src/index.ts +0 -3
  18. package/packages/orm/src/migration.ts +26 -8
  19. package/packages/orm/src/model.ts +4 -0
  20. package/packages/orm/src/queryBuilder.ts +12 -5
  21. package/packages/orm/src/types.ts +7 -74
  22. package/packages/swagger/dist/index.js +78 -20
  23. package/packages/swagger/src/generator.ts +172 -29
  24. package/types/core/src/index.d.ts +1 -3
  25. package/types/core/src/messenger.d.ts +45 -4
  26. package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -0
  27. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +2 -1
  28. package/types/core/src/server.d.ts +0 -4
  29. package/types/core/src/session.d.ts +7 -0
  30. package/types/orm/src/database.d.ts +34 -30
  31. package/types/orm/src/databaseResult.d.ts +26 -36
  32. package/types/orm/src/index.d.ts +1 -2
  33. package/types/orm/src/migration.d.ts +4 -3
  34. package/types/orm/src/types.d.ts +7 -34
  35. package/packages/core/src/scss.ts +0 -623
  36. package/types/core/src/scss.d.ts +0 -19
@@ -1829,64 +1829,6 @@ var init_middleware = __esm({
1829
1829
  }
1830
1830
  });
1831
1831
 
1832
- // ../orm/src/types.ts
1833
- var FetchResult;
1834
- var init_types = __esm({
1835
- "../orm/src/types.ts"() {
1836
- "use strict";
1837
- FetchResult = class {
1838
- records;
1839
- count;
1840
- sql;
1841
- constructor(records, sql = "") {
1842
- this.records = records;
1843
- this.count = records.length;
1844
- this.sql = sql;
1845
- }
1846
- /** Paginate the in-memory result set. */
1847
- toPaginate(page = 1, perPage = 20) {
1848
- const total = this.count;
1849
- const totalPages = Math.max(1, Math.ceil(total / perPage));
1850
- const offset = (page - 1) * perPage;
1851
- const data = this.records.slice(offset, offset + perPage);
1852
- return {
1853
- data,
1854
- page,
1855
- perPage,
1856
- total,
1857
- totalPages,
1858
- hasNext: page < totalPages,
1859
- hasPrev: page > 1
1860
- };
1861
- }
1862
- /** Return the first record or null. */
1863
- first() {
1864
- return this.records[0] ?? null;
1865
- }
1866
- /** Return the last record or null. */
1867
- last() {
1868
- return this.records[this.records.length - 1] ?? null;
1869
- }
1870
- /** Check if result is empty. */
1871
- isEmpty() {
1872
- return this.records.length === 0;
1873
- }
1874
- /** Convert to plain array. */
1875
- toArray() {
1876
- return [...this.records];
1877
- }
1878
- /** Convert to JSON string. */
1879
- toJSON() {
1880
- return JSON.stringify(this.records);
1881
- }
1882
- /** Iterate over records. */
1883
- [Symbol.iterator]() {
1884
- return this.records[Symbol.iterator]();
1885
- }
1886
- };
1887
- }
1888
- });
1889
-
1890
1832
  // ../orm/src/databaseResult.ts
1891
1833
  var DatabaseResult;
1892
1834
  var init_databaseResult = __esm({
@@ -1950,75 +1892,52 @@ var init_databaseResult = __esm({
1950
1892
  toArray() {
1951
1893
  return this.records;
1952
1894
  }
1953
- /** Pagination envelope — accepts either (page, perPage) or (offset, limit) style.
1895
+ /**
1896
+ * Describe the page this result IS — the canonical pagination envelope.
1954
1897
  *
1955
- * When called with two arguments both >= 0 and the first >= the second
1956
- * (i.e. offset-style), pass `{ offset, limit }` as the first argument.
1957
- * The simplest way is to always use the default (page, perPage) form and
1958
- * let the autoCRUD layer supply offset/limit from the query string.
1898
+ * Takes NO arguments and derives every field from the query that produced this
1899
+ * result (ADR-0043). Passing ANY argument RAISES: a DatabaseResult holds no
1900
+ * connection, so an argument could only re-slice the rows already in memory and
1901
+ * then report total_pages for pages it can never reach. To read page N, FETCH
1902
+ * page N (limit + offset) and call this with no arguments.
1959
1903
  *
1960
- * Returns a superset of keys for backwards-compatibility across all clients.
1961
- */
1962
- /**
1963
- * Describe the page this result actually IS. Takes no arguments.
1904
+ * The envelope is EXACTLY seven snake_case keys, identical across all four
1905
+ * frameworks: `records, total, page, per_page, total_pages, limit, offset`.
1964
1906
  *
1965
- * MEASURED 2026-08-05 on a real 250-row table read with limit=20 offset=40
1966
- * (page 3 of 13): this reported page 1 of 2 and returned 10 of the 20 rows.
1967
- * It ignored the query entirely - defaulting page to 1 and perPage to 10 -
1968
- * then re-sliced the rows it was handed, which were already just that page.
1969
- * So a caller who paginated correctly at the SQL level had the answer
1970
- * silently re-paginated underneath them, with a page number that was simply
1971
- * wrong.
1907
+ * per_page = the query's limit
1908
+ * page = floor(offset / limit) + 1
1909
+ * total = the TRUE total for the filter Database.fetch (and
1910
+ * QueryBuilder.get) run a COUNT probe whenever a limit was
1911
+ * applied NEVER the number of rows returned
1912
+ * total_pages = ceil(total / per_page)
1913
+ * records = the rows the query returned, VERBATIM (never re-sliced)
1914
+ * limit = the SQL limit actually applied
1915
+ * offset = the SQL offset actually applied
1972
1916
  *
1973
- * WITH page/perPage it slices this result in memory, the behaviour GitHub
1974
- * issue #106 asked for. Valid ONLY when the result holds the WHOLE set
1975
- * (records.length >= count). A PARTIAL result cannot be sliced by page number
1976
- * without lying: MEASURED on 100,000 rows read under the default cap of 100,
1977
- * pages 1-5 of 20 were right and every page from 6 onward came back EMPTY
1978
- * while totalPages reported 5,000.
1917
+ * The JSON payload is snake_case even though the method name is camelCase — a
1918
+ * JSON key is data, not a language surface (ADR-0043). The old duplicate and
1919
+ * camelCase keys (`data`, `count`, `perPage`, `totalPages`, `has_next`,
1920
+ * `has_prev`) are removed: Node emitted 13 keys, the worst offender of the four.
1979
1921
  *
1980
- * `total` is `count`, and `count` is now the TRUE total for the filter in
1981
- * all four frameworks - Database.fetch runs a COUNT probe whenever it applied
1982
- * a limit. It used to be ROWS RETURNED here and in Ruby while Python and PHP
1983
- * probed, so one query answered 20 in two frameworks and 250 in the other
1984
- * two.
1922
+ * @throws {TypeError} if called with any argument.
1985
1923
  */
1986
- toPaginate(page, perPage) {
1987
- if ((page !== void 0 || perPage !== void 0) && this.records.length < this.count) {
1924
+ toPaginate() {
1925
+ if (arguments.length > 0) {
1988
1926
  throw new TypeError(
1989
- `toPaginate(page, perPage) slices the rows this result holds, but this result holds only ${this.records.length} of ${this.count} rows - it is a PARTIAL result, so any page past the rows it holds comes back empty while totalPages claims it exists. MEASURED on 100,000 rows read under the default cap of 100: pages 1-5 of 20 were right and pages 6 onward returned NOTHING. Fetch the page you want instead: fetch(sql, params, perPage, (page - 1) * perPage), then call toPaginate() with no arguments.`
1927
+ "toPaginate() takes no arguments and derives the page from the query that ran (ADR-0043). A DatabaseResult holds no connection, so an argument could only re-slice the rows already in memory and report total_pages for pages it can never reach. To read a page, FETCH it: db.fetch(sql, params, perPage, (page - 1) * perPage), then call toPaginate() with no arguments."
1990
1928
  );
1991
1929
  }
1992
- let resolvedPerPage;
1993
- let resolvedPage;
1994
- let offset;
1995
- let rows;
1996
- if (page === void 0 && perPage === void 0) {
1997
- resolvedPerPage = this.limit > 0 ? this.limit : this.records.length;
1998
- resolvedPage = resolvedPerPage > 0 ? Math.floor(this.offset / resolvedPerPage) + 1 : 1;
1999
- offset = this.offset;
2000
- rows = this.records;
2001
- } else {
2002
- resolvedPage = page ?? 1;
2003
- resolvedPerPage = perPage ?? (this.limit > 0 ? this.limit : 10);
2004
- offset = (resolvedPage - 1) * resolvedPerPage;
2005
- rows = this.records.slice(offset, offset + resolvedPerPage);
2006
- }
2007
- const totalPages = resolvedPerPage > 0 ? Math.max(1, Math.ceil(this.count / resolvedPerPage)) : 1;
1930
+ const perPage = this.limit > 0 ? this.limit : this.records.length;
1931
+ const page = perPage > 0 ? Math.floor(this.offset / perPage) + 1 : 1;
1932
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this.count / perPage)) : 1;
2008
1933
  return {
2009
- records: rows,
2010
- data: rows,
2011
- count: this.count,
1934
+ records: this.records,
2012
1935
  total: this.count,
2013
- limit: resolvedPerPage,
2014
- offset,
2015
- page: resolvedPage,
2016
- per_page: resolvedPerPage,
2017
- perPage: resolvedPerPage,
2018
- totalPages,
1936
+ page,
1937
+ per_page: perPage,
2019
1938
  total_pages: totalPages,
2020
- has_next: resolvedPage < totalPages,
2021
- has_prev: resolvedPage > 1
1939
+ limit: perPage,
1940
+ offset: this.offset
2022
1941
  };
2023
1942
  }
2024
1943
  /** Iterable — for (const row of result) */
@@ -3862,7 +3781,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
3862
3781
  const elapsedMs = () => performance.now() - startedAt;
3863
3782
  if (budgetMs === null) return attempt();
3864
3783
  const started = attempt();
3865
- return new Promise((resolve31, reject) => {
3784
+ return new Promise((resolve30, reject) => {
3866
3785
  let expired = false;
3867
3786
  const timer = setTimeout(() => {
3868
3787
  expired = true;
@@ -3872,7 +3791,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
3872
3791
  (arrived) => {
3873
3792
  clearTimeout(timer);
3874
3793
  if (expired) abandon?.(arrived);
3875
- else resolve31(arrived);
3794
+ else resolve30(arrived);
3876
3795
  },
3877
3796
  (failure) => {
3878
3797
  clearTimeout(timer);
@@ -4429,10 +4348,10 @@ var init_mysql = __esm({
4429
4348
  ...timeoutOption
4430
4349
  });
4431
4350
  }
4432
- return new Promise((resolve31, reject) => {
4351
+ return new Promise((resolve30, reject) => {
4433
4352
  this.connection.connect((err) => {
4434
4353
  if (err) reject(err);
4435
- else resolve31();
4354
+ else resolve30();
4436
4355
  });
4437
4356
  });
4438
4357
  },
@@ -4454,10 +4373,10 @@ var init_mysql = __esm({
4454
4373
  }
4455
4374
  }
4456
4375
  queryPromise(sql, params) {
4457
- return new Promise((resolve31, reject) => {
4376
+ return new Promise((resolve30, reject) => {
4458
4377
  this.connection.query(sql, params ?? [], (err, results) => {
4459
4378
  if (err) reject(err);
4460
- else resolve31(results);
4379
+ else resolve30(results);
4461
4380
  });
4462
4381
  });
4463
4382
  }
@@ -4827,11 +4746,11 @@ var init_mssql = __esm({
4827
4746
  };
4828
4747
  }
4829
4748
  await withConnectTimeout(
4830
- () => new Promise((resolve31, reject) => {
4749
+ () => new Promise((resolve30, reject) => {
4831
4750
  this.connection = new Connection(tediousConfig);
4832
4751
  this.connection.on("connect", (err) => {
4833
4752
  if (err) reject(err);
4834
- else resolve31();
4753
+ else resolve30();
4835
4754
  });
4836
4755
  this.connection.connect();
4837
4756
  }),
@@ -4876,11 +4795,11 @@ var init_mssql = __esm({
4876
4795
  const tediousModule = requireTedious();
4877
4796
  const Request = tediousModule.Request;
4878
4797
  const TYPES = tediousModule.TYPES;
4879
- return new Promise((resolve31, reject) => {
4798
+ return new Promise((resolve30, reject) => {
4880
4799
  const rows = [];
4881
4800
  const request = new Request(sql, (err, rowCount) => {
4882
4801
  if (err) reject(err);
4883
- else resolve31({ rows, rowCount });
4802
+ else resolve30({ rows, rowCount });
4884
4803
  });
4885
4804
  if (params) {
4886
4805
  params.forEach((p, i) => {
@@ -5083,8 +5002,8 @@ var init_mssql = __esm({
5083
5002
  throw new Error("Use startTransactionAsync() for MSSQL.");
5084
5003
  }
5085
5004
  async startTransactionAsync() {
5086
- await new Promise((resolve31, reject) => {
5087
- this.connection.beginTransaction((err) => err ? reject(err) : resolve31());
5005
+ await new Promise((resolve30, reject) => {
5006
+ this.connection.beginTransaction((err) => err ? reject(err) : resolve30());
5088
5007
  });
5089
5008
  this._inTransaction = true;
5090
5009
  }
@@ -5092,8 +5011,8 @@ var init_mssql = __esm({
5092
5011
  throw new Error("Use commitAsync() for MSSQL.");
5093
5012
  }
5094
5013
  async commitAsync() {
5095
- await new Promise((resolve31, reject) => {
5096
- this.connection.commitTransaction((err) => err ? reject(err) : resolve31());
5014
+ await new Promise((resolve30, reject) => {
5015
+ this.connection.commitTransaction((err) => err ? reject(err) : resolve30());
5097
5016
  });
5098
5017
  this._inTransaction = false;
5099
5018
  }
@@ -5101,8 +5020,8 @@ var init_mssql = __esm({
5101
5020
  throw new Error("Use rollbackAsync() for MSSQL.");
5102
5021
  }
5103
5022
  async rollbackAsync() {
5104
- await new Promise((resolve31, reject) => {
5105
- this.connection.rollbackTransaction((err) => err ? reject(err) : resolve31());
5023
+ await new Promise((resolve30, reject) => {
5024
+ this.connection.rollbackTransaction((err) => err ? reject(err) : resolve30());
5106
5025
  });
5107
5026
  this._inTransaction = false;
5108
5027
  }
@@ -5372,8 +5291,8 @@ var init_firebird = __esm({
5372
5291
  fbConfig.database = normalizeFirebirdDbIdentifier(fbConfig.database);
5373
5292
  }
5374
5293
  this.db = await withConnectTimeout(
5375
- () => new Promise((resolve31, reject) => {
5376
- fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve31(db));
5294
+ () => new Promise((resolve30, reject) => {
5295
+ fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve30(db));
5377
5296
  }),
5378
5297
  connectTimeoutMillis(),
5379
5298
  fbConfig.host,
@@ -5440,20 +5359,20 @@ var init_firebird = __esm({
5440
5359
  return this.transaction ?? this.db;
5441
5360
  }
5442
5361
  queryPromise(sql, params) {
5443
- return new Promise((resolve31, reject) => {
5362
+ return new Promise((resolve30, reject) => {
5444
5363
  const translated = this.translateSql(sql);
5445
5364
  this.statementHandle().query(translated, params ?? [], (err, result) => {
5446
5365
  if (err) reject(err);
5447
- else resolve31(result ?? []);
5366
+ else resolve30(result ?? []);
5448
5367
  });
5449
5368
  });
5450
5369
  }
5451
5370
  executePromise(sql, params) {
5452
- return new Promise((resolve31, reject) => {
5371
+ return new Promise((resolve30, reject) => {
5453
5372
  const translated = this.translateSql(sql);
5454
5373
  this.statementHandle().execute(translated, params ?? [], (err) => {
5455
5374
  if (err) reject(err);
5456
- else resolve31();
5375
+ else resolve30();
5457
5376
  });
5458
5377
  });
5459
5378
  }
@@ -5569,12 +5488,12 @@ var init_firebird = __esm({
5569
5488
  }
5570
5489
  async startTransactionAsync() {
5571
5490
  this.ensureConnected();
5572
- await new Promise((resolve31, reject) => {
5491
+ await new Promise((resolve30, reject) => {
5573
5492
  this.db.transaction(0, (err, transaction) => {
5574
5493
  if (err) reject(err);
5575
5494
  else {
5576
5495
  this.transaction = transaction;
5577
- resolve31();
5496
+ resolve30();
5578
5497
  }
5579
5498
  });
5580
5499
  });
@@ -5584,12 +5503,12 @@ var init_firebird = __esm({
5584
5503
  }
5585
5504
  async commitAsync() {
5586
5505
  if (!this.transaction) throw new Error("No active transaction to commit.");
5587
- await new Promise((resolve31, reject) => {
5506
+ await new Promise((resolve30, reject) => {
5588
5507
  this.transaction.commit((err) => {
5589
5508
  if (err) reject(err);
5590
5509
  else {
5591
5510
  this.transaction = null;
5592
- resolve31();
5511
+ resolve30();
5593
5512
  }
5594
5513
  });
5595
5514
  });
@@ -5599,12 +5518,12 @@ var init_firebird = __esm({
5599
5518
  }
5600
5519
  async rollbackAsync() {
5601
5520
  if (!this.transaction) throw new Error("No active transaction to rollback.");
5602
- await new Promise((resolve31, reject) => {
5521
+ await new Promise((resolve30, reject) => {
5603
5522
  this.transaction.rollback((err) => {
5604
5523
  if (err) reject(err);
5605
5524
  else {
5606
5525
  this.transaction = null;
5607
- resolve31();
5526
+ resolve30();
5608
5527
  }
5609
5528
  });
5610
5529
  });
@@ -6665,6 +6584,7 @@ __export(database_exports, {
6665
6584
  getNamedAdapter: () => getNamedAdapter,
6666
6585
  initDatabase: () => initDatabase,
6667
6586
  parseDatabaseUrl: () => parseDatabaseUrl,
6587
+ probeTotal: () => probeTotal,
6668
6588
  resetRequestCaches: () => resetRequestCaches,
6669
6589
  resolveDbPool: () => resolveDbPool,
6670
6590
  setAdapter: () => setAdapter,
@@ -6718,6 +6638,29 @@ async function adapterCreateTable(adapter, name, columns) {
6718
6638
  if (adapter.createTableAsync) await adapter.createTableAsync(name, columns);
6719
6639
  else adapter.createTable(name, columns);
6720
6640
  }
6641
+ async function probeTotal(adapter, sql, params, limit) {
6642
+ if (limit === void 0 || limit <= 0) return void 0;
6643
+ try {
6644
+ const alias = adapter.countSubqueryAlias;
6645
+ const suffix = alias ? ` AS ${alias}` : "";
6646
+ const rows = await adapterFetch(
6647
+ adapter,
6648
+ `SELECT COUNT(*) AS tina4_total FROM (${sql}
6649
+ )${suffix}`,
6650
+ params,
6651
+ void 0,
6652
+ void 0,
6653
+ true
6654
+ );
6655
+ const row = Array.isArray(rows) ? rows[0] : void 0;
6656
+ if (!row) return void 0;
6657
+ const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
6658
+ const n = Number(value);
6659
+ return Number.isFinite(n) ? n : void 0;
6660
+ } catch {
6661
+ return void 0;
6662
+ }
6663
+ }
6721
6664
  function extractLastInsertId(result) {
6722
6665
  if (result && typeof result === "object") {
6723
6666
  const r = result;
@@ -7176,65 +7119,13 @@ var init_database = __esm({
7176
7119
  try {
7177
7120
  const rows = await adapterFetch(adapter, sql, params, limit, offset, opts?.noCache);
7178
7121
  this.lastError = null;
7179
- const total = await this.countProbe(adapter, sql, params, limit);
7122
+ const total = await probeTotal(adapter, sql, params, limit);
7180
7123
  return new DatabaseResult(rows, void 0, total, limit, offset, adapter, sql);
7181
7124
  } catch (e) {
7182
7125
  this.lastError = e?.message ?? String(e);
7183
7126
  throw e;
7184
7127
  }
7185
7128
  }
7186
- /**
7187
- * The true row count for `sql`, ignoring the pagination we appended.
7188
- *
7189
- * `count` is the TRUE TOTAL for the filter, not the number of rows this page
7190
- * returned. Node and Ruby used to populate it with `records.length` while
7191
- * Python and PHP populated it from a probe, so `db.fetch(sql).count` answered
7192
- * 20 here and 250 there for one query against one table, and every paginated
7193
- * response built on it under-reported. MEASURED 2026-08-05 on a 250-row table
7194
- * read with limit=20: Node reported total 20 over 2 pages against Python's
7195
- * 250 over 13.
7196
- *
7197
- * Only probed when a limit was actually applied. With no limit the rows
7198
- * returned ARE the whole answer for this SQL, so `records.length` is already
7199
- * the true total and a second round-trip would buy nothing — which is also
7200
- * what keeps `fetchAll()` at one query.
7201
- *
7202
- * BEST EFFORT, and it can never mask a real failure: it runs AFTER the main
7203
- * query (which has already thrown on bad SQL) and returns undefined on any
7204
- * error. `undefined` — not 0 — is the miss value, so DatabaseResult falls
7205
- * back to records.length, a true lower bound. Reporting 0 next to 100 real
7206
- * records would be the same "states a wrong number authoritatively" defect
7207
- * this change exists to remove.
7208
- *
7209
- * The closing paren goes on its OWN LINE: appended inline, a trailing
7210
- * `-- comment` in the caller's SQL comments it out and the probe dies with
7211
- * "incomplete input". Postgres, MySQL and MSSQL additionally require a name
7212
- * for the derived table; SQLite and Firebird do not, and Firebird rejects
7213
- * `AS` there — so the alias comes from the adapter, not an assumption.
7214
- */
7215
- async countProbe(adapter, sql, params, limit) {
7216
- if (limit === void 0 || limit <= 0) return void 0;
7217
- try {
7218
- const alias = adapter.countSubqueryAlias;
7219
- const suffix = alias ? ` AS ${alias}` : "";
7220
- const rows = await adapterFetch(
7221
- adapter,
7222
- `SELECT COUNT(*) AS tina4_total FROM (${sql}
7223
- )${suffix}`,
7224
- params,
7225
- void 0,
7226
- void 0,
7227
- true
7228
- );
7229
- const row = Array.isArray(rows) ? rows[0] : void 0;
7230
- if (!row) return void 0;
7231
- const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
7232
- const n = Number(value);
7233
- return Number.isFinite(n) ? n : void 0;
7234
- } catch {
7235
- return void 0;
7236
- }
7237
- }
7238
7129
  /**
7239
7130
  * Fetch a single row or null.
7240
7131
  *
@@ -7946,6 +7837,10 @@ async function discoverModels(modelsDir) {
7946
7837
  }
7947
7838
  const definition = {
7948
7839
  tableName: ModelClass.tableName,
7840
+ // The class name is the type name a generated OpenAPI client wants
7841
+ // (`Item`, not `items`). Carry it so Swagger keys components.schemas by
7842
+ // it. A model exported as `default` keeps its declared class name here.
7843
+ className: typeof ModelClass.name === "string" && ModelClass.name ? ModelClass.name : void 0,
7949
7844
  fields: ModelClass.fields,
7950
7845
  fieldMapping: ModelClass.fieldMapping,
7951
7846
  softDelete: ModelClass.softDelete ?? false,
@@ -8560,7 +8455,13 @@ async function status(adapter, options) {
8560
8455
  return result;
8561
8456
  }
8562
8457
  async function createMigration(description, options) {
8563
- if (options?.kind === "class") {
8458
+ const kind = (options?.kind ?? "sql").trim().toLowerCase();
8459
+ if (!["sql", "code", "class"].includes(kind)) {
8460
+ throw new Error(
8461
+ `Unknown migration kind "${kind}". Use "sql" (default) or "code" (alias: "class"). An unrecognised kind used to produce a .sql file silently, which is why this now throws.`
8462
+ );
8463
+ }
8464
+ if (kind === "code" || kind === "class") {
8564
8465
  return createClassMigration(description, options);
8565
8466
  }
8566
8467
  const dir = resolve4(options?.migrationsDir ?? "migrations");
@@ -8687,15 +8588,13 @@ var init_migration = __esm({
8687
8588
  * Scaffold a new migration file.
8688
8589
  *
8689
8590
  * kind="sql" — creates {timestamp}_{description}.sql + .down.sql (default)
8690
- * kind="class" — creates {timestamp}_{description}.ts with a TypeScript class template
8591
+ * kind="code" — creates {timestamp}_{description}.ts with a TypeScript class
8592
+ * template. "class" is accepted as a legacy alias.
8691
8593
  *
8692
8594
  * Returns the path to the created up file (or class file).
8693
8595
  */
8694
8596
  async create(description, kind = "sql") {
8695
- if (kind === "class") {
8696
- return createClassMigration(description, { migrationsDir: this.dir });
8697
- }
8698
- return createMigration(description, { migrationsDir: this.dir });
8597
+ return createMigration(description, { migrationsDir: this.dir, kind });
8699
8598
  }
8700
8599
  /** Return list of completed (applied) migration filenames. */
8701
8600
  async getApplied() {
@@ -8938,15 +8837,8 @@ function generateCrudRoutes(models, options = {}) {
8938
8837
  const total = Number(countRow[0]?.total ?? 0);
8939
8838
  const limit = qp.limit ?? 100;
8940
8839
  const page = qp.page ?? 1;
8941
- res.json({
8942
- data: rows,
8943
- meta: {
8944
- total,
8945
- page,
8946
- limit,
8947
- totalPages: Math.ceil(total / limit)
8948
- }
8949
- });
8840
+ const offset = (page - 1) * limit;
8841
+ res.json(new DatabaseResult(rows, void 0, total, limit, offset).toPaginate());
8950
8842
  }
8951
8843
  });
8952
8844
  routes.push({
@@ -9100,6 +8992,7 @@ var init_autoCrud = __esm({
9100
8992
  "../orm/src/autoCrud.ts"() {
9101
8993
  "use strict";
9102
8994
  init_database();
8995
+ init_databaseResult();
9103
8996
  init_query();
9104
8997
  init_validation();
9105
8998
  AutoCrud = class _AutoCrud {
@@ -9354,17 +9247,19 @@ var init_queryBuilder = __esm({
9354
9247
  this.ensureDb();
9355
9248
  const sql = this.toSql();
9356
9249
  const allParams = [...this.params, ...this.havingParams];
9250
+ const queryParams = allParams.length > 0 ? allParams : void 0;
9357
9251
  const rows = await adapterFetch(
9358
9252
  this.db,
9359
9253
  sql,
9360
- allParams.length > 0 ? allParams : void 0,
9254
+ queryParams,
9361
9255
  this.limitVal,
9362
9256
  this.offsetVal
9363
9257
  );
9258
+ const total = await probeTotal(this.db, sql, queryParams, this.limitVal);
9364
9259
  return new DatabaseResult(
9365
9260
  rows,
9366
9261
  void 0,
9367
- void 0,
9262
+ total,
9368
9263
  this.limitVal,
9369
9264
  this.offsetVal,
9370
9265
  this.db,
@@ -12941,7 +12836,6 @@ __export(src_exports, {
12941
12836
  DatabaseUrl: () => DatabaseUrl,
12942
12837
  DocStoreDriverMissing: () => DocStoreDriverMissing,
12943
12838
  FakeData: () => FakeData2,
12944
- FetchResult: () => FetchResult,
12945
12839
  FirebirdAdapter: () => FirebirdAdapter,
12946
12840
  InvalidId: () => InvalidId,
12947
12841
  LocalStorage: () => LocalStorage,
@@ -13041,7 +12935,6 @@ __export(src_exports, {
13041
12935
  var init_src = __esm({
13042
12936
  "../orm/src/index.ts"() {
13043
12937
  "use strict";
13044
- init_types();
13045
12938
  init_databaseResult();
13046
12939
  init_database();
13047
12940
  init_database();
@@ -13451,7 +13344,7 @@ ${s}\r
13451
13344
  connect() {
13452
13345
  if (this.connected) return Promise.resolve();
13453
13346
  if (this.connecting) return this.connecting;
13454
- this.connecting = new Promise((resolve31, reject) => {
13347
+ this.connecting = new Promise((resolve30, reject) => {
13455
13348
  const sock = net.createConnection({ host: this.host, port: this.port });
13456
13349
  sock.setNoDelay(true);
13457
13350
  const onError = (err) => {
@@ -13488,7 +13381,7 @@ ${s}\r
13488
13381
  sock.on("error", (e) => {
13489
13382
  this.brokenError = e;
13490
13383
  });
13491
- resolve31();
13384
+ resolve30();
13492
13385
  } catch (e) {
13493
13386
  onError(e);
13494
13387
  }
@@ -13551,12 +13444,12 @@ ${s}\r
13551
13444
  }
13552
13445
  /** Send one command and await its reply (assumes socket is up). */
13553
13446
  raw(args) {
13554
- return new Promise((resolve31, reject) => {
13447
+ return new Promise((resolve30, reject) => {
13555
13448
  if (!this.sock || this.sock.destroyed) {
13556
13449
  reject(this.brokenError ?? new Error("redis socket not connected"));
13557
13450
  return;
13558
13451
  }
13559
- this.waiters.push({ resolve: resolve31, reject });
13452
+ this.waiters.push({ resolve: resolve30, reject });
13560
13453
  this.sock.write(_RespClient.encode(args));
13561
13454
  });
13562
13455
  }
@@ -13898,7 +13791,7 @@ ${s}\r
13898
13791
  connect() {
13899
13792
  if (this.connected) return Promise.resolve();
13900
13793
  if (this.connecting) return this.connecting;
13901
- this.connecting = new Promise((resolve31, reject) => {
13794
+ this.connecting = new Promise((resolve30, reject) => {
13902
13795
  const sock = net.createConnection({ host: this.host, port: this.port });
13903
13796
  sock.setNoDelay(true);
13904
13797
  sock.once("error", (err) => {
@@ -13919,7 +13812,7 @@ ${s}\r
13919
13812
  p.resolve(this.buffer.toString("utf-8"));
13920
13813
  }
13921
13814
  });
13922
- resolve31();
13815
+ resolve30();
13923
13816
  });
13924
13817
  });
13925
13818
  return this.connecting;
@@ -13952,13 +13845,13 @@ ${s}\r
13952
13845
  async send(payload, terminator) {
13953
13846
  await this.connect();
13954
13847
  if (!this.sock || this.sock.destroyed) return "";
13955
- return new Promise((resolve31) => {
13848
+ return new Promise((resolve30) => {
13956
13849
  this.buffer = Buffer.alloc(0);
13957
- this.pending = { terminator, resolve: resolve31 };
13850
+ this.pending = { terminator, resolve: resolve30 };
13958
13851
  const timer = setTimeout(() => {
13959
- if (this.pending && this.pending.resolve === resolve31) {
13852
+ if (this.pending && this.pending.resolve === resolve30) {
13960
13853
  this.pending = null;
13961
- resolve31(this.buffer.toString("utf-8"));
13854
+ resolve30(this.buffer.toString("utf-8"));
13962
13855
  }
13963
13856
  }, 4e3);
13964
13857
  if (timer.unref) timer.unref();
@@ -15484,16 +15377,27 @@ async function parseBody(req2) {
15484
15377
  }
15485
15378
  const contentType = req2.headers["content-type"] ?? "";
15486
15379
  const chunks = [];
15487
- await new Promise((resolve31, reject) => {
15488
- req2.on("data", (chunk) => chunks.push(chunk));
15489
- req2.on("end", resolve31);
15380
+ await new Promise((resolve30, reject) => {
15381
+ let received = 0;
15382
+ let refused = false;
15383
+ req2.on("data", (chunk) => {
15384
+ if (refused) return;
15385
+ received += chunk.length;
15386
+ if (received > TINA4_MAX_UPLOAD_SIZE) {
15387
+ refused = true;
15388
+ chunks.length = 0;
15389
+ reject(new PayloadTooLargeError(received, TINA4_MAX_UPLOAD_SIZE));
15390
+ return;
15391
+ }
15392
+ chunks.push(chunk);
15393
+ });
15394
+ req2.on("end", () => {
15395
+ if (!refused) resolve30();
15396
+ });
15490
15397
  req2.on("error", reject);
15491
15398
  });
15492
15399
  const raw = Buffer.concat(chunks);
15493
15400
  if (raw.length === 0) return;
15494
- if (raw.length > TINA4_MAX_UPLOAD_SIZE) {
15495
- throw new PayloadTooLargeError(raw.length, TINA4_MAX_UPLOAD_SIZE);
15496
- }
15497
15401
  if (contentType.includes("multipart/form-data")) {
15498
15402
  const boundary = extractBoundary(contentType);
15499
15403
  if (boundary) {
@@ -17442,10 +17346,17 @@ var init_session = __esm({
17442
17346
  *
17443
17347
  * session.flash("message", "Saved!") // set
17444
17348
  * session.flash("message") // get + auto-remove → "Saved!"
17349
+ * session.flash("message", null) // get + auto-remove (null is a GET sentinel)
17350
+ *
17351
+ * `null` — NOT just `undefined` — is the GET sentinel, so `flash(key, null)`
17352
+ * READS and clears rather than STORING null. This matches the Python master
17353
+ * (`if value is not None`), PHP (`if ($value !== null)`) and Ruby
17354
+ * (`if value.nil?`): passing the language's "no value" literal means GET. A
17355
+ * caller wanting to persist an explicit null should store it with `set()`.
17445
17356
  */
17446
17357
  flash(key, value) {
17447
17358
  const flashKey = `${FLASH_PREFIX}${key}`;
17448
- if (value !== void 0) {
17359
+ if (value !== void 0 && value !== null) {
17449
17360
  this.set(flashKey, value);
17450
17361
  return void 0;
17451
17362
  }
@@ -23327,14 +23238,14 @@ data: ${channel.buffer.shift()}
23327
23238
  `;
23328
23239
  continue;
23329
23240
  }
23330
- const gotMessage = await new Promise((resolve31) => {
23241
+ const gotMessage = await new Promise((resolve30) => {
23331
23242
  const timer = setTimeout(() => {
23332
23243
  channel.wake = null;
23333
- resolve31(false);
23244
+ resolve30(false);
23334
23245
  }, keepaliveMs);
23335
23246
  channel.wake = () => {
23336
23247
  clearTimeout(timer);
23337
- resolve31(true);
23248
+ resolve30(true);
23338
23249
  };
23339
23250
  });
23340
23251
  if (!gotMessage) yield `: keep-alive
@@ -25849,7 +25760,7 @@ var init_websocket = __esm({
25849
25760
  * Start the WebSocket server.
25850
25761
  */
25851
25762
  async start() {
25852
- return new Promise((resolve31, reject) => {
25763
+ return new Promise((resolve30, reject) => {
25853
25764
  this.server = createServer((req2, res) => {
25854
25765
  res.writeHead(426, { "Content-Type": "text/plain" });
25855
25766
  res.end("Upgrade Required");
@@ -25859,7 +25770,7 @@ var init_websocket = __esm({
25859
25770
  });
25860
25771
  this.server.listen(this.port, () => {
25861
25772
  this.startIdleReaper();
25862
- resolve31();
25773
+ resolve30();
25863
25774
  });
25864
25775
  this.server.on("error", (err) => {
25865
25776
  this.emit("error", err);
@@ -26423,7 +26334,7 @@ var init_websocket = __esm({
26423
26334
  client.trackerId = this.onAdd(socket.remoteAddress ?? "unknown", "/__dev_reload");
26424
26335
  }
26425
26336
  this.clients.add(client);
26426
- const cleanup2 = () => {
26337
+ const cleanup = () => {
26427
26338
  if (!this.clients.has(client)) return;
26428
26339
  this.clients.delete(client);
26429
26340
  if (client.trackerId && this.onRemove) this.onRemove(client.trackerId);
@@ -26446,13 +26357,13 @@ var init_websocket = __esm({
26446
26357
  socket.end();
26447
26358
  } catch {
26448
26359
  }
26449
- cleanup2();
26360
+ cleanup();
26450
26361
  return;
26451
26362
  }
26452
26363
  }
26453
26364
  });
26454
- socket.on("close", cleanup2);
26455
- socket.on("error", cleanup2);
26365
+ socket.on("close", cleanup);
26366
+ socket.on("error", cleanup);
26456
26367
  return true;
26457
26368
  }
26458
26369
  /**
@@ -27970,7 +27881,7 @@ var init_queue = __esm({
27970
27881
  const jobs = this.popBatch(resolvedBatchSize);
27971
27882
  if (jobs.length === 0) {
27972
27883
  if (resolvedPollInterval <= 0) break;
27973
- await new Promise((resolve31) => setTimeout(resolve31, resolvedPollInterval));
27884
+ await new Promise((resolve30) => setTimeout(resolve30, resolvedPollInterval));
27974
27885
  continue;
27975
27886
  }
27976
27887
  yield jobs;
@@ -27980,7 +27891,7 @@ var init_queue = __esm({
27980
27891
  const raw = this.pop();
27981
27892
  if (raw === null) {
27982
27893
  if (resolvedPollInterval <= 0) break;
27983
- await new Promise((resolve31) => setTimeout(resolve31, resolvedPollInterval));
27894
+ await new Promise((resolve30) => setTimeout(resolve30, resolvedPollInterval));
27984
27895
  continue;
27985
27896
  }
27986
27897
  yield createJob(raw, this);
@@ -32500,23 +32411,23 @@ var init_devAdmin = __esm({
32500
32411
  });
32501
32412
  };
32502
32413
  handleDevAdminJs = async (_req, res) => {
32503
- const { readFileSync: readFileSync29, existsSync: existsSync37 } = await import("node:fs");
32504
- const { dirname: dirname16, join: join39, resolve: resolve31 } = await import("node:path");
32414
+ const { readFileSync: readFileSync28, existsSync: existsSync36 } = await import("node:fs");
32415
+ const { dirname: dirname15, join: join38, resolve: resolve30 } = await import("node:path");
32505
32416
  const { fileURLToPath: fileURLToPath9 } = await import("node:url");
32506
- const dir = dirname16(fileURLToPath9(import.meta.url));
32417
+ const dir = dirname15(fileURLToPath9(import.meta.url));
32507
32418
  const candidates = [
32508
- join39(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
32419
+ join38(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
32509
32420
  // src/../public/js/
32510
- join39(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
32421
+ join38(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
32511
32422
  // deeper nesting
32512
- resolve31(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
32513
- resolve31(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
32423
+ resolve30(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
32424
+ resolve30(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
32514
32425
  // project public/
32515
32426
  ];
32516
32427
  for (const jsPath of candidates) {
32517
- if (existsSync37(jsPath)) {
32428
+ if (existsSync36(jsPath)) {
32518
32429
  try {
32519
- const content = readFileSync29(jsPath, "utf-8");
32430
+ const content = readFileSync28(jsPath, "utf-8");
32520
32431
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
32521
32432
  res.raw.end(content);
32522
32433
  return;
@@ -32977,8 +32888,12 @@ function sanitizeSecurity(reqs, schemes) {
32977
32888
  function generate(routes, models = []) {
32978
32889
  const info = {
32979
32890
  title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
32980
- version: process.env.TINA4_SWAGGER_VERSION ?? "0.0.1",
32981
- description: process.env.TINA4_SWAGGER_DESCRIPTION ?? "Auto-generated API documentation"
32891
+ // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
32892
+ // 0.0.1). description defaults to the empty string, not a canned sentence.
32893
+ // Both are the settled cross-framework defaults (parity with the Python
32894
+ // master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
32895
+ version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
32896
+ description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
32982
32897
  };
32983
32898
  const contactEmail = (process.env.TINA4_SWAGGER_CONTACT_EMAIL ?? "").trim();
32984
32899
  const contactName = (process.env.TINA4_SWAGGER_CONTACT_TEAM ?? "").trim();
@@ -33011,9 +32926,11 @@ function generate(routes, models = []) {
33011
32926
  const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
33012
32927
  const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
33013
32928
  const refSchemas = /* @__PURE__ */ new Set();
32929
+ const tableToSchema = /* @__PURE__ */ new Map();
33014
32930
  for (const model of models) {
33015
- const schema = modelToSchema(model);
33016
- spec.components.schemas[model.tableName] = schema;
32931
+ const schemaKey = schemaNameForModel(model);
32932
+ tableToSchema.set(model.tableName, schemaKey);
32933
+ spec.components.schemas[schemaKey] = modelToSchema(model);
33017
32934
  }
33018
32935
  const usedTags = [];
33019
32936
  const seenIds = /* @__PURE__ */ new Set();
@@ -33040,11 +32957,11 @@ function generate(routes, models = []) {
33040
32957
  if (route.meta?.deprecated) operation.deprecated = true;
33041
32958
  const pathParams = extractPathParams(route.pattern);
33042
32959
  if (pathParams.length > 0) {
33043
- operation.parameters = pathParams.map((name) => ({
32960
+ operation.parameters = pathParams.map(({ name, schema }) => ({
33044
32961
  name,
33045
32962
  in: "path",
33046
32963
  required: true,
33047
- schema: { type: "string" }
32964
+ schema
33048
32965
  }));
33049
32966
  }
33050
32967
  if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
@@ -33070,19 +32987,20 @@ function generate(routes, models = []) {
33070
32987
  };
33071
32988
  } else if (method === "post" || method === "put") {
33072
32989
  const modelName = inferModelFromPath(route.pattern);
33073
- if (modelName && models.some((m) => m.tableName === modelName)) {
33074
- const media = {
33075
- schema: { $ref: `#/components/schemas/${modelName}` }
33076
- };
32990
+ const schemaKey = modelName ? tableToSchema.get(modelName) : void 0;
32991
+ if (schemaKey) {
32992
+ const sref = `#/components/schemas/${schemaKey}`;
32993
+ const media = { schema: { $ref: sref } };
33077
32994
  if (route.meta?.example !== void 0) media.example = route.meta.example;
33078
32995
  operation.requestBody = {
33079
32996
  required: true,
33080
32997
  content: { "application/json": media }
33081
32998
  };
33082
- operation.responses = {
33083
- ...method === "post" ? { "201": { description: "Created", content: { "application/json": { schema: { $ref: `#/components/schemas/${modelName}` } } } } } : { "200": { description: "Updated", content: { "application/json": { schema: { $ref: `#/components/schemas/${modelName}` } } } } },
33084
- "422": { description: "Validation failed" }
33085
- };
32999
+ if (route.meta?.responses === void 0) {
33000
+ operation.responses = {
33001
+ "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
33002
+ };
33003
+ }
33086
33004
  } else if (route.meta?.example !== void 0) {
33087
33005
  operation.requestBody = {
33088
33006
  content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
@@ -33169,6 +33087,21 @@ function resolveServers() {
33169
33087
  const dev = (process.env.SWAGGER_DEV_URL ?? "").trim();
33170
33088
  return dev.length > 0 ? [{ url: dev }] : [{ url: "/" }];
33171
33089
  }
33090
+ function schemaNameForModel(model) {
33091
+ const explicit = model.className?.trim();
33092
+ if (explicit) return explicit;
33093
+ return deriveClassName(model.tableName);
33094
+ }
33095
+ function deriveClassName(tableName) {
33096
+ return singularize(tableName).split(/[_\s-]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || tableName;
33097
+ }
33098
+ function singularize(word) {
33099
+ if (/ies$/i.test(word) && word.length > 3) return word.slice(0, -3) + "y";
33100
+ if (/(ses|xes|zes|ches|shes)$/i.test(word)) return word.slice(0, -2);
33101
+ if (/ss$/i.test(word)) return word;
33102
+ if (/s$/i.test(word) && word.length > 1) return word.slice(0, -1);
33103
+ return word;
33104
+ }
33172
33105
  function modelToSchema(model) {
33173
33106
  const properties = {};
33174
33107
  const required = [];
@@ -33242,15 +33175,35 @@ function inferSchema(value) {
33242
33175
  if (typeof value === "number") return { type: Number.isInteger(value) ? "integer" : "number" };
33243
33176
  return { type: "string" };
33244
33177
  }
33178
+ function segmentParam(segment) {
33179
+ if (segment.startsWith("{") && segment.endsWith("}")) {
33180
+ const inner = segment.slice(1, -1);
33181
+ if (inner.startsWith("...")) return { name: inner.slice(3), type: "string" };
33182
+ const colon = inner.indexOf(":");
33183
+ if (colon >= 0) return { name: inner.slice(0, colon), type: inner.slice(colon + 1) };
33184
+ return { name: inner, type: "string" };
33185
+ }
33186
+ if (segment.startsWith("[") && segment.endsWith("]")) {
33187
+ const inner = segment.slice(1, -1);
33188
+ return { name: inner.startsWith("...") ? inner.slice(3) : inner, type: "string" };
33189
+ }
33190
+ if (segment.startsWith(":") && segment.length > 1) {
33191
+ return { name: segment.slice(1), type: "string" };
33192
+ }
33193
+ return null;
33194
+ }
33245
33195
  function patternToOpenAPI(pattern) {
33246
- return pattern.replace(/\[\.\.\.(\w+)\]/g, "{$1}").replace(/\[(\w+)\]/g, "{$1}");
33196
+ return pattern.split("/").map((segment) => {
33197
+ const p = segmentParam(segment);
33198
+ return p ? `{${p.name}}` : segment;
33199
+ }).join("/");
33247
33200
  }
33248
33201
  function extractPathParams(pattern) {
33249
33202
  const params = [];
33250
- const regex = /\[(?:\.\.\.)?(\w+)\]/g;
33251
- let match;
33252
- while ((match = regex.exec(pattern)) !== null) {
33253
- params.push(match[1]);
33203
+ for (const segment of pattern.split("/")) {
33204
+ const p = segmentParam(segment);
33205
+ if (!p) continue;
33206
+ params.push({ name: p.name, schema: { ...PARAM_TYPE_SCHEMA[p.type] ?? { type: "string" } } });
33254
33207
  }
33255
33208
  return params;
33256
33209
  }
@@ -33272,8 +33225,12 @@ function inferModelFromPath(pattern) {
33272
33225
  if (rest.length === 1 && /^[[{]\.{0,3}\w+[\]}]$/.test(rest[0])) return candidate;
33273
33226
  return null;
33274
33227
  }
33228
+ function operationIdBase(method, openApiPath) {
33229
+ const clean = openApiPath.replace(/^\/+|\/+$/g, "").replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
33230
+ return clean ? `${method}_${clean}` : method;
33231
+ }
33275
33232
  function uniqueOperationId(method, openApiPath, seen) {
33276
- const base = (method + openApiPath.replace(/[/{}]/g, "_")).replace(/_+/g, "_").replace(/_$/, "");
33233
+ const base = operationIdBase(method, openApiPath);
33277
33234
  let oid = base;
33278
33235
  let n = 2;
33279
33236
  while (seen.has(oid)) {
@@ -33283,13 +33240,25 @@ function uniqueOperationId(method, openApiPath, seen) {
33283
33240
  seen.add(oid);
33284
33241
  return oid;
33285
33242
  }
33286
- var WRITE_METHODS, registeredSchemes, registeredSchemas;
33243
+ var WRITE_METHODS, registeredSchemes, registeredSchemas, PARAM_TYPE_SCHEMA;
33287
33244
  var init_generator = __esm({
33288
33245
  "../swagger/src/generator.ts"() {
33289
33246
  "use strict";
33290
33247
  WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
33291
33248
  registeredSchemes = {};
33292
33249
  registeredSchemas = {};
33250
+ PARAM_TYPE_SCHEMA = {
33251
+ int: { type: "integer" },
33252
+ integer: { type: "integer" },
33253
+ float: { type: "number" },
33254
+ number: { type: "number" },
33255
+ uuid: { type: "string", format: "uuid" },
33256
+ slug: { type: "string", pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
33257
+ alpha: { type: "string", pattern: "^[A-Za-z]+$" },
33258
+ alnum: { type: "string", pattern: "^[A-Za-z0-9]+$" },
33259
+ path: { type: "string" },
33260
+ string: { type: "string" }
33261
+ };
33293
33262
  }
33294
33263
  });
33295
33264
 
@@ -33609,10 +33578,29 @@ function openBrowser(url) {
33609
33578
  }, 2e3);
33610
33579
  }
33611
33580
  function resolvePortAndHost(config) {
33612
- const port = config?.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? 7148;
33581
+ const tina4Port = process.env.TINA4_PORT;
33582
+ const legacyPort = process.env.PORT;
33583
+ let port;
33584
+ if (config?.port !== void 0) {
33585
+ port = config.port;
33586
+ } else if (tina4Port && /^\d+$/.test(tina4Port)) {
33587
+ port = parseInt(tina4Port, 10);
33588
+ } else if (legacyPort && /^\d+$/.test(legacyPort)) {
33589
+ port = parseInt(legacyPort, 10);
33590
+ warnDeprecatedPort(port);
33591
+ } else {
33592
+ port = 7148;
33593
+ }
33613
33594
  const host = config?.host ?? process.env.TINA4_HOST ?? process.env.HOST ?? "0.0.0.0";
33614
33595
  return { port, host };
33615
33596
  }
33597
+ function warnDeprecatedPort(port) {
33598
+ if (portDeprecationWarned) return;
33599
+ portDeprecationWarned = true;
33600
+ Log.warning(
33601
+ `PORT is deprecated and will be removed in 3.14 - use TINA4_PORT instead (binding port ${port} from PORT)`
33602
+ );
33603
+ }
33616
33604
  function isBannerSuppressed() {
33617
33605
  return isTruthy(process.env.TINA4_SUPPRESS);
33618
33606
  }
@@ -33884,6 +33872,29 @@ function deployGallery(name) {
33884
33872
  </body>
33885
33873
  </html>`;
33886
33874
  }
33875
+ function startLoopWatchdog() {
33876
+ const raw = (process.env.TINA4_LOOP_LAG_WARN_MS ?? "").trim();
33877
+ const threshold = /^\d+$/.test(raw) ? parseInt(raw, 10) : 250;
33878
+ if (threshold <= 0) {
33879
+ return { stop: () => {
33880
+ } };
33881
+ }
33882
+ let last = Date.now();
33883
+ let warned = 0;
33884
+ const timer = setInterval(() => {
33885
+ const now = Date.now();
33886
+ const lag = now - last - LOOP_WATCHDOG_TICK_MS;
33887
+ last = now;
33888
+ if (lag < threshold) return;
33889
+ warned++;
33890
+ if (warned > 5 && warned % 20 !== 0) return;
33891
+ Log.warning(
33892
+ `Event loop blocked for ${lag}ms. Node serves every request on one loop, so a handler doing CPU-bound work or synchronous I/O stalls all the others for that long. Move the work to Tina4's queue, or await it. Set TINA4_LOOP_LAG_WARN_MS to change the ${threshold}ms threshold, or 0 to silence.`
33893
+ );
33894
+ }, LOOP_WATCHDOG_TICK_MS);
33895
+ timer.unref();
33896
+ return { stop: () => clearInterval(timer) };
33897
+ }
33887
33898
  async function start(config) {
33888
33899
  const isManaged = process.argv.includes("--managed");
33889
33900
  if (!isManaged && process.env.TINA4_OVERRIDE_CLIENT !== "true") {
@@ -34133,7 +34144,9 @@ async function startServer(config) {
34133
34144
  const resolved = resolvePortAndHost(config);
34134
34145
  const host = resolved.host;
34135
34146
  let port = resolved.port;
34136
- port = findAvailablePort(port);
34147
+ if (!cluster.isWorker) {
34148
+ port = findAvailablePort(port);
34149
+ }
34137
34150
  const isProduction = (process.env.TINA4_PRODUCTION ?? "").toLowerCase() === "true";
34138
34151
  if (cluster.isPrimary && isProduction) {
34139
34152
  const numCPUs = os2.cpus().length;
@@ -34349,7 +34362,20 @@ ${reset2}
34349
34362
  await sessionAutoStart(rawReq, rawRes, req2);
34350
34363
  await middleware.run(req2, res);
34351
34364
  if (res.raw.writableEnded) return;
34352
- await req2.parseBody();
34365
+ try {
34366
+ await req2.parseBody();
34367
+ } catch (err) {
34368
+ const status2 = err?.statusCode;
34369
+ if (typeof status2 === "number" && status2 >= 400 && status2 < 500) {
34370
+ if (!rawRes.writableEnded) {
34371
+ rawRes.statusCode = status2;
34372
+ rawRes.setHeader("content-type", "application/json");
34373
+ rawRes.end(JSON.stringify({ error: err.message }));
34374
+ }
34375
+ return;
34376
+ }
34377
+ throw err;
34378
+ }
34353
34379
  const pathname = req2.path;
34354
34380
  const reqStartTime = DevAdmin.isEnabled() ? Date.now() : 0;
34355
34381
  const matchedPattern = { value: "" };
@@ -34548,8 +34574,10 @@ ${reset2}
34548
34574
  };
34549
34575
  process.on("SIGTERM", onSigterm);
34550
34576
  process.on("SIGINT", onSigint);
34577
+ const loopWatchdog = startLoopWatchdog();
34551
34578
  resolvePromise({
34552
34579
  close: () => {
34580
+ loopWatchdog.stop();
34553
34581
  process.off("SIGTERM", onSigterm);
34554
34582
  process.off("SIGINT", onSigint);
34555
34583
  stopAllBackgroundTasks();
@@ -34564,7 +34592,7 @@ ${reset2}
34564
34592
  });
34565
34593
  });
34566
34594
  }
34567
- var __filename, __dirname, BUILTIN_ERROR_TEMPLATES_DIR, BUILTIN_PUBLIC_DIR, swaggerAssetsEnabled, DEFAULT_SHUTDOWN_TIMEOUT_SECONDS, TINA4_VERSION2, frondCache, _LEGACY_ENV_VARS, TEMPLATE_PAGES_DIR, HTTP_REASON_PHRASES, templateCache, _dispatchFn, _serverHandle, FALLBACK_STAGES;
34595
+ var __filename, __dirname, BUILTIN_ERROR_TEMPLATES_DIR, BUILTIN_PUBLIC_DIR, swaggerAssetsEnabled, DEFAULT_SHUTDOWN_TIMEOUT_SECONDS, TINA4_VERSION2, frondCache, _LEGACY_ENV_VARS, portDeprecationWarned, TEMPLATE_PAGES_DIR, HTTP_REASON_PHRASES, templateCache, _dispatchFn, _serverHandle, LOOP_WATCHDOG_TICK_MS, FALLBACK_STAGES;
34568
34596
  var init_server = __esm({
34569
34597
  "../core/src/server.ts"() {
34570
34598
  "use strict";
@@ -34617,6 +34645,7 @@ var init_server = __esm({
34617
34645
  SWAGGER_VERSION: "TINA4_SWAGGER_VERSION",
34618
34646
  ORM_PLURAL_TABLE_NAMES: "TINA4_ORM_PLURAL_TABLE_NAMES"
34619
34647
  };
34648
+ portDeprecationWarned = false;
34620
34649
  TEMPLATE_PAGES_DIR = "pages";
34621
34650
  HTTP_REASON_PHRASES = {
34622
34651
  100: "Continue",
@@ -34653,6 +34682,7 @@ var init_server = __esm({
34653
34682
  templateCache = null;
34654
34683
  _dispatchFn = null;
34655
34684
  _serverHandle = null;
34685
+ LOOP_WATCHDOG_TICK_MS = 100;
34656
34686
  FALLBACK_STAGES = [
34657
34687
  serveTemplateFallback,
34658
34688
  serveLandingPage,
@@ -34740,433 +34770,6 @@ var init_env = __esm({
34740
34770
  }
34741
34771
  });
34742
34772
 
34743
- // ../core/src/scss.ts
34744
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync19, readdirSync as readdirSync18 } from "node:fs";
34745
- import { join as join28, resolve as resolve20, dirname as dirname13 } from "node:path";
34746
- function compileString(scss, importPaths, variables) {
34747
- const imported = /* @__PURE__ */ new Set();
34748
- scss = resolveImports(scss, importPaths, imported);
34749
- scss = scss.replace(/(?<![:"'])\/\/[^\n]*/g, "");
34750
- scss = extractVariables(scss, variables);
34751
- const mixins = {};
34752
- scss = extractMixins(scss, mixins);
34753
- scss = resolveIncludes(scss, mixins);
34754
- scss = resolveInterpolation(scss, variables);
34755
- scss = substituteVariables(scss, variables);
34756
- scss = evalMath(scss);
34757
- scss = resolveColorFunctions(scss);
34758
- const css = flattenNesting(scss);
34759
- return cleanup(css);
34760
- }
34761
- function resolveImports(content, paths, imported) {
34762
- return content.replace(/@import\s+["']?([^"';\n]+)["']?\s*;/g, (_match, name) => {
34763
- name = name.trim();
34764
- const candidates = [];
34765
- for (const base of paths) {
34766
- candidates.push(
34767
- join28(base, `${name}.scss`),
34768
- join28(base, `_${name}.scss`),
34769
- join28(base, name)
34770
- );
34771
- }
34772
- for (const candidate of candidates) {
34773
- if (existsSync25(candidate) && !imported.has(candidate)) {
34774
- imported.add(candidate);
34775
- const fileContent = readFileSync23(candidate, "utf-8");
34776
- return resolveImports(fileContent, [dirname13(candidate), ...paths], imported);
34777
- }
34778
- }
34779
- return `/* IMPORT NOT FOUND: ${name} */`;
34780
- });
34781
- }
34782
- function stripVariableFlags(value) {
34783
- let declaresDefault = false;
34784
- for (; ; ) {
34785
- const match = VARIABLE_FLAG.exec(value);
34786
- if (match === null) return [value.trim(), declaresDefault];
34787
- if (match[1] === "default") declaresDefault = true;
34788
- value = value.slice(0, match.index);
34789
- }
34790
- }
34791
- function extractVariables(scss, variables) {
34792
- return scss.replace(/\$([a-zA-Z_][\w-]*)\s*:\s*([^;]+);/g, (_m, name, value) => {
34793
- const [stripped, declaresDefault] = stripVariableFlags(value.trim());
34794
- if (declaresDefault && (variables[name] ?? "null") !== "null") {
34795
- return "";
34796
- }
34797
- let resolved = stripped;
34798
- for (const [vName, vVal] of Object.entries(variables)) {
34799
- resolved = resolved.replaceAll(`$${vName}`, vVal);
34800
- }
34801
- variables[name] = resolved;
34802
- return "";
34803
- });
34804
- }
34805
- function substituteVariables(scss, variables) {
34806
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
34807
- for (const name of sorted) {
34808
- scss = scss.replaceAll(`$${name}`, variables[name]);
34809
- }
34810
- return scss;
34811
- }
34812
- function resolveInterpolation(scss, variables) {
34813
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
34814
- return scss.replace(/#\{([^{}]*)\}/g, (_m, inner) => {
34815
- let resolved = inner.trim();
34816
- for (const name of sorted) {
34817
- resolved = resolved.replaceAll(`$${name}`, variables[name]);
34818
- }
34819
- return resolved;
34820
- });
34821
- }
34822
- function extractMixins(scss, mixins) {
34823
- const pattern = /@mixin\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*\{/g;
34824
- let match;
34825
- const locations = [];
34826
- while ((match = pattern.exec(scss)) !== null) {
34827
- const name = match[1];
34828
- const paramsStr = match[2] ?? "";
34829
- const params = paramsStr.split(",").map((p) => p.trim().replace(/^\$/, "")).filter(Boolean);
34830
- const bodyStart = match.index + match[0].length;
34831
- const body = findBlock(scss, bodyStart);
34832
- if (body !== null) {
34833
- mixins[name] = { params, body };
34834
- locations.push({
34835
- start: match.index,
34836
- end: bodyStart + body.length + 1,
34837
- name
34838
- });
34839
- }
34840
- }
34841
- let result = scss;
34842
- for (const loc of locations.reverse()) {
34843
- result = result.slice(0, loc.start) + result.slice(loc.end);
34844
- }
34845
- return result;
34846
- }
34847
- function resolveIncludes(scss, mixins) {
34848
- return scss.replace(
34849
- /@include\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*;/g,
34850
- (_m, name, argsStr) => {
34851
- if (!(name in mixins)) {
34852
- return `/* MIXIN NOT FOUND: ${name} */`;
34853
- }
34854
- const mixin = mixins[name];
34855
- const args = argsStr ? argsStr.split(",").map((a) => a.trim()).filter(Boolean) : [];
34856
- let body = mixin.body;
34857
- for (let i = 0; i < mixin.params.length; i++) {
34858
- const paramName = mixin.params[i].split(":")[0].trim();
34859
- const defaultVal = mixin.params[i].includes(":") ? mixin.params[i].split(":").slice(1).join(":").trim() : "";
34860
- const value = i < args.length ? args[i] : defaultVal;
34861
- body = body.replaceAll(`$${paramName}`, value);
34862
- }
34863
- return body;
34864
- }
34865
- );
34866
- }
34867
- function evalMath(scss) {
34868
- const placeholders = [];
34869
- const masked = scss.replace(/calc\([^()]*\)/g, (m) => {
34870
- placeholders.push(m);
34871
- return `\0CALC${placeholders.length - 1}\0`;
34872
- });
34873
- const folded = masked.replace(
34874
- /([\d.]+)([a-z%]*)\s*([+\-*/])\s*([\d.]+)([a-z%]*)/g,
34875
- (full, n1, u1, op, n2, u2) => {
34876
- const num1 = parseFloat(n1);
34877
- const num2 = parseFloat(n2);
34878
- if (Number.isNaN(num1) || Number.isNaN(num2)) return full;
34879
- const unit1 = u1 || "";
34880
- const unit2 = u2 || "";
34881
- let unit;
34882
- if (unit1 === unit2) {
34883
- unit = unit1;
34884
- } else if ((op === "*" || op === "/") && unit1 === "") {
34885
- unit = unit2;
34886
- } else if ((op === "*" || op === "/") && unit2 === "") {
34887
- unit = unit1;
34888
- } else {
34889
- return full;
34890
- }
34891
- let result;
34892
- switch (op) {
34893
- case "+":
34894
- result = num1 + num2;
34895
- break;
34896
- case "-":
34897
- result = num1 - num2;
34898
- break;
34899
- case "*":
34900
- result = num1 * num2;
34901
- break;
34902
- case "/":
34903
- if (num2 === 0) return full;
34904
- result = num1 / num2;
34905
- break;
34906
- default:
34907
- return full;
34908
- }
34909
- if (result === Math.floor(result)) {
34910
- return `${Math.floor(result)}${unit}`;
34911
- }
34912
- return `${result.toFixed(2)}${unit}`;
34913
- }
34914
- );
34915
- return folded.replace(/\x00CALC(\d+)\x00/g, (_m, idx) => {
34916
- return placeholders[parseInt(idx, 10)];
34917
- });
34918
- }
34919
- function resolveColorFunctions(scss) {
34920
- scss = scss.replace(
34921
- /lighten\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g,
34922
- (_m, color, amt) => adjustLightness(color.trim(), parseFloat(amt.trim().replace(/%$/, "")) / 100)
34923
- );
34924
- scss = scss.replace(
34925
- /darken\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g,
34926
- (_m, color, amt) => adjustLightness(color.trim(), -(parseFloat(amt.trim().replace(/%$/, "")) / 100))
34927
- );
34928
- scss = scss.replace(/rgba\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*([\d.]+)\s*\)/g, (whole, hex, alpha) => {
34929
- const rgb = hexToRgb(hex);
34930
- return rgb === null ? whole : `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha.trim()})`;
34931
- });
34932
- scss = scss.replace(/rgb\(\s*(#[0-9a-fA-F]{3,8})\s*\)/g, (whole, hex) => {
34933
- const rgb = hexToRgb(hex);
34934
- return rgb === null ? whole : `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
34935
- });
34936
- scss = scss.replace(
34937
- /mix\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*(#[0-9a-fA-F]{3,8})\s*(?:,\s*([\d.]+%?)\s*)?\)/g,
34938
- (whole, h1, h2, weight) => {
34939
- const c1 = hexToRgb(h1);
34940
- const c2 = hexToRgb(h2);
34941
- if (c1 === null || c2 === null) return whole;
34942
- const w = weight ? parseFloat(weight.replace(/%$/, "")) / 100 : 0.5;
34943
- const mixed = [0, 1, 2].map((i) => Math.round(c1[i] * w + c2[i] * (1 - w)));
34944
- return `#${mixed.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
34945
- }
34946
- );
34947
- return scss;
34948
- }
34949
- function hexToRgb(color) {
34950
- let c = color.trim().replace(/^#/, "");
34951
- if (c.length === 3) {
34952
- c = c.split("").map((ch) => ch + ch).join("");
34953
- }
34954
- if (!/^[0-9a-fA-F]{6}$/.test(c)) return null;
34955
- return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
34956
- }
34957
- function adjustLightness(color, amount) {
34958
- const rgb = hexToRgb(color);
34959
- if (rgb === null) return color;
34960
- let [r, g, b] = rgb.map((v) => v / 255);
34961
- const max = Math.max(r, g, b);
34962
- const min = Math.min(r, g, b);
34963
- let l = (max + min) / 2;
34964
- const d = max - min;
34965
- let h = 0;
34966
- let s = 0;
34967
- if (d !== 0) {
34968
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
34969
- if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
34970
- else if (max === g) h = (b - r) / d + 2;
34971
- else h = (r - g) / d + 4;
34972
- h /= 6;
34973
- }
34974
- l = Math.max(0, Math.min(1, l + amount));
34975
- if (s === 0) {
34976
- r = g = b = l;
34977
- } else {
34978
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
34979
- const p = 2 * l - q;
34980
- r = hueToRgb(p, q, h + 1 / 3);
34981
- g = hueToRgb(p, q, h);
34982
- b = hueToRgb(p, q, h - 1 / 3);
34983
- }
34984
- const hex = (v) => Math.trunc(v * 255).toString(16).padStart(2, "0");
34985
- return `#${hex(r)}${hex(g)}${hex(b)}`;
34986
- }
34987
- function hueToRgb(p, q, t) {
34988
- if (t < 0) t += 1;
34989
- if (t > 1) t -= 1;
34990
- if (t < 1 / 6) return p + (q - p) * 6 * t;
34991
- if (t < 1 / 2) return q;
34992
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
34993
- return p;
34994
- }
34995
- function flattenNesting(scss) {
34996
- const output = [];
34997
- flattenBlock(scss, [], output);
34998
- return output.join("\n");
34999
- }
35000
- function flattenBlock(content, parentSelectors, output) {
35001
- let pos = 0;
35002
- const properties = [];
35003
- while (pos < content.length) {
35004
- while (pos < content.length && /[\s]/.test(content[pos])) {
35005
- pos++;
35006
- }
35007
- if (pos >= content.length) break;
35008
- if (content[pos] === "/" && content[pos + 1] === "*") {
35009
- const end = content.indexOf("*/", pos + 2);
35010
- if (end === -1) break;
35011
- output.push(content.slice(pos, end + 2));
35012
- pos = end + 2;
35013
- continue;
35014
- }
35015
- if (content.slice(pos, pos + 6) === "@media") {
35016
- const brace = content.indexOf("{", pos);
35017
- if (brace === -1) break;
35018
- const mediaQuery = content.slice(pos, brace).trim();
35019
- const body = findBlock(content, brace + 1);
35020
- if (body === null) break;
35021
- pos = brace + 1 + body.length + 1;
35022
- const innerOutput = [];
35023
- flattenBlock(body, parentSelectors, innerOutput);
35024
- if (innerOutput.length > 0) {
35025
- output.push(`${mediaQuery} {`);
35026
- for (const line of innerOutput) {
35027
- output.push(` ${line}`);
35028
- }
35029
- output.push("}");
35030
- }
35031
- continue;
35032
- }
35033
- const bracePos = content.indexOf("{", pos);
35034
- const semiPos = content.indexOf(";", pos);
35035
- if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
35036
- const prop = content.slice(pos, semiPos).trim();
35037
- if (prop && !prop.startsWith("@")) {
35038
- properties.push(prop);
35039
- }
35040
- pos = semiPos + 1;
35041
- continue;
35042
- }
35043
- if (bracePos !== -1) {
35044
- const selectorText = content.slice(pos, bracePos).trim();
35045
- const body = findBlock(content, bracePos + 1);
35046
- if (body === null) break;
35047
- pos = bracePos + 1 + body.length + 1;
35048
- if (!selectorText) continue;
35049
- const selectors = selectorText.split(",").map((s) => s.trim());
35050
- const newSelectors = [];
35051
- for (const sel of selectors) {
35052
- if (parentSelectors.length > 0) {
35053
- for (const parent of parentSelectors) {
35054
- if (sel.includes("&")) {
35055
- newSelectors.push(sel.replace(/&/g, parent));
35056
- } else {
35057
- newSelectors.push(`${parent} ${sel}`);
35058
- }
35059
- }
35060
- } else {
35061
- newSelectors.push(sel);
35062
- }
35063
- }
35064
- flattenBlock(body, newSelectors, output);
35065
- continue;
35066
- }
35067
- const remaining = content.slice(pos).trim();
35068
- if (remaining) {
35069
- properties.push(remaining);
35070
- }
35071
- break;
35072
- }
35073
- if (properties.length > 0 && parentSelectors.length > 0) {
35074
- const selectorStr = parentSelectors.join(", ");
35075
- output.push(`${selectorStr} {`);
35076
- for (const prop of properties) {
35077
- output.push(` ${prop};`);
35078
- }
35079
- output.push("}");
35080
- }
35081
- }
35082
- function findBlock(content, start2) {
35083
- let depth = 1;
35084
- let pos = start2;
35085
- while (pos < content.length && depth > 0) {
35086
- if (content[pos] === "{") depth++;
35087
- else if (content[pos] === "}") depth--;
35088
- if (depth > 0) pos++;
35089
- }
35090
- return depth === 0 ? content.slice(start2, pos) : null;
35091
- }
35092
- function cleanup(css) {
35093
- css = css.replace(/[^{}]+\{\s*\}/g, "");
35094
- css = css.replace(/\n{3,}/g, "\n\n");
35095
- css = css.split("\n").map((line) => line.trimEnd()).join("\n");
35096
- return css.trim() + "\n";
35097
- }
35098
- var ScssCompiler, VARIABLE_FLAG;
35099
- var init_scss = __esm({
35100
- "../core/src/scss.ts"() {
35101
- "use strict";
35102
- ScssCompiler = class {
35103
- _importPaths;
35104
- _variables;
35105
- constructor(config) {
35106
- this._importPaths = config?.importPaths ? [...config.importPaths] : [];
35107
- this._variables = config?.variables ? { ...config.variables } : {};
35108
- }
35109
- /** Compile an SCSS string to CSS. */
35110
- compile(source) {
35111
- return compileString(source, this._importPaths, { ...this._variables });
35112
- }
35113
- /** Compile an SCSS file to CSS. */
35114
- compileFile(filePath) {
35115
- const absPath = resolve20(filePath);
35116
- const content = readFileSync23(absPath, "utf-8");
35117
- const paths = [dirname13(absPath), ...this._importPaths];
35118
- return compileString(content, paths, { ...this._variables });
35119
- }
35120
- /** Add a directory to the import resolution path. */
35121
- addImportPath(path8) {
35122
- this._importPaths.push(resolve20(path8));
35123
- }
35124
- /** Set or override an SCSS variable. */
35125
- setVariable(name, value) {
35126
- const key = name.startsWith("$") ? name.slice(1) : name;
35127
- this._variables[key] = value;
35128
- }
35129
- /** Compile all .scss files in a directory into a single CSS output file. */
35130
- compileScss(scssDir = "src/scss", output = "src/public/css/default.css", minify = false) {
35131
- const absDir = resolve20(scssDir);
35132
- if (!existsSync25(absDir)) return "";
35133
- const files = readdirSync18(absDir).filter((f) => f.endsWith(".scss") && !f.startsWith("_")).sort().map((f) => join28(absDir, f));
35134
- if (files.length === 0) return "";
35135
- const paths = [absDir, ...this._importPaths];
35136
- const imported = /* @__PURE__ */ new Set();
35137
- let merged = "";
35138
- for (const file of files) {
35139
- const content = readFileSync23(file, "utf-8");
35140
- imported.add(file);
35141
- merged += resolveImports(content, paths, imported) + "\n";
35142
- }
35143
- let css = compileString(merged, paths, { ...this._variables });
35144
- if (minify) {
35145
- css = css.replace(/\/\*.*?\*\//gs, "");
35146
- css = css.replace(/\s+/g, " ");
35147
- css = css.replace(/\s*([{}:;,])\s*/g, "$1");
35148
- css = css.replace(/;}/g, "}");
35149
- css = css.trim();
35150
- }
35151
- const absOutput = resolve20(output);
35152
- const outDir = dirname13(absOutput);
35153
- if (!existsSync25(outDir)) mkdirSync19(outDir, { recursive: true });
35154
- let existing = null;
35155
- try {
35156
- existing = existsSync25(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
35157
- } catch {
35158
- existing = null;
35159
- }
35160
- if (existing !== css) {
35161
- writeFileSync17(absOutput, css, "utf-8");
35162
- }
35163
- return css;
35164
- }
35165
- };
35166
- VARIABLE_FLAG = /\s*!(default|global)\s*$/;
35167
- }
35168
- });
35169
-
35170
34773
  // ../core/src/mqttMessage.ts
35171
34774
  var MqttMessage;
35172
34775
  var init_mqttMessage = __esm({
@@ -35240,7 +34843,7 @@ var init_mqttMessage = __esm({
35240
34843
  import net2 from "node:net";
35241
34844
  import tls from "node:tls";
35242
34845
  import { randomBytes as randomBytes7 } from "node:crypto";
35243
- import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
34846
+ import { existsSync as existsSync25, readFileSync as readFileSync23 } from "node:fs";
35244
34847
  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;
35245
34848
  var init_mqtt = __esm({
35246
34849
  "../core/src/mqtt.ts"() {
@@ -35443,7 +35046,7 @@ var init_mqtt = __esm({
35443
35046
  */
35444
35047
  async connect() {
35445
35048
  this.closeSocket();
35446
- if (this.secure && this.tlsVerify && this.caFile && !existsSync26(this.caFile)) {
35049
+ if (this.secure && this.tlsVerify && this.caFile && !existsSync25(this.caFile)) {
35447
35050
  throw new MqttError(
35448
35051
  `MQTT CA file not found: ${this.caFile} -- TINA4_MQTT_CA_FILE (or caFile) must point at the broker's CA certificate in PEM form`
35449
35052
  );
@@ -35680,7 +35283,7 @@ var init_mqtt = __esm({
35680
35283
  * a later client.
35681
35284
  */
35682
35285
  openSocket() {
35683
- return new Promise((resolve31, reject) => {
35286
+ return new Promise((resolve30, reject) => {
35684
35287
  let settled = false;
35685
35288
  const settle = (fn) => {
35686
35289
  if (settled) return;
@@ -35705,10 +35308,10 @@ var init_mqtt = __esm({
35705
35308
  servername: this.host,
35706
35309
  rejectUnauthorized: this.tlsVerify
35707
35310
  };
35708
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
35709
- sock = tls.connect(opts, () => settle(() => resolve31(sock)));
35311
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync23(this.caFile);
35312
+ sock = tls.connect(opts, () => settle(() => resolve30(sock)));
35710
35313
  } else {
35711
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve31(sock)));
35314
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve30(sock)));
35712
35315
  }
35713
35316
  sock.once("error", (err) => {
35714
35317
  settle(() => {
@@ -35747,13 +35350,13 @@ var init_mqtt = __esm({
35747
35350
  writePacket(header, body) {
35748
35351
  if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
35749
35352
  const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
35750
- return new Promise((resolve31, reject) => {
35353
+ return new Promise((resolve30, reject) => {
35751
35354
  this.socket.write(packet, (err) => {
35752
35355
  if (err) {
35753
35356
  reject(new MqttError(`MQTT write failed: ${err.message}`));
35754
35357
  } else {
35755
35358
  this.lastWriteAt = Date.now();
35756
- resolve31();
35359
+ resolve30();
35757
35360
  }
35758
35361
  });
35759
35362
  });
@@ -35786,7 +35389,7 @@ var init_mqtt = __esm({
35786
35389
  if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
35787
35390
  if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
35788
35391
  if (this.socketError !== null) return Promise.reject(this.socketError);
35789
- return new Promise((resolve31, reject) => {
35392
+ return new Promise((resolve30, reject) => {
35790
35393
  let timer = null;
35791
35394
  if (deadline !== null) {
35792
35395
  const remaining = deadline - Date.now();
@@ -35801,7 +35404,7 @@ var init_mqtt = __esm({
35801
35404
  }
35802
35405
  }, remaining);
35803
35406
  }
35804
- this.waiter = { need, resolve: resolve31, reject, timer };
35407
+ this.waiter = { need, resolve: resolve30, reject, timer };
35805
35408
  this.serviceWaiter();
35806
35409
  });
35807
35410
  }
@@ -35925,8 +35528,8 @@ var init_mqtt = __esm({
35925
35528
  });
35926
35529
 
35927
35530
  // ../core/src/service.ts
35928
- import { readdirSync as readdirSync19, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
35929
- import { join as join29, extname as extname8 } from "node:path";
35531
+ import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
35532
+ import { join as join28, extname as extname8 } from "node:path";
35930
35533
  import { pathToFileURL } from "node:url";
35931
35534
  function matchCronField(field, value) {
35932
35535
  if (field === "*") return true;
@@ -36092,14 +35695,14 @@ var init_service = __esm({
36092
35695
  const discovered = [];
36093
35696
  let entries;
36094
35697
  try {
36095
- entries = readdirSync19(dir);
35698
+ entries = readdirSync18(dir);
36096
35699
  } catch {
36097
35700
  return discovered;
36098
35701
  }
36099
35702
  for (const entry of entries) {
36100
35703
  const ext = extname8(entry);
36101
35704
  if (ext !== ".ts" && ext !== ".js") continue;
36102
- const fullPath = join29(dir, entry);
35705
+ const fullPath = join28(dir, entry);
36103
35706
  const stat = statSync18(fullPath);
36104
35707
  if (!stat.isFile()) continue;
36105
35708
  try {
@@ -36208,14 +35811,14 @@ var init_service = __esm({
36208
35811
  const dir = serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
36209
35812
  let entries;
36210
35813
  try {
36211
- entries = readdirSync19(dir);
35814
+ entries = readdirSync18(dir);
36212
35815
  } catch {
36213
35816
  return;
36214
35817
  }
36215
35818
  for (const entry of entries) {
36216
35819
  const ext = extname8(entry);
36217
35820
  if (ext !== ".ts" && ext !== ".js") continue;
36218
- const fullPath = join29(dir, entry);
35821
+ const fullPath = join28(dir, entry);
36219
35822
  if (watchedFiles.has(fullPath)) continue;
36220
35823
  watchedFiles.add(fullPath);
36221
35824
  watchFile(fullPath, { interval: 1e3 }, async () => {
@@ -36259,7 +35862,7 @@ import https from "node:https";
36259
35862
  import { URL as URL2 } from "node:url";
36260
35863
  import { randomBytes as randomBytes8 } from "node:crypto";
36261
35864
  import { promises as fsp, createWriteStream } from "node:fs";
36262
- import { basename as basename6 } from "node:path";
35865
+ import { basename as basename5 } from "node:path";
36263
35866
  import { pipeline } from "node:stream/promises";
36264
35867
  function sameOrigin(urlA, urlB) {
36265
35868
  try {
@@ -36549,7 +36152,7 @@ var init_api = __esm({
36549
36152
  error: err instanceof Error ? err.message : String(err)
36550
36153
  };
36551
36154
  }
36552
- uploadName = filename || basename6(filePath);
36155
+ uploadName = filename || basename5(filePath);
36553
36156
  } else {
36554
36157
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
36555
36158
  }
@@ -36764,12 +36367,12 @@ var init_api = __esm({
36764
36367
  * authenticate to.
36765
36368
  */
36766
36369
  performRequest(method, url, headers, data, redirectsLeft) {
36767
- return new Promise((resolve31) => {
36370
+ return new Promise((resolve30) => {
36768
36371
  let parsed;
36769
36372
  try {
36770
36373
  parsed = new URL2(url);
36771
36374
  } catch (err) {
36772
- resolve31({ kind: "error", error: err instanceof Error ? err.message : String(err) });
36375
+ resolve30({ kind: "error", error: err instanceof Error ? err.message : String(err) });
36773
36376
  return;
36774
36377
  }
36775
36378
  const isHttps = parsed.protocol === "https:";
@@ -36794,7 +36397,7 @@ var init_api = __esm({
36794
36397
  try {
36795
36398
  nextUrl = new URL2(location, url).toString();
36796
36399
  } catch {
36797
- resolve31({ kind: "response", res });
36400
+ resolve30({ kind: "response", res });
36798
36401
  return;
36799
36402
  }
36800
36403
  const crossOrigin = !sameOrigin(url, nextUrl);
@@ -36812,17 +36415,17 @@ var init_api = __esm({
36812
36415
  deleteHeaderCaseInsensitive(nextHeaders, name);
36813
36416
  }
36814
36417
  }
36815
- this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve31);
36418
+ this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve30);
36816
36419
  return;
36817
36420
  }
36818
- resolve31({ kind: "response", res });
36421
+ resolve30({ kind: "response", res });
36819
36422
  });
36820
36423
  req2.on("timeout", () => {
36821
36424
  req2.destroy();
36822
- resolve31({ kind: "error", error: `Request timed out after ${this.timeout}s` });
36425
+ resolve30({ kind: "error", error: `Request timed out after ${this.timeout}s` });
36823
36426
  });
36824
36427
  req2.on("error", (err) => {
36825
- resolve31({ kind: "error", error: err.message });
36428
+ resolve30({ kind: "error", error: err.message });
36826
36429
  });
36827
36430
  if (data) {
36828
36431
  req2.write(data);
@@ -36832,7 +36435,7 @@ var init_api = __esm({
36832
36435
  }
36833
36436
  /** Buffer a response body, parse JSON if possible, and store cookies. */
36834
36437
  readResponse(res) {
36835
- return new Promise((resolve31) => {
36438
+ return new Promise((resolve30) => {
36836
36439
  const chunks = [];
36837
36440
  res.on("data", (chunk) => {
36838
36441
  chunks.push(chunk);
@@ -36847,7 +36450,7 @@ var init_api = __esm({
36847
36450
  } catch {
36848
36451
  parsed = raw;
36849
36452
  }
36850
- resolve31({
36453
+ resolve30({
36851
36454
  http_code: res.statusCode ?? null,
36852
36455
  body: parsed,
36853
36456
  headers: respHeaders,
@@ -36855,7 +36458,7 @@ var init_api = __esm({
36855
36458
  });
36856
36459
  });
36857
36460
  res.on("error", (err) => {
36858
- resolve31({ http_code: null, body: null, headers: {}, error: err.message });
36461
+ resolve30({ http_code: null, body: null, headers: {}, error: err.message });
36859
36462
  });
36860
36463
  });
36861
36464
  }
@@ -36908,14 +36511,14 @@ var init_api = __esm({
36908
36511
  // ../core/src/messenger.ts
36909
36512
  import net3 from "node:net";
36910
36513
  import tls2 from "node:tls";
36911
- import { readFileSync as readFileSync25 } from "node:fs";
36912
- import { basename as basename7 } from "node:path";
36514
+ import { readFileSync as readFileSync24 } from "node:fs";
36515
+ import { basename as basename6 } from "node:path";
36913
36516
  import { randomUUID as randomUUID7 } from "node:crypto";
36914
36517
  function tlsRejectUnauthorized() {
36915
36518
  return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
36916
36519
  }
36917
36520
  function readResponse(socket) {
36918
- return new Promise((resolve31, reject) => {
36521
+ return new Promise((resolve30, reject) => {
36919
36522
  let buffer = "";
36920
36523
  const onData = (chunk) => {
36921
36524
  buffer += chunk.toString("utf-8");
@@ -36927,7 +36530,7 @@ function readResponse(socket) {
36927
36530
  if (line.length >= 4 && line[3] === " ") {
36928
36531
  socket.removeListener("data", onData);
36929
36532
  socket.removeListener("error", onError);
36930
- resolve31({ code, text: buffer.trim() });
36533
+ resolve30({ code, text: buffer.trim() });
36931
36534
  return;
36932
36535
  }
36933
36536
  }
@@ -36941,10 +36544,10 @@ function readResponse(socket) {
36941
36544
  });
36942
36545
  }
36943
36546
  function sendCommand(socket, command) {
36944
- return new Promise((resolve31, reject) => {
36547
+ return new Promise((resolve30, reject) => {
36945
36548
  socket.write(command + "\r\n", "utf-8", (err) => {
36946
36549
  if (err) return reject(err);
36947
- readResponse(socket).then(resolve31, reject);
36550
+ readResponse(socket).then(resolve30, reject);
36948
36551
  });
36949
36552
  });
36950
36553
  }
@@ -37000,8 +36603,8 @@ function buildMimeMessage(options) {
37000
36603
  lines.push(options.body);
37001
36604
  }
37002
36605
  for (const filePath of options.attachments) {
37003
- const fileName = basename7(filePath);
37004
- const fileData = readFileSync25(filePath);
36606
+ const fileName = basename6(filePath);
36607
+ const fileData = readFileSync24(filePath);
37005
36608
  const base64Data = fileData.toString("base64");
37006
36609
  lines.push("");
37007
36610
  lines.push(`--${boundary}`);
@@ -37044,7 +36647,7 @@ function imapQuote(s) {
37044
36647
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
37045
36648
  }
37046
36649
  function imapReadLine(socket) {
37047
- return new Promise((resolve31, reject) => {
36650
+ return new Promise((resolve30, reject) => {
37048
36651
  let buffer = "";
37049
36652
  const onData = (chunk) => {
37050
36653
  buffer += chunk.toString("utf-8");
@@ -37052,7 +36655,7 @@ function imapReadLine(socket) {
37052
36655
  if (nlIndex !== -1) {
37053
36656
  socket.removeListener("data", onData);
37054
36657
  socket.removeListener("error", onError);
37055
- resolve31(buffer);
36658
+ resolve30(buffer);
37056
36659
  }
37057
36660
  };
37058
36661
  const onError = (err) => {
@@ -37064,7 +36667,7 @@ function imapReadLine(socket) {
37064
36667
  });
37065
36668
  }
37066
36669
  function imapCommand(socket, command) {
37067
- return new Promise((resolve31, reject) => {
36670
+ return new Promise((resolve30, reject) => {
37068
36671
  imapTagCounter++;
37069
36672
  const tag = `T${imapTagCounter}`;
37070
36673
  const fullCommand = `${tag} ${command}\r
@@ -37075,7 +36678,7 @@ function imapCommand(socket, command) {
37075
36678
  if (buffer.includes(`${tag} OK`)) {
37076
36679
  socket.removeListener("data", onData);
37077
36680
  socket.removeListener("error", onError);
37078
- resolve31(buffer);
36681
+ resolve30(buffer);
37079
36682
  return;
37080
36683
  }
37081
36684
  if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
@@ -37104,91 +36707,149 @@ function parseSearchResponse(response) {
37104
36707
  if (!match) return [];
37105
36708
  return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
37106
36709
  }
37107
- function parseHeaderResponse(uid, response) {
37108
- const headers = {};
37109
- const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
37110
- if (headerBlock) {
37111
- const lines = headerBlock[1].split(/\r\n/);
37112
- let currentKey = "";
37113
- for (const line of lines) {
37114
- if (/^\s/.test(line) && currentKey) {
37115
- headers[currentKey] += " " + line.trim();
37116
- } else {
37117
- const colonIdx = line.indexOf(":");
37118
- if (colonIdx > 0) {
37119
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
37120
- headers[currentKey] = line.substring(colonIdx + 1).trim();
37121
- }
37122
- }
37123
- }
37124
- }
37125
- const seen = /\\Seen/i.test(response);
37126
- return {
37127
- uid,
37128
- subject: headers["subject"] ?? "",
37129
- from: headers["from"] ?? "",
37130
- to: headers["to"] ?? "",
37131
- date: headers["date"] ?? "",
37132
- snippet: "",
37133
- seen
37134
- };
36710
+ function extractRawMessage(response) {
36711
+ const m = response.match(/\{(\d+)\}\r\n/);
36712
+ if (!m) return response;
36713
+ const start2 = (m.index ?? 0) + m[0].length;
36714
+ return response.slice(start2, start2 + parseInt(m[1], 10));
37135
36715
  }
37136
- function parseFullMessage(uid, response) {
37137
- const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
37138
- const rawMessage = bodyMatch ? bodyMatch[2] : response;
37139
- const headerEnd = rawMessage.indexOf("\r\n\r\n");
37140
- const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
37141
- const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
36716
+ function parseMimeHeaders(section) {
37142
36717
  const headers = {};
37143
- const headerLines = headerSection.split(/\r\n/);
37144
36718
  let currentKey = "";
37145
- for (const line of headerLines) {
36719
+ for (const line of section.split(/\r\n/)) {
37146
36720
  if (/^\s/.test(line) && currentKey) {
37147
36721
  headers[currentKey] += " " + line.trim();
37148
36722
  } else {
37149
- const colonIdx = line.indexOf(":");
37150
- if (colonIdx > 0) {
37151
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
37152
- headers[currentKey] = line.substring(colonIdx + 1).trim();
36723
+ const idx = line.indexOf(":");
36724
+ if (idx > 0) {
36725
+ currentKey = line.substring(0, idx).trim().toLowerCase();
36726
+ headers[currentKey] = line.substring(idx + 1).trim();
36727
+ }
36728
+ }
36729
+ }
36730
+ return headers;
36731
+ }
36732
+ function decodeTransfer(body, encoding) {
36733
+ const enc = encoding.toLowerCase().trim();
36734
+ if (enc === "base64") {
36735
+ try {
36736
+ return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf-8");
36737
+ } catch {
36738
+ return body;
36739
+ }
36740
+ }
36741
+ if (enc === "quoted-printable") {
36742
+ return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
36743
+ }
36744
+ return body;
36745
+ }
36746
+ function decodeAttachmentBytes(body, encoding) {
36747
+ const enc = encoding.toLowerCase().trim();
36748
+ if (enc === "base64") {
36749
+ return Buffer.from(body.replace(/\s+/g, ""), "base64");
36750
+ }
36751
+ const trimmed = body.replace(/\r\n$/, "");
36752
+ if (enc === "quoted-printable") {
36753
+ const collapsed = trimmed.replace(/=\r?\n/g, "");
36754
+ const bytes = [];
36755
+ for (let i = 0; i < collapsed.length; i++) {
36756
+ const hex = collapsed.substring(i + 1, i + 3);
36757
+ if (collapsed[i] === "=" && /^[0-9A-Fa-f]{2}$/.test(hex)) {
36758
+ bytes.push(parseInt(hex, 16));
36759
+ i += 2;
36760
+ } else {
36761
+ bytes.push(collapsed.charCodeAt(i) & 255);
37153
36762
  }
37154
36763
  }
36764
+ return Buffer.from(bytes);
37155
36765
  }
36766
+ return Buffer.from(trimmed, "utf-8");
36767
+ }
36768
+ function attachmentFilename(disposition, contentType) {
36769
+ const d = disposition.match(/filename="?([^";\r\n]+)"?/i);
36770
+ if (d) return d[1].trim();
36771
+ const c = contentType.match(/name="?([^";\r\n]+)"?/i);
36772
+ if (c) return c[1].trim();
36773
+ return "attachment";
36774
+ }
36775
+ function makeSnippet(bodyText, bodyHtml) {
36776
+ return (bodyText || bodyHtml || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200);
36777
+ }
36778
+ function toIsoDate(raw) {
36779
+ if (!raw) return "";
36780
+ const d = new Date(raw);
36781
+ return Number.isNaN(d.getTime()) ? raw : d.toISOString();
36782
+ }
36783
+ function parseMessage(response) {
36784
+ const raw = extractRawMessage(response);
36785
+ const headerEnd = raw.indexOf("\r\n\r\n");
36786
+ const headerSection = headerEnd >= 0 ? raw.substring(0, headerEnd) : raw;
36787
+ const bodySection = headerEnd >= 0 ? raw.substring(headerEnd + 4) : "";
36788
+ const headers = parseMimeHeaders(headerSection);
37156
36789
  const contentType = headers["content-type"] ?? "text/plain";
37157
36790
  let bodyText = "";
37158
36791
  let bodyHtml = "";
36792
+ const attachments = [];
37159
36793
  if (contentType.includes("multipart")) {
37160
36794
  const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
37161
36795
  if (boundaryMatch) {
37162
- const boundary = boundaryMatch[1];
37163
- const parts = bodySection.split("--" + boundary);
37164
- for (const part of parts) {
37165
- if (part.trim() === "" || part.trim() === "--") continue;
37166
- const partHeaderEnd = part.indexOf("\r\n\r\n");
37167
- const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
37168
- const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
37169
- if (partHeaders.includes("text/html")) {
37170
- bodyHtml = partBody;
37171
- } else if (partHeaders.includes("text/plain")) {
37172
- bodyText = partBody;
36796
+ const boundary = "--" + boundaryMatch[1];
36797
+ for (const part of bodySection.split(boundary)) {
36798
+ const trimmed = part.trim();
36799
+ if (trimmed === "" || trimmed === "--") continue;
36800
+ const pEnd = part.indexOf("\r\n\r\n");
36801
+ if (pEnd < 0) continue;
36802
+ const pHeaders = parseMimeHeaders(part.substring(0, pEnd));
36803
+ const pBody = part.substring(pEnd + 4);
36804
+ const cte = pHeaders["content-transfer-encoding"] ?? "";
36805
+ const pType = pHeaders["content-type"] ?? "text/plain";
36806
+ const disposition = pHeaders["content-disposition"] ?? "";
36807
+ if (/attachment/i.test(disposition)) {
36808
+ const content = decodeAttachmentBytes(pBody, cte);
36809
+ attachments.push({
36810
+ filename: attachmentFilename(disposition, pType),
36811
+ contentType: pType.split(";")[0].trim(),
36812
+ size: content.length,
36813
+ content
36814
+ });
36815
+ } else if (pType.includes("text/html")) {
36816
+ bodyHtml = decodeTransfer(pBody, cte).trim();
36817
+ } else if (pType.includes("text/plain")) {
36818
+ bodyText = decodeTransfer(pBody, cte).trim();
37173
36819
  }
37174
36820
  }
37175
36821
  }
37176
36822
  } else if (contentType.includes("text/html")) {
37177
- bodyHtml = bodySection;
36823
+ bodyHtml = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
37178
36824
  } else {
37179
- bodyText = bodySection;
36825
+ bodyText = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
37180
36826
  }
37181
- bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
37182
- bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
36827
+ return { headers, bodyText, bodyHtml, attachments };
36828
+ }
36829
+ function parseSummary(uid, response) {
36830
+ const { headers, bodyText, bodyHtml } = parseMessage(response);
36831
+ return {
36832
+ uid,
36833
+ subject: headers["subject"] ?? "",
36834
+ from: headers["from"] ?? "",
36835
+ to: headers["to"] ?? "",
36836
+ date: toIsoDate(headers["date"] ?? ""),
36837
+ snippet: makeSnippet(bodyText, bodyHtml),
36838
+ seen: /\\Seen/i.test(response)
36839
+ };
36840
+ }
36841
+ function parseFullMessage(uid, response) {
36842
+ const { headers, bodyText, bodyHtml, attachments } = parseMessage(response);
37183
36843
  return {
37184
36844
  uid,
37185
36845
  subject: headers["subject"] ?? "",
37186
36846
  from: headers["from"] ?? "",
37187
36847
  to: headers["to"] ?? "",
37188
36848
  cc: headers["cc"] ?? "",
37189
- date: headers["date"] ?? "",
36849
+ date: toIsoDate(headers["date"] ?? ""),
37190
36850
  bodyText,
37191
36851
  bodyHtml,
36852
+ attachments,
37192
36853
  headers
37193
36854
  };
37194
36855
  }
@@ -37304,89 +36965,89 @@ var init_messenger = __esm({
37304
36965
  }
37305
36966
  const messageId = `${randomUUID7()}@${this.host}`;
37306
36967
  if (allRecipients.length === 0) {
37307
- return { success: false, message: "No recipients specified" };
36968
+ return { success: false, message: "No recipients specified", id: null };
37308
36969
  }
37309
36970
  if (!this.fromAddress) {
37310
- return { success: false, message: "No from address configured" };
36971
+ return { success: false, message: "No from address configured", id: null };
37311
36972
  }
37312
36973
  try {
37313
36974
  let socket;
37314
36975
  if (this.port === 465) {
37315
36976
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
37316
- await new Promise((resolve31, reject) => {
37317
- socket.once("secureConnect", resolve31);
36977
+ await new Promise((resolve30, reject) => {
36978
+ socket.once("secureConnect", resolve30);
37318
36979
  socket.once("error", reject);
37319
36980
  });
37320
36981
  } else {
37321
36982
  socket = net3.createConnection({ host: this.host, port: this.port });
37322
- await new Promise((resolve31, reject) => {
37323
- socket.once("connect", resolve31);
36983
+ await new Promise((resolve30, reject) => {
36984
+ socket.once("connect", resolve30);
37324
36985
  socket.once("error", reject);
37325
36986
  });
37326
36987
  }
37327
36988
  const greeting = await readResponse(socket);
37328
36989
  if (greeting.code !== 220) {
37329
36990
  socket.destroy();
37330
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
36991
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}`, id: null };
37331
36992
  }
37332
36993
  const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
37333
36994
  if (ehlo.code !== 250) {
37334
36995
  socket.destroy();
37335
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
36996
+ return { success: false, message: `EHLO failed: ${ehlo.text}`, id: null };
37336
36997
  }
37337
36998
  if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
37338
36999
  const starttls = await sendCommand(socket, "STARTTLS");
37339
37000
  if (starttls.code !== 220) {
37340
37001
  socket.destroy();
37341
- return { success: false, message: `STARTTLS failed: ${starttls.text}` };
37002
+ return { success: false, message: `STARTTLS failed: ${starttls.text}`, id: null };
37342
37003
  }
37343
37004
  const plainSocket = socket;
37344
37005
  socket = tls2.connect(
37345
37006
  { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
37346
37007
  );
37347
- await new Promise((resolve31, reject) => {
37348
- socket.once("secureConnect", resolve31);
37008
+ await new Promise((resolve30, reject) => {
37009
+ socket.once("secureConnect", resolve30);
37349
37010
  socket.once("error", reject);
37350
37011
  });
37351
37012
  const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
37352
37013
  if (ehlo2.code !== 250) {
37353
37014
  socket.destroy();
37354
- return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
37015
+ return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}`, id: null };
37355
37016
  }
37356
37017
  }
37357
37018
  if (this.username && this.password) {
37358
37019
  const auth = await sendCommand(socket, "AUTH LOGIN");
37359
37020
  if (auth.code !== 334) {
37360
37021
  socket.destroy();
37361
- return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
37022
+ return { success: false, message: `AUTH LOGIN failed: ${auth.text}`, id: null };
37362
37023
  }
37363
37024
  const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
37364
37025
  if (userResp.code !== 334) {
37365
37026
  socket.destroy();
37366
- return { success: false, message: `AUTH username failed: ${userResp.text}` };
37027
+ return { success: false, message: `AUTH username failed: ${userResp.text}`, id: null };
37367
37028
  }
37368
37029
  const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
37369
37030
  if (passResp.code !== 235) {
37370
37031
  socket.destroy();
37371
- return { success: false, message: `AUTH password failed: ${passResp.text}` };
37032
+ return { success: false, message: `AUTH password failed: ${passResp.text}`, id: null };
37372
37033
  }
37373
37034
  }
37374
37035
  const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
37375
37036
  if (mailFrom.code !== 250) {
37376
37037
  socket.destroy();
37377
- return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
37038
+ return { success: false, message: `MAIL FROM failed: ${mailFrom.text}`, id: null };
37378
37039
  }
37379
37040
  for (const recipient of allRecipients) {
37380
37041
  const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
37381
37042
  if (rcpt.code !== 250 && rcpt.code !== 251) {
37382
37043
  socket.destroy();
37383
- return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
37044
+ return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}`, id: null };
37384
37045
  }
37385
37046
  }
37386
37047
  const dataCmd = await sendCommand(socket, "DATA");
37387
37048
  if (dataCmd.code !== 354) {
37388
37049
  socket.destroy();
37389
- return { success: false, message: `DATA failed: ${dataCmd.text}` };
37050
+ return { success: false, message: `DATA failed: ${dataCmd.text}`, id: null };
37390
37051
  }
37391
37052
  const mimeMessage = buildMimeMessage({
37392
37053
  from: this.fromAddress,
@@ -37405,15 +37066,30 @@ var init_messenger = __esm({
37405
37066
  const endData = await sendCommand(socket, mimeMessage + "\r\n.");
37406
37067
  if (endData.code !== 250) {
37407
37068
  socket.destroy();
37408
- return { success: false, message: `Message delivery failed: ${endData.text}` };
37069
+ return { success: false, message: `Message delivery failed: ${endData.text}`, id: null };
37409
37070
  }
37410
37071
  await sendCommand(socket, "QUIT");
37411
37072
  socket.destroy();
37412
37073
  return { success: true, message: "Email sent successfully", id: messageId };
37413
37074
  } catch (err) {
37414
37075
  const errMsg = err instanceof Error ? err.message : String(err);
37415
- return { success: false, message: `SMTP error: ${errMsg}` };
37076
+ return { success: false, message: `SMTP error: ${errMsg}`, id: null };
37077
+ }
37078
+ }
37079
+ /**
37080
+ * Render a Frond template STRING and send it as an HTML email (G7, parity with
37081
+ * Python's send_template). Extra send() options (cc, bcc, replyTo, attachments,
37082
+ * headers) pass through. If the Frond package cannot be loaded the raw template
37083
+ * is sent verbatim (matches Python's ImportError fallback) rather than failing.
37084
+ */
37085
+ async sendTemplate(to, subject, template, data = {}, cc, bcc, replyTo, attachments, headers) {
37086
+ let body = template;
37087
+ try {
37088
+ const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
37089
+ body = new Frond2().renderString(template, data);
37090
+ } catch {
37416
37091
  }
37092
+ return this.send(to, subject, body, true, void 0, cc, bcc, replyTo, attachments, headers);
37417
37093
  }
37418
37094
  /**
37419
37095
  * Test the SMTP connection without sending an email.
@@ -37423,14 +37099,14 @@ var init_messenger = __esm({
37423
37099
  let socket;
37424
37100
  if (this.port === 465) {
37425
37101
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
37426
- await new Promise((resolve31, reject) => {
37427
- socket.once("secureConnect", resolve31);
37102
+ await new Promise((resolve30, reject) => {
37103
+ socket.once("secureConnect", resolve30);
37428
37104
  socket.once("error", reject);
37429
37105
  });
37430
37106
  } else {
37431
37107
  socket = net3.createConnection({ host: this.host, port: this.port });
37432
- await new Promise((resolve31, reject) => {
37433
- socket.once("connect", resolve31);
37108
+ await new Promise((resolve30, reject) => {
37109
+ socket.once("connect", resolve30);
37434
37110
  socket.once("error", reject);
37435
37111
  });
37436
37112
  }
@@ -37465,14 +37141,14 @@ var init_messenger = __esm({
37465
37141
  const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
37466
37142
  if (useTls) {
37467
37143
  socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
37468
- await new Promise((resolve31, reject) => {
37469
- socket.once("secureConnect", resolve31);
37144
+ await new Promise((resolve30, reject) => {
37145
+ socket.once("secureConnect", resolve30);
37470
37146
  socket.once("error", reject);
37471
37147
  });
37472
37148
  } else {
37473
37149
  socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
37474
- await new Promise((resolve31, reject) => {
37475
- socket.once("connect", resolve31);
37150
+ await new Promise((resolve30, reject) => {
37151
+ socket.once("connect", resolve30);
37476
37152
  socket.once("error", reject);
37477
37153
  });
37478
37154
  }
@@ -37509,7 +37185,7 @@ var init_messenger = __esm({
37509
37185
  }
37510
37186
  try {
37511
37187
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37512
- const searchResp = await imapCommand(socket, "SEARCH ALL");
37188
+ const searchResp = await imapCommand(socket, "UID SEARCH ALL");
37513
37189
  const uids = parseSearchResponse(searchResp);
37514
37190
  if (uids.length === 0) return [];
37515
37191
  uids.reverse();
@@ -37517,8 +37193,8 @@ var init_messenger = __esm({
37517
37193
  if (selected.length === 0) return [];
37518
37194
  const messages = [];
37519
37195
  for (const uid of selected) {
37520
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
37521
- messages.push(parseHeaderResponse(uid, fetchResp));
37196
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
37197
+ messages.push(parseSummary(uid, fetchResp));
37522
37198
  }
37523
37199
  return messages;
37524
37200
  } catch (err) {
@@ -37528,7 +37204,7 @@ var init_messenger = __esm({
37528
37204
  }
37529
37205
  }
37530
37206
  /**
37531
- * Read a single message by sequence number or UID.
37207
+ * Read a single message by its IMAP UID.
37532
37208
  */
37533
37209
  async read(uid, folder = "INBOX") {
37534
37210
  let socket;
@@ -37539,11 +37215,11 @@ var init_messenger = __esm({
37539
37215
  }
37540
37216
  try {
37541
37217
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37542
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
37218
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY[])`);
37543
37219
  if (!/\{\d+\}/.test(fetchResp)) {
37544
37220
  return null;
37545
37221
  }
37546
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
37222
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
37547
37223
  return parseFullMessage(uid, fetchResp);
37548
37224
  } catch (err) {
37549
37225
  throw imapFail("read", err);
@@ -37570,14 +37246,14 @@ var init_messenger = __esm({
37570
37246
  }
37571
37247
  try {
37572
37248
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37573
- const searchResp = await imapCommand(socket, `SEARCH ${query}`);
37249
+ const searchResp = await imapCommand(socket, `UID SEARCH ${query}`);
37574
37250
  const uids = parseSearchResponse(searchResp);
37575
37251
  if (uids.length === 0) return [];
37576
37252
  uids.reverse();
37577
37253
  const messages = [];
37578
37254
  for (const uid of uids.slice(0, limit)) {
37579
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
37580
- messages.push(parseHeaderResponse(uid, fetchResp));
37255
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
37256
+ messages.push(parseSummary(uid, fetchResp));
37581
37257
  }
37582
37258
  return messages;
37583
37259
  } catch (err) {
@@ -37587,26 +37263,46 @@ var init_messenger = __esm({
37587
37263
  }
37588
37264
  }
37589
37265
  /**
37590
- * Delete a message by UID.
37266
+ * Delete a message by UID (mark \Deleted, then EXPUNGE).
37267
+ *
37268
+ * `delete` is the one cross-framework name (python/php/ruby/node all spell it
37269
+ * `delete`). `deleteMessage` remains as a DEPRECATED alias for one release.
37591
37270
  */
37592
- async deleteMessage(uid, folder = "INBOX") {
37271
+ async delete(uid, folder = "INBOX") {
37593
37272
  const socket = await this.imapConnect();
37594
37273
  try {
37595
37274
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37596
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
37275
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Deleted)`);
37597
37276
  await imapCommand(socket, "EXPUNGE");
37598
37277
  } finally {
37599
37278
  await this.imapDisconnect(socket);
37600
37279
  }
37601
37280
  }
37281
+ /** @deprecated Use {@link delete} — kept as an alias for one release (G7). */
37282
+ async deleteMessage(uid, folder = "INBOX") {
37283
+ return this.delete(uid, folder);
37284
+ }
37602
37285
  /**
37603
- * Mark a message as read.
37286
+ * Mark a message as read (+FLAGS \Seen).
37604
37287
  */
37605
37288
  async markRead(uid, folder = "INBOX") {
37606
37289
  const socket = await this.imapConnect();
37607
37290
  try {
37608
37291
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37609
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
37292
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
37293
+ } finally {
37294
+ await this.imapDisconnect(socket);
37295
+ }
37296
+ }
37297
+ /**
37298
+ * Mark a message as unread (-FLAGS \Seen) — the inverse of markRead (G7,
37299
+ * parity with Python's mark_unread).
37300
+ */
37301
+ async markUnread(uid, folder = "INBOX") {
37302
+ const socket = await this.imapConnect();
37303
+ try {
37304
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37305
+ await imapCommand(socket, `UID STORE ${uid} -FLAGS (\\Seen)`);
37610
37306
  } finally {
37611
37307
  await this.imapDisconnect(socket);
37612
37308
  }
@@ -37623,7 +37319,7 @@ var init_messenger = __esm({
37623
37319
  }
37624
37320
  try {
37625
37321
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37626
- const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
37322
+ const searchResp = await imapCommand(socket, "UID SEARCH UNSEEN");
37627
37323
  return parseSearchResponse(searchResp).length;
37628
37324
  } catch (err) {
37629
37325
  throw imapFail("unread", err);
@@ -38379,17 +38075,17 @@ __export(ai_exports, {
38379
38075
  skillBlock: () => skillBlock,
38380
38076
  writeOrMerge: () => writeOrMerge
38381
38077
  });
38382
- import { existsSync as existsSync27, mkdirSync as mkdirSync20, writeFileSync as writeFileSync18, readFileSync as readFileSync26 } from "node:fs";
38078
+ import { existsSync as existsSync26, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17, readFileSync as readFileSync25 } from "node:fs";
38383
38079
  import { homedir } from "node:os";
38384
- import { join as join30, resolve as resolve21, relative as relative10, dirname as dirname14 } from "node:path";
38080
+ import { join as join29, resolve as resolve20, relative as relative10, dirname as dirname13 } from "node:path";
38385
38081
  import { fileURLToPath as fileURLToPath7 } from "node:url";
38386
38082
  import { execSync as execSync2, execFileSync as execFileSync3 } from "node:child_process";
38387
38083
  import { createInterface } from "node:readline";
38388
38084
  function readVersion() {
38389
38085
  try {
38390
- const thisDir = dirname14(fileURLToPath7(import.meta.url));
38391
- const rootPkg = resolve21(thisDir, "..", "..", "..", "package.json");
38392
- const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
38086
+ const thisDir = dirname13(fileURLToPath7(import.meta.url));
38087
+ const rootPkg = resolve20(thisDir, "..", "..", "..", "package.json");
38088
+ const pkg = JSON.parse(readFileSync25(rootPkg, "utf-8"));
38393
38089
  return pkg.version ?? "0.0.0";
38394
38090
  } catch {
38395
38091
  return "0.0.0";
@@ -38438,8 +38134,8 @@ function downloadSkillsSync(jobs) {
38438
38134
  function installSkills(root = ".", targets) {
38439
38135
  const ref = skillsRef();
38440
38136
  const dests = targets ?? [
38441
- join30(resolve21(root), ".claude", "skills"),
38442
- join30(homedir(), ".claude", "skills")
38137
+ join29(resolve20(root), ".claude", "skills"),
38138
+ join29(homedir(), ".claude", "skills")
38443
38139
  ];
38444
38140
  const jobs = [];
38445
38141
  const index = /* @__PURE__ */ new Map();
@@ -38457,9 +38153,9 @@ function installSkills(root = ".", targets) {
38457
38153
  const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
38458
38154
  skillMdUrl[skill] = `${base}/SKILL.md`;
38459
38155
  for (const dest of dests) {
38460
- add(`${base}/SKILL.md`, join30(dest, skill, "SKILL.md"));
38156
+ add(`${base}/SKILL.md`, join29(dest, skill, "SKILL.md"));
38461
38157
  for (const r of spec.references) {
38462
- add(`${base}/references/${r}`, join30(dest, skill, "references", r));
38158
+ add(`${base}/references/${r}`, join29(dest, skill, "references", r));
38463
38159
  }
38464
38160
  }
38465
38161
  }
@@ -38471,10 +38167,10 @@ function installSkills(root = ".", targets) {
38471
38167
  return installed;
38472
38168
  }
38473
38169
  function isInstalled(root, tool) {
38474
- return existsSync27(join30(resolve21(root), tool.contextFile));
38170
+ return existsSync26(join29(resolve20(root), tool.contextFile));
38475
38171
  }
38476
38172
  function showMenu(root = ".") {
38477
- const r = resolve21(root);
38173
+ const r = resolve20(root);
38478
38174
  console.log("\n Tina4 AI Context Installer\n");
38479
38175
  for (let i = 0; i < AI_TOOLS.length; i++) {
38480
38176
  const tool = AI_TOOLS[i];
@@ -38492,16 +38188,16 @@ function showMenu(root = ".") {
38492
38188
  const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
38493
38189
  console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
38494
38190
  console.log();
38495
- return new Promise((resolve31) => {
38191
+ return new Promise((resolve30) => {
38496
38192
  const rl = createInterface({ input: process.stdin, output: process.stdout });
38497
38193
  rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
38498
38194
  rl.close();
38499
- resolve31(answer.trim());
38195
+ resolve30(answer.trim());
38500
38196
  });
38501
38197
  });
38502
38198
  }
38503
38199
  function installSelected(root, selection) {
38504
- const rootPath = resolve21(root);
38200
+ const rootPath = resolve20(root);
38505
38201
  const created = [];
38506
38202
  let indices;
38507
38203
  let doInstallTina4Ai = false;
@@ -38594,33 +38290,33 @@ function looksLikeOldFrameworkInstall(existing) {
38594
38290
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
38595
38291
  const block = skillBlock(contextFile);
38596
38292
  const [start2, end] = markersFor(contextFile);
38597
- if (!existsSync27(contextPath)) {
38598
- writeFileSync18(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38293
+ if (!existsSync26(contextPath)) {
38294
+ writeFileSync17(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38599
38295
  return "Installed";
38600
38296
  }
38601
- const existing = readFileSync26(contextPath, "utf-8");
38297
+ const existing = readFileSync25(contextPath, "utf-8");
38602
38298
  if (hasMarkers(existing, start2, end)) {
38603
- writeFileSync18(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
38299
+ writeFileSync17(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
38604
38300
  return "Refreshed skill block in";
38605
38301
  }
38606
38302
  if (looksLikeOldFrameworkInstall(existing)) {
38607
38303
  const head = existing.replace(/^\s+/, "");
38608
38304
  const preamble = existing.slice(0, existing.length - head.length);
38609
38305
  const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
38610
- writeFileSync18(contextPath, newContent, "utf-8");
38306
+ writeFileSync17(contextPath, newContent, "utf-8");
38611
38307
  return "Migrated (replaced old framework dump in)";
38612
38308
  }
38613
- writeFileSync18(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38309
+ writeFileSync17(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38614
38310
  return "Appended skill block to";
38615
38311
  }
38616
38312
  function installForTool(root, tool, context) {
38617
38313
  const created = [];
38618
- const contextPath = join30(root, tool.contextFile);
38314
+ const contextPath = join29(root, tool.contextFile);
38619
38315
  if (tool.configDir) {
38620
- mkdirSync20(join30(root, tool.configDir), { recursive: true });
38316
+ mkdirSync19(join29(root, tool.configDir), { recursive: true });
38621
38317
  }
38622
- const parentDir = dirname14(contextPath);
38623
- mkdirSync20(parentDir, { recursive: true });
38318
+ const parentDir = dirname13(contextPath);
38319
+ mkdirSync19(parentDir, { recursive: true });
38624
38320
  const action = writeOrMerge(contextPath, tool.contextFile, context);
38625
38321
  const rel = relative10(root, contextPath);
38626
38322
  created.push(rel);
@@ -38655,7 +38351,7 @@ function installTina4Ai() {
38655
38351
  function installClaudeSkills(root) {
38656
38352
  const created = [];
38657
38353
  for (const skill of installSkills(root)) {
38658
- created.push(join30(".claude", "skills", skill));
38354
+ created.push(join29(".claude", "skills", skill));
38659
38355
  console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
38660
38356
  }
38661
38357
  return created;
@@ -38996,11 +38692,11 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
38996
38692
  }
38997
38693
  function generateClaudeCodeContext() {
38998
38694
  try {
38999
- const thisDir = dirname14(fileURLToPath7(import.meta.url));
39000
- const repoRoot = resolve21(thisDir, "..", "..", "..");
39001
- const claudeMdPath = join30(repoRoot, "CLAUDE.md");
39002
- if (existsSync27(claudeMdPath)) {
39003
- return readFileSync26(claudeMdPath, "utf-8");
38695
+ const thisDir = dirname13(fileURLToPath7(import.meta.url));
38696
+ const repoRoot = resolve20(thisDir, "..", "..", "..");
38697
+ const claudeMdPath = join29(repoRoot, "CLAUDE.md");
38698
+ if (existsSync26(claudeMdPath)) {
38699
+ return readFileSync25(claudeMdPath, "utf-8");
39004
38700
  }
39005
38701
  } catch {
39006
38702
  }
@@ -39516,16 +39212,8 @@ var init_rabbitmqBackend = __esm({
39516
39212
  process.stdout.write(String(msgCount));
39517
39213
  closeConnection();
39518
39214
  }
39519
- else if (operation === "purge") {
39520
- // Queue.Purge
39521
- const qBuf = Buffer.from(queueName, "utf-8");
39522
- const purgePayload = Buffer.alloc(4 + qBuf.length);
39523
- purgePayload.writeUInt16BE(0, 0);
39524
- purgePayload.writeUInt8(qBuf.length, 2);
39525
- qBuf.copy(purgePayload, 3);
39526
- purgePayload.writeUInt8(0, 3 + qBuf.length); // no-wait=false
39527
- sendMethod(1, 50, 30, purgePayload);
39528
- }
39215
+ // No "purge" operation: clear()/purge() refuse by name (ADR-0022),
39216
+ // so nothing ever sends Queue.Purge and the drain path is gone.
39529
39217
  }
39530
39218
  else if (classId === 60 && methodId === 71) {
39531
39219
  // Basic.Get-Ok \u2014 message body will follow in content frames
@@ -39536,11 +39224,6 @@ var init_rabbitmqBackend = __esm({
39536
39224
  process.stdout.write("__EMPTY__");
39537
39225
  closeConnection();
39538
39226
  }
39539
- else if (classId === 50 && methodId === 31) {
39540
- // Queue.Purge-Ok
39541
- process.stdout.write("__PURGED__");
39542
- closeConnection();
39543
- }
39544
39227
  else if (classId === 10 && methodId === 50) {
39545
39228
  // Connection.Close (server-initiated, e.g. a channel/protocol error)
39546
39229
  // \u2192 send Connection.Close-Ok and exit non-zero so the caller sees the
@@ -39670,8 +39353,15 @@ var init_rabbitmqBackend = __esm({
39670
39353
  const num = parseInt(result, 10);
39671
39354
  return isNaN(num) ? 0 : num;
39672
39355
  }
39673
- clear(queue) {
39674
- this.execSync("purge", queue);
39356
+ clear(_queue) {
39357
+ throw new Error(
39358
+ "The rabbitmq queue backend cannot perform clear(): RabbitMQ cannot address messages by status (basic.get pops the head of the queue), so a status-addressed clear would have to drain the entire live queue and destroy pending work. Use the file or mongodb backend."
39359
+ );
39360
+ }
39361
+ purge(_queue, _status) {
39362
+ throw new Error(
39363
+ "The rabbitmq queue backend cannot perform purge(): RabbitMQ cannot address messages by status (basic.get pops the head of the queue), so a status-addressed purge would have to drain the entire live queue and destroy pending work. Use the file or mongodb backend."
39364
+ );
39675
39365
  }
39676
39366
  };
39677
39367
  }
@@ -40233,6 +39923,14 @@ var init_kafkaBackend = __esm({
40233
39923
  return 0;
40234
39924
  }
40235
39925
  clear(_queue) {
39926
+ throw new Error(
39927
+ "The kafka queue backend cannot perform clear(): Kafka has no notion of job status and cannot delete records on demand. A log is read in offset order and records leave only by retention. Use the file or mongodb backend."
39928
+ );
39929
+ }
39930
+ purge(_queue, _status) {
39931
+ throw new Error(
39932
+ "The kafka queue backend cannot perform purge(): Kafka has no notion of job status to purge by. A log is read in offset order and records leave only by retention. Use the file or mongodb backend."
39933
+ );
40236
39934
  }
40237
39935
  };
40238
39936
  }
@@ -40992,7 +40690,6 @@ __export(src_exports3, {
40992
40690
  RouteRef: () => RouteRef,
40993
40691
  Router: () => Router,
40994
40692
  SafeString: () => SafeString2,
40995
- ScssCompiler: () => ScssCompiler,
40996
40693
  SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
40997
40694
  ServiceRunner: () => ServiceRunner,
40998
40695
  Session: () => Session,
@@ -41189,7 +40886,6 @@ var init_src3 = __esm({
41189
40886
  init_session();
41190
40887
  init_i18n();
41191
40888
  init_fakeData();
41192
- init_scss();
41193
40889
  init_queue();
41194
40890
  init_job();
41195
40891
  init_mqtt();
@@ -41464,20 +41160,20 @@ ${cdStep} npm install
41464
41160
  }
41465
41161
 
41466
41162
  // src/commands/serve.ts
41467
- import { resolve as resolve22 } from "node:path";
41468
- import { existsSync as existsSync28 } from "node:fs";
41163
+ import { resolve as resolve21 } from "node:path";
41164
+ import { existsSync as existsSync27 } from "node:fs";
41469
41165
  async function serveProject(options) {
41470
41166
  if (options.noReload) {
41471
41167
  process.env.TINA4_NO_RELOAD = "true";
41472
41168
  }
41473
41169
  const port = options.port ?? 7148;
41474
41170
  const cwd = process.cwd();
41475
- const routesDir = resolve22(cwd, "src/routes");
41476
- const ormDir = resolve22(cwd, "src/orm");
41477
- const modelsDir = resolve22(cwd, "src/models");
41478
- const templatesDir = resolve22(cwd, "src/templates");
41479
- const staticDir = resolve22(cwd, "public");
41480
- if (!existsSync28(routesDir) && !existsSync28(modelsDir) && !existsSync28(ormDir)) {
41171
+ const routesDir = resolve21(cwd, "src/routes");
41172
+ const ormDir = resolve21(cwd, "src/orm");
41173
+ const modelsDir = resolve21(cwd, "src/models");
41174
+ const templatesDir = resolve21(cwd, "src/templates");
41175
+ const staticDir = resolve21(cwd, "public");
41176
+ if (!existsSync27(routesDir) && !existsSync27(modelsDir) && !existsSync27(ormDir)) {
41481
41177
  console.error(" Error: Not a Tina4 project. Run this from a project created with 'tina4 init'.");
41482
41178
  process.exit(1);
41483
41179
  }
@@ -41493,12 +41189,12 @@ async function serveProject(options) {
41493
41189
 
41494
41190
  // src/commands/migrate.ts
41495
41191
  init_dotenv();
41496
- import { existsSync as existsSync29, readdirSync as readdirSync20, readFileSync as readFileSync27 } from "node:fs";
41497
- import { join as join31, resolve as resolve23 } from "node:path";
41192
+ import { existsSync as existsSync28, readdirSync as readdirSync19, readFileSync as readFileSync26 } from "node:fs";
41193
+ import { join as join30, resolve as resolve22 } from "node:path";
41498
41194
  async function runMigrations(migrationDir) {
41499
41195
  loadEnv();
41500
- const dir = resolve23(migrationDir ?? "migrations");
41501
- if (!existsSync29(dir)) {
41196
+ const dir = resolve22(migrationDir ?? "migrations");
41197
+ if (!existsSync28(dir)) {
41502
41198
  console.log(" No migrations/ directory found. Nothing to run.");
41503
41199
  return;
41504
41200
  }
@@ -41529,7 +41225,7 @@ async function runMigrations(migrationDir) {
41529
41225
  process.exit(1);
41530
41226
  }
41531
41227
  await ensureMigrationTable2();
41532
- const files = readdirSync20(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql")).sort((a, b) => {
41228
+ const files = readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql")).sort((a, b) => {
41533
41229
  const aMatch = a.match(/^(\d+)/);
41534
41230
  const bMatch = b.match(/^(\d+)/);
41535
41231
  if (aMatch && bMatch) {
@@ -41551,7 +41247,7 @@ async function runMigrations(migrationDir) {
41551
41247
  if (await isMigrationApplied2(name)) {
41552
41248
  continue;
41553
41249
  }
41554
- const sql = readFileSync27(join31(dir, file), "utf-8").trim();
41250
+ const sql = readFileSync26(join30(dir, file), "utf-8").trim();
41555
41251
  if (!sql) continue;
41556
41252
  console.log(` Migrating: ${file}`);
41557
41253
  const adapter = getAdapter2();
@@ -41576,17 +41272,17 @@ async function runMigrations(migrationDir) {
41576
41272
  }
41577
41273
 
41578
41274
  // src/commands/migrateCreate.ts
41579
- import { existsSync as existsSync30, mkdirSync as mkdirSync21, writeFileSync as writeFileSync19 } from "node:fs";
41580
- import { join as join32, resolve as resolve24 } from "node:path";
41275
+ import { existsSync as existsSync29, mkdirSync as mkdirSync20, writeFileSync as writeFileSync18 } from "node:fs";
41276
+ import { join as join31, resolve as resolve23 } from "node:path";
41581
41277
  async function createMigration2(description) {
41582
41278
  if (!description) {
41583
41279
  console.error(" Usage: tina4 migrate:create <description>");
41584
41280
  console.error(' Example: tina4 migrate:create "create users table"');
41585
41281
  process.exit(1);
41586
41282
  }
41587
- const dir = resolve24("migrations");
41588
- if (!existsSync30(dir)) {
41589
- mkdirSync21(dir, { recursive: true });
41283
+ const dir = resolve23("migrations");
41284
+ if (!existsSync29(dir)) {
41285
+ mkdirSync20(dir, { recursive: true });
41590
41286
  }
41591
41287
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
41592
41288
  const now = /* @__PURE__ */ new Date();
@@ -41600,8 +41296,8 @@ async function createMigration2(description) {
41600
41296
  ].join("");
41601
41297
  const upFileName = `${timestamp2}_${safeName}.sql`;
41602
41298
  const downFileName = `${timestamp2}_${safeName}.down.sql`;
41603
- const upPath = join32(dir, upFileName);
41604
- const downPath = join32(dir, downFileName);
41299
+ const upPath = join31(dir, upFileName);
41300
+ const downPath = join31(dir, downFileName);
41605
41301
  const upTemplate = `-- Migration: ${description}
41606
41302
  -- Created: ${now.toISOString()}
41607
41303
 
@@ -41610,8 +41306,8 @@ async function createMigration2(description) {
41610
41306
  -- Created: ${now.toISOString()}
41611
41307
 
41612
41308
  `;
41613
- writeFileSync19(upPath, upTemplate, "utf-8");
41614
- writeFileSync19(downPath, downTemplate, "utf-8");
41309
+ writeFileSync18(upPath, upTemplate, "utf-8");
41310
+ writeFileSync18(downPath, downTemplate, "utf-8");
41615
41311
  console.log(` Created migration: ${upFileName}`);
41616
41312
  console.log(` Created rollback: ${downFileName}`);
41617
41313
  console.log(` Path: ${dir}`);
@@ -41619,10 +41315,10 @@ async function createMigration2(description) {
41619
41315
 
41620
41316
  // src/commands/migrateStatus.ts
41621
41317
  init_dotenv();
41622
- import { resolve as resolve25 } from "node:path";
41318
+ import { resolve as resolve24 } from "node:path";
41623
41319
  async function migrateStatus(migrationDir) {
41624
41320
  loadEnv();
41625
- const dir = resolve25(migrationDir ?? "migrations");
41321
+ const dir = resolve24(migrationDir ?? "migrations");
41626
41322
  let initDatabase2;
41627
41323
  let ensureMigrationTable2;
41628
41324
  let statusFn;
@@ -41667,10 +41363,10 @@ async function migrateStatus(migrationDir) {
41667
41363
 
41668
41364
  // src/commands/migrateRollback.ts
41669
41365
  init_dotenv();
41670
- import { resolve as resolve26 } from "node:path";
41366
+ import { resolve as resolve25 } from "node:path";
41671
41367
  async function migrateRollback(migrationDir) {
41672
41368
  loadEnv();
41673
- const dir = resolve26(migrationDir ?? "migrations");
41369
+ const dir = resolve25(migrationDir ?? "migrations");
41674
41370
  let initDatabase2;
41675
41371
  let ensureMigrationTable2;
41676
41372
  let rollbackFn;
@@ -41710,11 +41406,11 @@ async function migrateRollback(migrationDir) {
41710
41406
  }
41711
41407
 
41712
41408
  // src/commands/routes.ts
41713
- import { existsSync as existsSync31 } from "node:fs";
41714
- import { resolve as resolve27 } from "node:path";
41409
+ import { existsSync as existsSync30 } from "node:fs";
41410
+ import { resolve as resolve26 } from "node:path";
41715
41411
  async function listRoutes() {
41716
- const routesDir = resolve27("src/routes");
41717
- if (!existsSync31(routesDir)) {
41412
+ const routesDir = resolve26("src/routes");
41413
+ if (!existsSync30(routesDir)) {
41718
41414
  console.error(" No src/routes/ directory found. Are you in a Tina4 project?");
41719
41415
  process.exit(1);
41720
41416
  }
@@ -41754,14 +41450,14 @@ async function listRoutes() {
41754
41450
  }
41755
41451
 
41756
41452
  // src/commands/test.ts
41757
- import { existsSync as existsSync32, readdirSync as readdirSync22 } from "node:fs";
41758
- import { resolve as resolve28, join as join33 } from "node:path";
41453
+ import { existsSync as existsSync31, readdirSync as readdirSync21 } from "node:fs";
41454
+ import { resolve as resolve27, join as join32 } from "node:path";
41759
41455
  import { execSync as execSync3 } from "node:child_process";
41760
41456
  async function runTests(testPath) {
41761
41457
  const cwd = process.cwd();
41762
41458
  if (testPath) {
41763
- const file = resolve28(testPath);
41764
- if (!existsSync32(file)) {
41459
+ const file = resolve27(testPath);
41460
+ if (!existsSync31(file)) {
41765
41461
  console.error(` Error: Test file not found: ${testPath}`);
41766
41462
  process.exit(1);
41767
41463
  }
@@ -41781,14 +41477,14 @@ async function runTests(testPath) {
41781
41477
  ];
41782
41478
  let testFiles = [];
41783
41479
  for (const candidate of candidates) {
41784
- const fullPath = resolve28(cwd, candidate);
41785
- if (!existsSync32(fullPath)) continue;
41480
+ const fullPath = resolve27(cwd, candidate);
41481
+ if (!existsSync31(fullPath)) continue;
41786
41482
  if (candidate.endsWith(".ts")) {
41787
41483
  testFiles.push(fullPath);
41788
41484
  break;
41789
41485
  }
41790
41486
  try {
41791
- const files = readdirSync22(fullPath).filter((f) => f.endsWith(".ts")).map((f) => join33(fullPath, f));
41487
+ const files = readdirSync21(fullPath).filter((f) => f.endsWith(".ts")).map((f) => join32(fullPath, f));
41792
41488
  testFiles.push(...files);
41793
41489
  } catch {
41794
41490
  }
@@ -41817,8 +41513,8 @@ async function runTests(testPath) {
41817
41513
  }
41818
41514
 
41819
41515
  // src/commands/generate.ts
41820
- import { existsSync as existsSync33, mkdirSync as mkdirSync22, writeFileSync as writeFileSync20 } from "node:fs";
41821
- import { join as join34, resolve as resolve29 } from "node:path";
41516
+ import { existsSync as existsSync32, mkdirSync as mkdirSync21, writeFileSync as writeFileSync19 } from "node:fs";
41517
+ import { join as join33, resolve as resolve28 } from "node:path";
41822
41518
  var FIELD_TYPE_MAP = {
41823
41519
  string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
41824
41520
  str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
@@ -41835,16 +41531,16 @@ var FIELD_TYPE_MAP = {
41835
41531
  blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
41836
41532
  };
41837
41533
  function ensureDir(dir) {
41838
- if (!existsSync33(dir)) {
41839
- mkdirSync22(dir, { recursive: true });
41534
+ if (!existsSync32(dir)) {
41535
+ mkdirSync21(dir, { recursive: true });
41840
41536
  }
41841
41537
  }
41842
41538
  function writeFileSafe(path8, content) {
41843
- if (existsSync33(path8)) {
41539
+ if (existsSync32(path8)) {
41844
41540
  console.log(` File already exists: ${path8}`);
41845
41541
  return;
41846
41542
  }
41847
- writeFileSync20(path8, content, "utf-8");
41543
+ writeFileSync19(path8, content, "utf-8");
41848
41544
  console.log(` Created ${path8}`);
41849
41545
  }
41850
41546
  function toSnake(name) {
@@ -42001,9 +41697,9 @@ async function generate2(what, name, extraArgs = []) {
42001
41697
  function generateModel(name, flags, emitTest = true) {
42002
41698
  const fields = fieldsOrDefault(flags.fields || "");
42003
41699
  const table2 = toTableName(name);
42004
- const dir = resolve29("src/models");
41700
+ const dir = resolve28("src/models");
42005
41701
  ensureDir(dir);
42006
- const path8 = join34(dir, `${name}.ts`);
41702
+ const path8 = join33(dir, `${name}.ts`);
42007
41703
  const fieldLines = [
42008
41704
  ` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`
42009
41705
  ];
@@ -42037,8 +41733,8 @@ function generateRoute(name, flags, emitTest = true) {
42037
41733
  const singular = routePath.endsWith("s") ? routePath.slice(0, -1) : routePath;
42038
41734
  const model = flags.model;
42039
41735
  const isPublic = Boolean(flags.public);
42040
- const base = resolve29("src/routes/api", routePath);
42041
- const idDir = join34(base, "[id]");
41736
+ const base = resolve28("src/routes/api", routePath);
41737
+ const idDir = join33(base, "[id]");
42042
41738
  ensureDir(base);
42043
41739
  ensureDir(idDir);
42044
41740
  const table2 = model ? toTableName(model) : "";
@@ -42049,7 +41745,7 @@ function generateRoute(name, flags, emitTest = true) {
42049
41745
  const writeDoc = isPublic ? "Public (--public): no token required." : "Secure by default: requires a Bearer token (use --public to open).";
42050
41746
  if (model) {
42051
41747
  writeFileSafe(
42052
- join34(base, "get.ts"),
41748
+ join33(base, "get.ts"),
42053
41749
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42054
41750
  ${modelImportBase}
42055
41751
  export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
@@ -42065,7 +41761,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42065
41761
  );
42066
41762
  } else {
42067
41763
  writeFileSafe(
42068
- join34(base, "get.ts"),
41764
+ join33(base, "get.ts"),
42069
41765
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42070
41766
 
42071
41767
  export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
@@ -42084,7 +41780,7 @@ ${aiFill(`list_${routePath}`, {
42084
41780
  }
42085
41781
  if (model) {
42086
41782
  writeFileSafe(
42087
- join34(base, "post.ts"),
41783
+ join33(base, "post.ts"),
42088
41784
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42089
41785
  ${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
42090
41786
 
@@ -42106,7 +41802,7 @@ ${extend(
42106
41802
  );
42107
41803
  } else {
42108
41804
  writeFileSafe(
42109
- join34(base, "post.ts"),
41805
+ join33(base, "post.ts"),
42110
41806
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42111
41807
 
42112
41808
  ${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
@@ -42126,7 +41822,7 @@ ${aiFill(`create_${singular}`, {
42126
41822
  }
42127
41823
  if (model) {
42128
41824
  writeFileSafe(
42129
- join34(idDir, "get.ts"),
41825
+ join33(idDir, "get.ts"),
42130
41826
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42131
41827
  ${modelImportId}
42132
41828
  export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
@@ -42144,7 +41840,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42144
41840
  );
42145
41841
  } else {
42146
41842
  writeFileSafe(
42147
- join34(idDir, "get.ts"),
41843
+ join33(idDir, "get.ts"),
42148
41844
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42149
41845
 
42150
41846
  export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
@@ -42163,7 +41859,7 @@ ${aiFill(`get_${singular}`, {
42163
41859
  }
42164
41860
  if (model) {
42165
41861
  writeFileSafe(
42166
- join34(idDir, "put.ts"),
41862
+ join33(idDir, "put.ts"),
42167
41863
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42168
41864
  ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
42169
41865
 
@@ -42191,7 +41887,7 @@ ${extend(
42191
41887
  );
42192
41888
  } else {
42193
41889
  writeFileSafe(
42194
- join34(idDir, "put.ts"),
41890
+ join33(idDir, "put.ts"),
42195
41891
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42196
41892
 
42197
41893
  ${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
@@ -42211,7 +41907,7 @@ ${aiFill(`update_${singular}`, {
42211
41907
  }
42212
41908
  if (model) {
42213
41909
  writeFileSafe(
42214
- join34(idDir, "delete.ts"),
41910
+ join33(idDir, "delete.ts"),
42215
41911
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42216
41912
  ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
42217
41913
 
@@ -42230,7 +41926,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42230
41926
  );
42231
41927
  } else {
42232
41928
  writeFileSafe(
42233
- join34(idDir, "delete.ts"),
41929
+ join33(idDir, "delete.ts"),
42234
41930
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42235
41931
 
42236
41932
  ${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
@@ -42275,7 +41971,7 @@ function generateCrud(name, flags) {
42275
41971
  }
42276
41972
  function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
42277
41973
  const ts = timestamp();
42278
- const dir = resolve29("migrations");
41974
+ const dir = resolve28("migrations");
42279
41975
  ensureDir(dir);
42280
41976
  let table2;
42281
41977
  if (tableOverride) {
@@ -42287,7 +41983,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
42287
41983
  const fields = fieldsOverride || parseFields(flags.fields || "");
42288
41984
  const isCreate = name.startsWith("create_") || fieldsOverride !== void 0;
42289
41985
  const fileName = `${ts}_${name}.sql`;
42290
- const path8 = join34(dir, fileName);
41986
+ const path8 = join33(dir, fileName);
42291
41987
  let upSql;
42292
41988
  let downSql;
42293
41989
  if (isCreate) {
@@ -42319,7 +42015,7 @@ ${upSql}
42319
42015
  ${downSql}
42320
42016
  `;
42321
42017
  writeFileSafe(path8, content);
42322
- const downPath = join34(dir, `${ts}_${name}.down.sql`);
42018
+ const downPath = join33(dir, `${ts}_${name}.down.sql`);
42323
42019
  const downContent = `-- Rollback: ${name}
42324
42020
  -- Created: ${now}
42325
42021
 
@@ -42330,9 +42026,9 @@ ${downSql}
42330
42026
  }
42331
42027
  function generateMiddleware(name, _flags) {
42332
42028
  const snake = toSnake(name);
42333
- const dir = resolve29("src/middleware");
42029
+ const dir = resolve28("src/middleware");
42334
42030
  ensureDir(dir);
42335
- const path8 = join34(dir, `${snake}.ts`);
42031
+ const path8 = join33(dir, `${snake}.ts`);
42336
42032
  const content = `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42337
42033
 
42338
42034
  /**
@@ -42371,9 +42067,9 @@ function generateTest(name, flags) {
42371
42067
  const snake = toSnake(name);
42372
42068
  const singular = snake.endsWith("s") ? snake.slice(0, -1) : snake;
42373
42069
  const model = flags.model;
42374
- const dir = resolve29("tests");
42070
+ const dir = resolve28("tests");
42375
42071
  ensureDir(dir);
42376
- const path8 = join34(dir, `${snake}.test.ts`);
42072
+ const path8 = join33(dir, `${snake}.test.ts`);
42377
42073
  if (model && flags["secure-writes"]) {
42378
42074
  const isPublic = Boolean(flags.public);
42379
42075
  const posture = isPublic ? "open (--public)" : "gated";
@@ -42508,9 +42204,9 @@ function generateForm(name, flags) {
42508
42204
  datetime: "datetime-local",
42509
42205
  blob: "file"
42510
42206
  };
42511
- const dir = resolve29("src/templates/forms");
42207
+ const dir = resolve28("src/templates/forms");
42512
42208
  ensureDir(dir);
42513
- const path8 = join34(dir, `${table2}.twig`);
42209
+ const path8 = join33(dir, `${table2}.twig`);
42514
42210
  let fieldHtml = "";
42515
42211
  for (const [fname, ftype] of fields) {
42516
42212
  const itype = inputTypes[ftype] || "text";
@@ -42560,9 +42256,9 @@ function generateView(name, flags) {
42560
42256
  const table2 = toTableName(name);
42561
42257
  const routeName = toPlural(table2);
42562
42258
  const cols = fields.map(([f]) => f);
42563
- const dir = resolve29("src/templates/pages");
42259
+ const dir = resolve28("src/templates/pages");
42564
42260
  ensureDir(dir);
42565
- const listPath = join34(dir, `${routeName}.twig`);
42261
+ const listPath = join33(dir, `${routeName}.twig`);
42566
42262
  const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
42567
42263
  const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
42568
42264
  const listContent = `{% extends "base.twig" %}
@@ -42598,7 +42294,7 @@ ${td}
42598
42294
  {% endblock %}
42599
42295
  `;
42600
42296
  writeFileSafe(listPath, listContent);
42601
- const detailPath = join34(dir, `${table2}.twig`);
42297
+ const detailPath = join33(dir, `${table2}.twig`);
42602
42298
  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");
42603
42299
  const detailContent = `{% extends "base.twig" %}
42604
42300
  {% block title %}${name} Detail{% endblock %}
@@ -42620,14 +42316,14 @@ ${detailFields}
42620
42316
  function generateAuth(_flags) {
42621
42317
  console.log("\n Generating authentication scaffolding...\n");
42622
42318
  generateModel("User", { fields: "email:string,password:string,role:string" }, false);
42623
- const registerDir = resolve29("src/routes/api/auth/register");
42624
- const loginDir = resolve29("src/routes/api/auth/login");
42625
- const meDir = resolve29("src/routes/api/auth/me");
42319
+ const registerDir = resolve28("src/routes/api/auth/register");
42320
+ const loginDir = resolve28("src/routes/api/auth/login");
42321
+ const meDir = resolve28("src/routes/api/auth/me");
42626
42322
  ensureDir(registerDir);
42627
42323
  ensureDir(loginDir);
42628
42324
  ensureDir(meDir);
42629
42325
  writeFileSafe(
42630
- join34(registerDir, "post.ts"),
42326
+ join33(registerDir, "post.ts"),
42631
42327
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42632
42328
  import { hashPassword } from "tina4-nodejs";
42633
42329
  import User from "../../../../models/User.js";
@@ -42658,7 +42354,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42658
42354
  `
42659
42355
  );
42660
42356
  writeFileSafe(
42661
- join34(loginDir, "post.ts"),
42357
+ join33(loginDir, "post.ts"),
42662
42358
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42663
42359
  import { checkPassword, getToken } from "tina4-nodejs";
42664
42360
  import User from "../../../../models/User.js";
@@ -42689,7 +42385,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42689
42385
  `
42690
42386
  );
42691
42387
  writeFileSafe(
42692
- join34(meDir, "get.ts"),
42388
+ join33(meDir, "get.ts"),
42693
42389
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42694
42390
  import { authenticateRequest } from "tina4-nodejs";
42695
42391
 
@@ -42705,10 +42401,10 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42705
42401
  }
42706
42402
  `
42707
42403
  );
42708
- const formsDir = resolve29("src/templates/forms");
42404
+ const formsDir = resolve28("src/templates/forms");
42709
42405
  ensureDir(formsDir);
42710
42406
  writeFileSafe(
42711
- join34(formsDir, "login.twig"),
42407
+ join33(formsDir, "login.twig"),
42712
42408
  `{% extends "base.twig" %}
42713
42409
  {% block title %}Login{% endblock %}
42714
42410
  {% block content %}
@@ -42732,7 +42428,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42732
42428
  `
42733
42429
  );
42734
42430
  writeFileSafe(
42735
- join34(formsDir, "register.twig"),
42431
+ join33(formsDir, "register.twig"),
42736
42432
  `{% extends "base.twig" %}
42737
42433
  {% block title %}Register{% endblock %}
42738
42434
  {% block content %}
@@ -42766,9 +42462,9 @@ function generateService(name, flags) {
42766
42462
  const snake = toSnake(name);
42767
42463
  const camel = toCamel(toPascal(name)) || snake;
42768
42464
  const cron = flags.cron;
42769
- const dir = resolve29("src/services");
42465
+ const dir = resolve28("src/services");
42770
42466
  ensureDir(dir);
42771
- const path8 = join34(dir, `${snake}.ts`);
42467
+ const path8 = join33(dir, `${snake}.ts`);
42772
42468
  let scheduleField;
42773
42469
  let note;
42774
42470
  if (cron && cron !== true) {
@@ -42817,9 +42513,9 @@ function generateQueue(name, _flags) {
42817
42513
  const topic = name.replace(/^\//, "");
42818
42514
  const slug = toSnake(topic.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
42819
42515
  const pascal = toPascal(topic) || "Topic";
42820
- const dir = resolve29("src/services");
42516
+ const dir = resolve28("src/services");
42821
42517
  ensureDir(dir);
42822
- const path8 = join34(dir, `${slug}_consumer.ts`);
42518
+ const path8 = join33(dir, `${slug}_consumer.ts`);
42823
42519
  const body = aiFill(`handle${pascal}`, {
42824
42520
  intent: `process ONE ${topic} job payload`,
42825
42521
  given: "payload -> the produced job data (job.payload)",
@@ -42877,9 +42573,9 @@ export default {
42877
42573
  emitQueueTest(topic, slug, pascal);
42878
42574
  }
42879
42575
  function generateValidator(name, _flags) {
42880
- const dir = resolve29("src/validators");
42576
+ const dir = resolve28("src/validators");
42881
42577
  ensureDir(dir);
42882
- const path8 = join34(dir, `${toSnake(name)}.ts`);
42578
+ const path8 = join33(dir, `${toSnake(name)}.ts`);
42883
42579
  const rules = extend(
42884
42580
  "add / adjust the validation rules for this payload",
42885
42581
  `e.g. .email("email").minLength("name", 2).integer("age"); ground: tina4_context("validate request body with Validator", "nodejs")`
@@ -42904,9 +42600,9 @@ ${rules} validator.required("name"); // starter rule (matches the model's def
42904
42600
  }
42905
42601
  function generateSeeder(name, _flags) {
42906
42602
  const table2 = toTableName(name);
42907
- const dir = resolve29("src/seeds");
42603
+ const dir = resolve28("src/seeds");
42908
42604
  ensureDir(dir);
42909
- const path8 = join34(dir, `${table2}_seeder.ts`);
42605
+ const path8 = join33(dir, `${table2}_seeder.ts`);
42910
42606
  const overrides = extend(
42911
42607
  "override fields that need a specific shape (seedOrm auto-fills the rest)",
42912
42608
  `e.g. return { email: (f) => f.email(), status: "active" }; ground: tina4_context("seed ORM model with FakeData", "nodejs")`
@@ -42948,9 +42644,9 @@ function generateWebsocket(name, _flags) {
42948
42644
  let slug = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
42949
42645
  const base = slug.startsWith("ws_") ? slug.slice(3) : slug;
42950
42646
  const handlerName = `${toCamel(toPascal(base))}Ws`;
42951
- const dir = resolve29("src/routes");
42647
+ const dir = resolve28("src/routes");
42952
42648
  ensureDir(dir);
42953
- const path8 = join34(dir, `ws_${base}.ts`);
42649
+ const path8 = join33(dir, `ws_${base}.ts`);
42954
42650
  const body = aiFill(handlerName, {
42955
42651
  intent: `handle an inbound "message" frame on ${wsPath}`,
42956
42652
  given: "data -> the message payload (string); connection -> WebSocketConnection",
@@ -42997,9 +42693,9 @@ function generateListener(name, _flags) {
42997
42693
  const event = name.trim();
42998
42694
  const slug = toSnake(event.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
42999
42695
  const handlerName = `on${toPascal(slug)}`;
43000
- const dir = resolve29("src/listeners");
42696
+ const dir = resolve28("src/listeners");
43001
42697
  ensureDir(dir);
43002
- const path8 = join34(dir, `${slug}.ts`);
42698
+ const path8 = join33(dir, `${slug}.ts`);
43003
42699
  const body = aiFill(handlerName, {
43004
42700
  intent: `react to the '${event}' event`,
43005
42701
  given: `args -> whatever Events.emit("${event}", ...args) passed`,
@@ -43028,9 +42724,9 @@ Events.on("${event}", ${handlerName});
43028
42724
  emitListenerTest(event, slug);
43029
42725
  }
43030
42726
  function writeTest(testName, content) {
43031
- const dir = resolve29("tests");
42727
+ const dir = resolve28("tests");
43032
42728
  ensureDir(dir);
43033
- writeFileSafe(join34(dir, `${testName}.test.ts`), content);
42729
+ writeFileSafe(join33(dir, `${testName}.test.ts`), content);
43034
42730
  }
43035
42731
  function standaloneTest(doc, body) {
43036
42732
  return `${doc}
@@ -43414,15 +43110,15 @@ assert("DOWN drops the ${table2} table", db.tableExists("${table2}") === false);
43414
43110
  }
43415
43111
 
43416
43112
  // src/commands/seed.ts
43417
- import { existsSync as existsSync34, readdirSync as readdirSync23 } from "node:fs";
43418
- import { resolve as resolve30, join as join35 } from "node:path";
43113
+ import { existsSync as existsSync33, readdirSync as readdirSync22 } from "node:fs";
43114
+ import { resolve as resolve29, join as join34 } from "node:path";
43419
43115
  import { execSync as execSync4 } from "node:child_process";
43420
43116
  async function runSeeds(seedPath) {
43421
43117
  const cwd = process.cwd();
43422
- const seedDir = resolve30(cwd, "src/seeds");
43118
+ const seedDir = resolve29(cwd, "src/seeds");
43423
43119
  if (seedPath) {
43424
- const file = resolve30(seedPath);
43425
- if (!existsSync34(file)) {
43120
+ const file = resolve29(seedPath);
43121
+ if (!existsSync33(file)) {
43426
43122
  console.error(` Error: Seed file not found: ${seedPath}`);
43427
43123
  process.exit(1);
43428
43124
  }
@@ -43435,14 +43131,14 @@ async function runSeeds(seedPath) {
43435
43131
  }
43436
43132
  return;
43437
43133
  }
43438
- if (!existsSync34(seedDir)) {
43134
+ if (!existsSync33(seedDir)) {
43439
43135
  console.log(" No seeds directory found.");
43440
43136
  console.log(" Create seed files in src/seeds/ (e.g. src/seeds/001-users.ts)");
43441
43137
  return;
43442
43138
  }
43443
43139
  let seedFiles;
43444
43140
  try {
43445
- seedFiles = readdirSync23(seedDir).filter((f) => f.endsWith(".ts")).sort().map((f) => join35(seedDir, f));
43141
+ seedFiles = readdirSync22(seedDir).filter((f) => f.endsWith(".ts")).sort().map((f) => join34(seedDir, f));
43446
43142
  } catch {
43447
43143
  console.log(" Could not read src/seeds/ directory.");
43448
43144
  return;
@@ -43580,8 +43276,8 @@ function runMetrics(args = []) {
43580
43276
  // src/commands/queue.ts
43581
43277
  init_dotenv();
43582
43278
  init_queue();
43583
- import { readdirSync as readdirSync24, statSync as statSync19 } from "node:fs";
43584
- import { extname as extname9, join as join36 } from "node:path";
43279
+ import { readdirSync as readdirSync23, statSync as statSync19 } from "node:fs";
43280
+ import { extname as extname9, join as join35 } from "node:path";
43585
43281
  import { pathToFileURL as pathToFileURL2 } from "node:url";
43586
43282
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["once", "json"]);
43587
43283
  function parseFlags2(args) {
@@ -43613,7 +43309,7 @@ async function resolveQueueHandler(servicesDir, topic) {
43613
43309
  let entries;
43614
43310
  try {
43615
43311
  if (!statSync19(servicesDir).isDirectory()) return null;
43616
- entries = readdirSync24(servicesDir);
43312
+ entries = readdirSync23(servicesDir);
43617
43313
  } catch {
43618
43314
  return null;
43619
43315
  }
@@ -43621,7 +43317,7 @@ async function resolveQueueHandler(servicesDir, topic) {
43621
43317
  if (entry.startsWith("_")) continue;
43622
43318
  const ext = extname9(entry);
43623
43319
  if (ext !== ".ts" && ext !== ".js") continue;
43624
- const fullPath = join36(servicesDir, entry);
43320
+ const fullPath = join35(servicesDir, entry);
43625
43321
  try {
43626
43322
  if (!statSync19(fullPath).isFile()) continue;
43627
43323
  const mod = await import(pathToFileURL2(fullPath).href);
@@ -43773,8 +43469,8 @@ async function queueCommand(args = []) {
43773
43469
  }
43774
43470
 
43775
43471
  // src/commands/build.ts
43776
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync35, statSync as statSync20 } from "node:fs";
43777
- import { basename as basename8, delimiter as delimiter2, join as join37 } from "node:path";
43472
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync34, statSync as statSync20 } from "node:fs";
43473
+ import { basename as basename7, delimiter as delimiter2, join as join36 } from "node:path";
43778
43474
  import { spawnSync as spawnSync3 } from "node:child_process";
43779
43475
  function parseFlags3(args) {
43780
43476
  const flags = {};
@@ -43803,7 +43499,7 @@ function whichDocker() {
43803
43499
  for (const dir of pathValue.split(delimiter2)) {
43804
43500
  if (!dir) continue;
43805
43501
  for (const ext of exts) {
43806
- const candidate = join37(dir, `docker${ext}`);
43502
+ const candidate = join36(dir, `docker${ext}`);
43807
43503
  try {
43808
43504
  accessSync2(candidate, constants2.X_OK);
43809
43505
  if (statSync20(candidate).isFile()) return candidate;
@@ -43817,11 +43513,11 @@ function buildImage(args) {
43817
43513
  const flags = parseFlags3(args);
43818
43514
  let tag = typeof flags.tag === "string" ? flags.tag : "";
43819
43515
  if (!tag) {
43820
- const dirName = basename8(process.cwd()).toLowerCase();
43516
+ const dirName = basename7(process.cwd()).toLowerCase();
43821
43517
  tag = `${dirName || "tina4app"}:latest`;
43822
43518
  }
43823
43519
  const dockerfile = typeof flags.file === "string" && flags.file ? flags.file : "Dockerfile";
43824
- if (!existsSync35(dockerfile) || !statSync20(dockerfile).isFile()) {
43520
+ if (!existsSync34(dockerfile) || !statSync20(dockerfile).isFile()) {
43825
43521
  console.log(` \u2717 No ${dockerfile} found.`);
43826
43522
  console.log(" A Tina4 app deploys as a container. Scaffold a Dockerfile first:");
43827
43523
  console.log(" tina4 deploy docker (or: tina4nodejs init)");
@@ -43849,30 +43545,30 @@ function buildImage(args) {
43849
43545
 
43850
43546
  // src/bin.ts
43851
43547
  import { execSync as execSync5, spawnSync as spawnSync4 } from "node:child_process";
43852
- import { existsSync as existsSync36, readFileSync as readFileSync28, statSync as statSync21 } from "node:fs";
43853
- import { delimiter as delimiter3, dirname as dirname15, join as join38 } from "node:path";
43548
+ import { existsSync as existsSync35, readFileSync as readFileSync27, statSync as statSync21 } from "node:fs";
43549
+ import { delimiter as delimiter3, dirname as dirname14, join as join37 } from "node:path";
43854
43550
  import { fileURLToPath as fileURLToPath8, pathToFileURL as pathToFileURL3 } from "node:url";
43855
43551
  function readCliVersion() {
43856
- let dir = dirname15(fileURLToPath8(import.meta.url));
43552
+ let dir = dirname14(fileURLToPath8(import.meta.url));
43857
43553
  for (let i = 0; i < 6; i++) {
43858
- const pkgPath = join38(dir, "package.json");
43859
- if (existsSync36(pkgPath)) {
43554
+ const pkgPath = join37(dir, "package.json");
43555
+ if (existsSync35(pkgPath)) {
43860
43556
  try {
43861
- const pkg = JSON.parse(readFileSync28(pkgPath, "utf-8"));
43557
+ const pkg = JSON.parse(readFileSync27(pkgPath, "utf-8"));
43862
43558
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
43863
43559
  } catch {
43864
43560
  }
43865
43561
  }
43866
- const parent = dirname15(dir);
43562
+ const parent = dirname14(dir);
43867
43563
  if (parent === dir) break;
43868
43564
  dir = parent;
43869
43565
  }
43870
43566
  return "0.0.0";
43871
43567
  }
43872
43568
  function inContainer() {
43873
- if (existsSync36("/.dockerenv") || existsSync36("/run/.containerenv")) return true;
43569
+ if (existsSync35("/.dockerenv") || existsSync35("/run/.containerenv")) return true;
43874
43570
  try {
43875
- const blob = readFileSync28("/proc/1/cgroup", "utf-8");
43571
+ const blob = readFileSync27("/proc/1/cgroup", "utf-8");
43876
43572
  return blob.includes("docker") || blob.includes("containerd") || blob.includes("kubepods");
43877
43573
  } catch {
43878
43574
  return false;
@@ -44017,7 +43713,7 @@ async function openConsole() {
44017
43713
  r.context.Database = Database2;
44018
43714
  r.context.Log = Log2;
44019
43715
  r.context.db = db;
44020
- await new Promise((resolve31) => r.on("exit", resolve31));
43716
+ await new Promise((resolve30) => r.on("exit", resolve30));
44021
43717
  }
44022
43718
  async function installAiContext(args) {
44023
43719
  const { showMenu: showMenu2, installSelected: installSelected2, installAll: installAll2 } = await Promise.resolve().then(() => (init_ai(), ai_exports));
@@ -44175,7 +43871,7 @@ function findClient() {
44175
43871
  for (const dir of (process.env.PATH ?? "").split(delimiter3)) {
44176
43872
  if (!dir) continue;
44177
43873
  for (const name of names) {
44178
- const candidate = join38(dir, name);
43874
+ const candidate = join37(dir, name);
44179
43875
  try {
44180
43876
  if (statSync21(candidate).isFile()) return candidate;
44181
43877
  } catch {