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
@@ -1828,64 +1828,6 @@ var init_middleware = __esm({
1828
1828
  }
1829
1829
  });
1830
1830
 
1831
- // ../orm/src/types.ts
1832
- var FetchResult;
1833
- var init_types = __esm({
1834
- "../orm/src/types.ts"() {
1835
- "use strict";
1836
- FetchResult = class {
1837
- records;
1838
- count;
1839
- sql;
1840
- constructor(records, sql = "") {
1841
- this.records = records;
1842
- this.count = records.length;
1843
- this.sql = sql;
1844
- }
1845
- /** Paginate the in-memory result set. */
1846
- toPaginate(page = 1, perPage = 20) {
1847
- const total = this.count;
1848
- const totalPages = Math.max(1, Math.ceil(total / perPage));
1849
- const offset = (page - 1) * perPage;
1850
- const data = this.records.slice(offset, offset + perPage);
1851
- return {
1852
- data,
1853
- page,
1854
- perPage,
1855
- total,
1856
- totalPages,
1857
- hasNext: page < totalPages,
1858
- hasPrev: page > 1
1859
- };
1860
- }
1861
- /** Return the first record or null. */
1862
- first() {
1863
- return this.records[0] ?? null;
1864
- }
1865
- /** Return the last record or null. */
1866
- last() {
1867
- return this.records[this.records.length - 1] ?? null;
1868
- }
1869
- /** Check if result is empty. */
1870
- isEmpty() {
1871
- return this.records.length === 0;
1872
- }
1873
- /** Convert to plain array. */
1874
- toArray() {
1875
- return [...this.records];
1876
- }
1877
- /** Convert to JSON string. */
1878
- toJSON() {
1879
- return JSON.stringify(this.records);
1880
- }
1881
- /** Iterate over records. */
1882
- [Symbol.iterator]() {
1883
- return this.records[Symbol.iterator]();
1884
- }
1885
- };
1886
- }
1887
- });
1888
-
1889
1831
  // ../orm/src/databaseResult.ts
1890
1832
  var DatabaseResult;
1891
1833
  var init_databaseResult = __esm({
@@ -1949,75 +1891,52 @@ var init_databaseResult = __esm({
1949
1891
  toArray() {
1950
1892
  return this.records;
1951
1893
  }
1952
- /** Pagination envelope — accepts either (page, perPage) or (offset, limit) style.
1894
+ /**
1895
+ * Describe the page this result IS — the canonical pagination envelope.
1953
1896
  *
1954
- * When called with two arguments both >= 0 and the first >= the second
1955
- * (i.e. offset-style), pass `{ offset, limit }` as the first argument.
1956
- * The simplest way is to always use the default (page, perPage) form and
1957
- * let the autoCRUD layer supply offset/limit from the query string.
1897
+ * Takes NO arguments and derives every field from the query that produced this
1898
+ * result (ADR-0043). Passing ANY argument RAISES: a DatabaseResult holds no
1899
+ * connection, so an argument could only re-slice the rows already in memory and
1900
+ * then report total_pages for pages it can never reach. To read page N, FETCH
1901
+ * page N (limit + offset) and call this with no arguments.
1958
1902
  *
1959
- * Returns a superset of keys for backwards-compatibility across all clients.
1960
- */
1961
- /**
1962
- * Describe the page this result actually IS. Takes no arguments.
1903
+ * The envelope is EXACTLY seven snake_case keys, identical across all four
1904
+ * frameworks: `records, total, page, per_page, total_pages, limit, offset`.
1963
1905
  *
1964
- * MEASURED 2026-08-05 on a real 250-row table read with limit=20 offset=40
1965
- * (page 3 of 13): this reported page 1 of 2 and returned 10 of the 20 rows.
1966
- * It ignored the query entirely - defaulting page to 1 and perPage to 10 -
1967
- * then re-sliced the rows it was handed, which were already just that page.
1968
- * So a caller who paginated correctly at the SQL level had the answer
1969
- * silently re-paginated underneath them, with a page number that was simply
1970
- * wrong.
1906
+ * per_page = the query's limit
1907
+ * page = floor(offset / limit) + 1
1908
+ * total = the TRUE total for the filter Database.fetch (and
1909
+ * QueryBuilder.get) run a COUNT probe whenever a limit was
1910
+ * applied NEVER the number of rows returned
1911
+ * total_pages = ceil(total / per_page)
1912
+ * records = the rows the query returned, VERBATIM (never re-sliced)
1913
+ * limit = the SQL limit actually applied
1914
+ * offset = the SQL offset actually applied
1971
1915
  *
1972
- * WITH page/perPage it slices this result in memory, the behaviour GitHub
1973
- * issue #106 asked for. Valid ONLY when the result holds the WHOLE set
1974
- * (records.length >= count). A PARTIAL result cannot be sliced by page number
1975
- * without lying: MEASURED on 100,000 rows read under the default cap of 100,
1976
- * pages 1-5 of 20 were right and every page from 6 onward came back EMPTY
1977
- * while totalPages reported 5,000.
1916
+ * The JSON payload is snake_case even though the method name is camelCase — a
1917
+ * JSON key is data, not a language surface (ADR-0043). The old duplicate and
1918
+ * camelCase keys (`data`, `count`, `perPage`, `totalPages`, `has_next`,
1919
+ * `has_prev`) are removed: Node emitted 13 keys, the worst offender of the four.
1978
1920
  *
1979
- * `total` is `count`, and `count` is now the TRUE total for the filter in
1980
- * all four frameworks - Database.fetch runs a COUNT probe whenever it applied
1981
- * a limit. It used to be ROWS RETURNED here and in Ruby while Python and PHP
1982
- * probed, so one query answered 20 in two frameworks and 250 in the other
1983
- * two.
1921
+ * @throws {TypeError} if called with any argument.
1984
1922
  */
1985
- toPaginate(page, perPage) {
1986
- if ((page !== void 0 || perPage !== void 0) && this.records.length < this.count) {
1923
+ toPaginate() {
1924
+ if (arguments.length > 0) {
1987
1925
  throw new TypeError(
1988
- `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.`
1926
+ "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."
1989
1927
  );
1990
1928
  }
1991
- let resolvedPerPage;
1992
- let resolvedPage;
1993
- let offset;
1994
- let rows;
1995
- if (page === void 0 && perPage === void 0) {
1996
- resolvedPerPage = this.limit > 0 ? this.limit : this.records.length;
1997
- resolvedPage = resolvedPerPage > 0 ? Math.floor(this.offset / resolvedPerPage) + 1 : 1;
1998
- offset = this.offset;
1999
- rows = this.records;
2000
- } else {
2001
- resolvedPage = page ?? 1;
2002
- resolvedPerPage = perPage ?? (this.limit > 0 ? this.limit : 10);
2003
- offset = (resolvedPage - 1) * resolvedPerPage;
2004
- rows = this.records.slice(offset, offset + resolvedPerPage);
2005
- }
2006
- const totalPages = resolvedPerPage > 0 ? Math.max(1, Math.ceil(this.count / resolvedPerPage)) : 1;
1929
+ const perPage = this.limit > 0 ? this.limit : this.records.length;
1930
+ const page = perPage > 0 ? Math.floor(this.offset / perPage) + 1 : 1;
1931
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this.count / perPage)) : 1;
2007
1932
  return {
2008
- records: rows,
2009
- data: rows,
2010
- count: this.count,
1933
+ records: this.records,
2011
1934
  total: this.count,
2012
- limit: resolvedPerPage,
2013
- offset,
2014
- page: resolvedPage,
2015
- per_page: resolvedPerPage,
2016
- perPage: resolvedPerPage,
2017
- totalPages,
1935
+ page,
1936
+ per_page: perPage,
2018
1937
  total_pages: totalPages,
2019
- has_next: resolvedPage < totalPages,
2020
- has_prev: resolvedPage > 1
1938
+ limit: perPage,
1939
+ offset: this.offset
2021
1940
  };
2022
1941
  }
2023
1942
  /** Iterable — for (const row of result) */
@@ -3861,7 +3780,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
3861
3780
  const elapsedMs = () => performance.now() - startedAt;
3862
3781
  if (budgetMs === null) return attempt();
3863
3782
  const started = attempt();
3864
- return new Promise((resolve21, reject) => {
3783
+ return new Promise((resolve20, reject) => {
3865
3784
  let expired = false;
3866
3785
  const timer = setTimeout(() => {
3867
3786
  expired = true;
@@ -3871,7 +3790,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
3871
3790
  (arrived) => {
3872
3791
  clearTimeout(timer);
3873
3792
  if (expired) abandon?.(arrived);
3874
- else resolve21(arrived);
3793
+ else resolve20(arrived);
3875
3794
  },
3876
3795
  (failure) => {
3877
3796
  clearTimeout(timer);
@@ -4428,10 +4347,10 @@ var init_mysql = __esm({
4428
4347
  ...timeoutOption
4429
4348
  });
4430
4349
  }
4431
- return new Promise((resolve21, reject) => {
4350
+ return new Promise((resolve20, reject) => {
4432
4351
  this.connection.connect((err) => {
4433
4352
  if (err) reject(err);
4434
- else resolve21();
4353
+ else resolve20();
4435
4354
  });
4436
4355
  });
4437
4356
  },
@@ -4453,10 +4372,10 @@ var init_mysql = __esm({
4453
4372
  }
4454
4373
  }
4455
4374
  queryPromise(sql, params) {
4456
- return new Promise((resolve21, reject) => {
4375
+ return new Promise((resolve20, reject) => {
4457
4376
  this.connection.query(sql, params ?? [], (err, results) => {
4458
4377
  if (err) reject(err);
4459
- else resolve21(results);
4378
+ else resolve20(results);
4460
4379
  });
4461
4380
  });
4462
4381
  }
@@ -4826,11 +4745,11 @@ var init_mssql = __esm({
4826
4745
  };
4827
4746
  }
4828
4747
  await withConnectTimeout(
4829
- () => new Promise((resolve21, reject) => {
4748
+ () => new Promise((resolve20, reject) => {
4830
4749
  this.connection = new Connection(tediousConfig);
4831
4750
  this.connection.on("connect", (err) => {
4832
4751
  if (err) reject(err);
4833
- else resolve21();
4752
+ else resolve20();
4834
4753
  });
4835
4754
  this.connection.connect();
4836
4755
  }),
@@ -4875,11 +4794,11 @@ var init_mssql = __esm({
4875
4794
  const tediousModule = requireTedious();
4876
4795
  const Request = tediousModule.Request;
4877
4796
  const TYPES = tediousModule.TYPES;
4878
- return new Promise((resolve21, reject) => {
4797
+ return new Promise((resolve20, reject) => {
4879
4798
  const rows = [];
4880
4799
  const request = new Request(sql, (err, rowCount) => {
4881
4800
  if (err) reject(err);
4882
- else resolve21({ rows, rowCount });
4801
+ else resolve20({ rows, rowCount });
4883
4802
  });
4884
4803
  if (params) {
4885
4804
  params.forEach((p, i) => {
@@ -5082,8 +5001,8 @@ var init_mssql = __esm({
5082
5001
  throw new Error("Use startTransactionAsync() for MSSQL.");
5083
5002
  }
5084
5003
  async startTransactionAsync() {
5085
- await new Promise((resolve21, reject) => {
5086
- this.connection.beginTransaction((err) => err ? reject(err) : resolve21());
5004
+ await new Promise((resolve20, reject) => {
5005
+ this.connection.beginTransaction((err) => err ? reject(err) : resolve20());
5087
5006
  });
5088
5007
  this._inTransaction = true;
5089
5008
  }
@@ -5091,8 +5010,8 @@ var init_mssql = __esm({
5091
5010
  throw new Error("Use commitAsync() for MSSQL.");
5092
5011
  }
5093
5012
  async commitAsync() {
5094
- await new Promise((resolve21, reject) => {
5095
- this.connection.commitTransaction((err) => err ? reject(err) : resolve21());
5013
+ await new Promise((resolve20, reject) => {
5014
+ this.connection.commitTransaction((err) => err ? reject(err) : resolve20());
5096
5015
  });
5097
5016
  this._inTransaction = false;
5098
5017
  }
@@ -5100,8 +5019,8 @@ var init_mssql = __esm({
5100
5019
  throw new Error("Use rollbackAsync() for MSSQL.");
5101
5020
  }
5102
5021
  async rollbackAsync() {
5103
- await new Promise((resolve21, reject) => {
5104
- this.connection.rollbackTransaction((err) => err ? reject(err) : resolve21());
5022
+ await new Promise((resolve20, reject) => {
5023
+ this.connection.rollbackTransaction((err) => err ? reject(err) : resolve20());
5105
5024
  });
5106
5025
  this._inTransaction = false;
5107
5026
  }
@@ -5371,8 +5290,8 @@ var init_firebird = __esm({
5371
5290
  fbConfig.database = normalizeFirebirdDbIdentifier(fbConfig.database);
5372
5291
  }
5373
5292
  this.db = await withConnectTimeout(
5374
- () => new Promise((resolve21, reject) => {
5375
- fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve21(db));
5293
+ () => new Promise((resolve20, reject) => {
5294
+ fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve20(db));
5376
5295
  }),
5377
5296
  connectTimeoutMillis(),
5378
5297
  fbConfig.host,
@@ -5439,20 +5358,20 @@ var init_firebird = __esm({
5439
5358
  return this.transaction ?? this.db;
5440
5359
  }
5441
5360
  queryPromise(sql, params) {
5442
- return new Promise((resolve21, reject) => {
5361
+ return new Promise((resolve20, reject) => {
5443
5362
  const translated = this.translateSql(sql);
5444
5363
  this.statementHandle().query(translated, params ?? [], (err, result) => {
5445
5364
  if (err) reject(err);
5446
- else resolve21(result ?? []);
5365
+ else resolve20(result ?? []);
5447
5366
  });
5448
5367
  });
5449
5368
  }
5450
5369
  executePromise(sql, params) {
5451
- return new Promise((resolve21, reject) => {
5370
+ return new Promise((resolve20, reject) => {
5452
5371
  const translated = this.translateSql(sql);
5453
5372
  this.statementHandle().execute(translated, params ?? [], (err) => {
5454
5373
  if (err) reject(err);
5455
- else resolve21();
5374
+ else resolve20();
5456
5375
  });
5457
5376
  });
5458
5377
  }
@@ -5568,12 +5487,12 @@ var init_firebird = __esm({
5568
5487
  }
5569
5488
  async startTransactionAsync() {
5570
5489
  this.ensureConnected();
5571
- await new Promise((resolve21, reject) => {
5490
+ await new Promise((resolve20, reject) => {
5572
5491
  this.db.transaction(0, (err, transaction) => {
5573
5492
  if (err) reject(err);
5574
5493
  else {
5575
5494
  this.transaction = transaction;
5576
- resolve21();
5495
+ resolve20();
5577
5496
  }
5578
5497
  });
5579
5498
  });
@@ -5583,12 +5502,12 @@ var init_firebird = __esm({
5583
5502
  }
5584
5503
  async commitAsync() {
5585
5504
  if (!this.transaction) throw new Error("No active transaction to commit.");
5586
- await new Promise((resolve21, reject) => {
5505
+ await new Promise((resolve20, reject) => {
5587
5506
  this.transaction.commit((err) => {
5588
5507
  if (err) reject(err);
5589
5508
  else {
5590
5509
  this.transaction = null;
5591
- resolve21();
5510
+ resolve20();
5592
5511
  }
5593
5512
  });
5594
5513
  });
@@ -5598,12 +5517,12 @@ var init_firebird = __esm({
5598
5517
  }
5599
5518
  async rollbackAsync() {
5600
5519
  if (!this.transaction) throw new Error("No active transaction to rollback.");
5601
- await new Promise((resolve21, reject) => {
5520
+ await new Promise((resolve20, reject) => {
5602
5521
  this.transaction.rollback((err) => {
5603
5522
  if (err) reject(err);
5604
5523
  else {
5605
5524
  this.transaction = null;
5606
- resolve21();
5525
+ resolve20();
5607
5526
  }
5608
5527
  });
5609
5528
  });
@@ -6664,6 +6583,7 @@ __export(database_exports, {
6664
6583
  getNamedAdapter: () => getNamedAdapter,
6665
6584
  initDatabase: () => initDatabase,
6666
6585
  parseDatabaseUrl: () => parseDatabaseUrl,
6586
+ probeTotal: () => probeTotal,
6667
6587
  resetRequestCaches: () => resetRequestCaches,
6668
6588
  resolveDbPool: () => resolveDbPool,
6669
6589
  setAdapter: () => setAdapter,
@@ -6717,6 +6637,29 @@ async function adapterCreateTable(adapter, name, columns) {
6717
6637
  if (adapter.createTableAsync) await adapter.createTableAsync(name, columns);
6718
6638
  else adapter.createTable(name, columns);
6719
6639
  }
6640
+ async function probeTotal(adapter, sql, params, limit) {
6641
+ if (limit === void 0 || limit <= 0) return void 0;
6642
+ try {
6643
+ const alias = adapter.countSubqueryAlias;
6644
+ const suffix = alias ? ` AS ${alias}` : "";
6645
+ const rows = await adapterFetch(
6646
+ adapter,
6647
+ `SELECT COUNT(*) AS tina4_total FROM (${sql}
6648
+ )${suffix}`,
6649
+ params,
6650
+ void 0,
6651
+ void 0,
6652
+ true
6653
+ );
6654
+ const row = Array.isArray(rows) ? rows[0] : void 0;
6655
+ if (!row) return void 0;
6656
+ const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
6657
+ const n = Number(value);
6658
+ return Number.isFinite(n) ? n : void 0;
6659
+ } catch {
6660
+ return void 0;
6661
+ }
6662
+ }
6720
6663
  function extractLastInsertId(result) {
6721
6664
  if (result && typeof result === "object") {
6722
6665
  const r = result;
@@ -7175,65 +7118,13 @@ var init_database = __esm({
7175
7118
  try {
7176
7119
  const rows = await adapterFetch(adapter, sql, params, limit, offset, opts?.noCache);
7177
7120
  this.lastError = null;
7178
- const total = await this.countProbe(adapter, sql, params, limit);
7121
+ const total = await probeTotal(adapter, sql, params, limit);
7179
7122
  return new DatabaseResult(rows, void 0, total, limit, offset, adapter, sql);
7180
7123
  } catch (e) {
7181
7124
  this.lastError = e?.message ?? String(e);
7182
7125
  throw e;
7183
7126
  }
7184
7127
  }
7185
- /**
7186
- * The true row count for `sql`, ignoring the pagination we appended.
7187
- *
7188
- * `count` is the TRUE TOTAL for the filter, not the number of rows this page
7189
- * returned. Node and Ruby used to populate it with `records.length` while
7190
- * Python and PHP populated it from a probe, so `db.fetch(sql).count` answered
7191
- * 20 here and 250 there for one query against one table, and every paginated
7192
- * response built on it under-reported. MEASURED 2026-08-05 on a 250-row table
7193
- * read with limit=20: Node reported total 20 over 2 pages against Python's
7194
- * 250 over 13.
7195
- *
7196
- * Only probed when a limit was actually applied. With no limit the rows
7197
- * returned ARE the whole answer for this SQL, so `records.length` is already
7198
- * the true total and a second round-trip would buy nothing — which is also
7199
- * what keeps `fetchAll()` at one query.
7200
- *
7201
- * BEST EFFORT, and it can never mask a real failure: it runs AFTER the main
7202
- * query (which has already thrown on bad SQL) and returns undefined on any
7203
- * error. `undefined` — not 0 — is the miss value, so DatabaseResult falls
7204
- * back to records.length, a true lower bound. Reporting 0 next to 100 real
7205
- * records would be the same "states a wrong number authoritatively" defect
7206
- * this change exists to remove.
7207
- *
7208
- * The closing paren goes on its OWN LINE: appended inline, a trailing
7209
- * `-- comment` in the caller's SQL comments it out and the probe dies with
7210
- * "incomplete input". Postgres, MySQL and MSSQL additionally require a name
7211
- * for the derived table; SQLite and Firebird do not, and Firebird rejects
7212
- * `AS` there — so the alias comes from the adapter, not an assumption.
7213
- */
7214
- async countProbe(adapter, sql, params, limit) {
7215
- if (limit === void 0 || limit <= 0) return void 0;
7216
- try {
7217
- const alias = adapter.countSubqueryAlias;
7218
- const suffix = alias ? ` AS ${alias}` : "";
7219
- const rows = await adapterFetch(
7220
- adapter,
7221
- `SELECT COUNT(*) AS tina4_total FROM (${sql}
7222
- )${suffix}`,
7223
- params,
7224
- void 0,
7225
- void 0,
7226
- true
7227
- );
7228
- const row = Array.isArray(rows) ? rows[0] : void 0;
7229
- if (!row) return void 0;
7230
- const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
7231
- const n = Number(value);
7232
- return Number.isFinite(n) ? n : void 0;
7233
- } catch {
7234
- return void 0;
7235
- }
7236
- }
7237
7128
  /**
7238
7129
  * Fetch a single row or null.
7239
7130
  *
@@ -7945,6 +7836,10 @@ async function discoverModels(modelsDir) {
7945
7836
  }
7946
7837
  const definition = {
7947
7838
  tableName: ModelClass.tableName,
7839
+ // The class name is the type name a generated OpenAPI client wants
7840
+ // (`Item`, not `items`). Carry it so Swagger keys components.schemas by
7841
+ // it. A model exported as `default` keeps its declared class name here.
7842
+ className: typeof ModelClass.name === "string" && ModelClass.name ? ModelClass.name : void 0,
7948
7843
  fields: ModelClass.fields,
7949
7844
  fieldMapping: ModelClass.fieldMapping,
7950
7845
  softDelete: ModelClass.softDelete ?? false,
@@ -8559,7 +8454,13 @@ async function status(adapter, options) {
8559
8454
  return result;
8560
8455
  }
8561
8456
  async function createMigration(description, options) {
8562
- if (options?.kind === "class") {
8457
+ const kind = (options?.kind ?? "sql").trim().toLowerCase();
8458
+ if (!["sql", "code", "class"].includes(kind)) {
8459
+ throw new Error(
8460
+ `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.`
8461
+ );
8462
+ }
8463
+ if (kind === "code" || kind === "class") {
8563
8464
  return createClassMigration(description, options);
8564
8465
  }
8565
8466
  const dir = resolve3(options?.migrationsDir ?? "migrations");
@@ -8686,15 +8587,13 @@ var init_migration = __esm({
8686
8587
  * Scaffold a new migration file.
8687
8588
  *
8688
8589
  * kind="sql" — creates {timestamp}_{description}.sql + .down.sql (default)
8689
- * kind="class" — creates {timestamp}_{description}.ts with a TypeScript class template
8590
+ * kind="code" — creates {timestamp}_{description}.ts with a TypeScript class
8591
+ * template. "class" is accepted as a legacy alias.
8690
8592
  *
8691
8593
  * Returns the path to the created up file (or class file).
8692
8594
  */
8693
8595
  async create(description, kind = "sql") {
8694
- if (kind === "class") {
8695
- return createClassMigration(description, { migrationsDir: this.dir });
8696
- }
8697
- return createMigration(description, { migrationsDir: this.dir });
8596
+ return createMigration(description, { migrationsDir: this.dir, kind });
8698
8597
  }
8699
8598
  /** Return list of completed (applied) migration filenames. */
8700
8599
  async getApplied() {
@@ -8937,15 +8836,8 @@ function generateCrudRoutes(models, options = {}) {
8937
8836
  const total = Number(countRow[0]?.total ?? 0);
8938
8837
  const limit = qp.limit ?? 100;
8939
8838
  const page = qp.page ?? 1;
8940
- res.json({
8941
- data: rows,
8942
- meta: {
8943
- total,
8944
- page,
8945
- limit,
8946
- totalPages: Math.ceil(total / limit)
8947
- }
8948
- });
8839
+ const offset = (page - 1) * limit;
8840
+ res.json(new DatabaseResult(rows, void 0, total, limit, offset).toPaginate());
8949
8841
  }
8950
8842
  });
8951
8843
  routes.push({
@@ -9099,6 +8991,7 @@ var init_autoCrud = __esm({
9099
8991
  "../orm/src/autoCrud.ts"() {
9100
8992
  "use strict";
9101
8993
  init_database();
8994
+ init_databaseResult();
9102
8995
  init_query();
9103
8996
  init_validation();
9104
8997
  AutoCrud = class _AutoCrud {
@@ -9353,17 +9246,19 @@ var init_queryBuilder = __esm({
9353
9246
  this.ensureDb();
9354
9247
  const sql = this.toSql();
9355
9248
  const allParams = [...this.params, ...this.havingParams];
9249
+ const queryParams = allParams.length > 0 ? allParams : void 0;
9356
9250
  const rows = await adapterFetch(
9357
9251
  this.db,
9358
9252
  sql,
9359
- allParams.length > 0 ? allParams : void 0,
9253
+ queryParams,
9360
9254
  this.limitVal,
9361
9255
  this.offsetVal
9362
9256
  );
9257
+ const total = await probeTotal(this.db, sql, queryParams, this.limitVal);
9363
9258
  return new DatabaseResult(
9364
9259
  rows,
9365
9260
  void 0,
9366
- void 0,
9261
+ total,
9367
9262
  this.limitVal,
9368
9263
  this.offsetVal,
9369
9264
  this.db,
@@ -12940,7 +12835,6 @@ __export(src_exports, {
12940
12835
  DatabaseUrl: () => DatabaseUrl,
12941
12836
  DocStoreDriverMissing: () => DocStoreDriverMissing,
12942
12837
  FakeData: () => FakeData2,
12943
- FetchResult: () => FetchResult,
12944
12838
  FirebirdAdapter: () => FirebirdAdapter,
12945
12839
  InvalidId: () => InvalidId,
12946
12840
  LocalStorage: () => LocalStorage,
@@ -13040,7 +12934,6 @@ __export(src_exports, {
13040
12934
  var init_src = __esm({
13041
12935
  "../orm/src/index.ts"() {
13042
12936
  "use strict";
13043
- init_types();
13044
12937
  init_databaseResult();
13045
12938
  init_database();
13046
12939
  init_database();
@@ -13450,7 +13343,7 @@ ${s}\r
13450
13343
  connect() {
13451
13344
  if (this.connected) return Promise.resolve();
13452
13345
  if (this.connecting) return this.connecting;
13453
- this.connecting = new Promise((resolve21, reject) => {
13346
+ this.connecting = new Promise((resolve20, reject) => {
13454
13347
  const sock = net.createConnection({ host: this.host, port: this.port });
13455
13348
  sock.setNoDelay(true);
13456
13349
  const onError = (err) => {
@@ -13487,7 +13380,7 @@ ${s}\r
13487
13380
  sock.on("error", (e) => {
13488
13381
  this.brokenError = e;
13489
13382
  });
13490
- resolve21();
13383
+ resolve20();
13491
13384
  } catch (e) {
13492
13385
  onError(e);
13493
13386
  }
@@ -13550,12 +13443,12 @@ ${s}\r
13550
13443
  }
13551
13444
  /** Send one command and await its reply (assumes socket is up). */
13552
13445
  raw(args) {
13553
- return new Promise((resolve21, reject) => {
13446
+ return new Promise((resolve20, reject) => {
13554
13447
  if (!this.sock || this.sock.destroyed) {
13555
13448
  reject(this.brokenError ?? new Error("redis socket not connected"));
13556
13449
  return;
13557
13450
  }
13558
- this.waiters.push({ resolve: resolve21, reject });
13451
+ this.waiters.push({ resolve: resolve20, reject });
13559
13452
  this.sock.write(_RespClient.encode(args));
13560
13453
  });
13561
13454
  }
@@ -13897,7 +13790,7 @@ ${s}\r
13897
13790
  connect() {
13898
13791
  if (this.connected) return Promise.resolve();
13899
13792
  if (this.connecting) return this.connecting;
13900
- this.connecting = new Promise((resolve21, reject) => {
13793
+ this.connecting = new Promise((resolve20, reject) => {
13901
13794
  const sock = net.createConnection({ host: this.host, port: this.port });
13902
13795
  sock.setNoDelay(true);
13903
13796
  sock.once("error", (err) => {
@@ -13918,7 +13811,7 @@ ${s}\r
13918
13811
  p.resolve(this.buffer.toString("utf-8"));
13919
13812
  }
13920
13813
  });
13921
- resolve21();
13814
+ resolve20();
13922
13815
  });
13923
13816
  });
13924
13817
  return this.connecting;
@@ -13951,13 +13844,13 @@ ${s}\r
13951
13844
  async send(payload, terminator) {
13952
13845
  await this.connect();
13953
13846
  if (!this.sock || this.sock.destroyed) return "";
13954
- return new Promise((resolve21) => {
13847
+ return new Promise((resolve20) => {
13955
13848
  this.buffer = Buffer.alloc(0);
13956
- this.pending = { terminator, resolve: resolve21 };
13849
+ this.pending = { terminator, resolve: resolve20 };
13957
13850
  const timer = setTimeout(() => {
13958
- if (this.pending && this.pending.resolve === resolve21) {
13851
+ if (this.pending && this.pending.resolve === resolve20) {
13959
13852
  this.pending = null;
13960
- resolve21(this.buffer.toString("utf-8"));
13853
+ resolve20(this.buffer.toString("utf-8"));
13961
13854
  }
13962
13855
  }, 4e3);
13963
13856
  if (timer.unref) timer.unref();
@@ -15483,16 +15376,27 @@ async function parseBody(req2) {
15483
15376
  }
15484
15377
  const contentType = req2.headers["content-type"] ?? "";
15485
15378
  const chunks = [];
15486
- await new Promise((resolve21, reject) => {
15487
- req2.on("data", (chunk) => chunks.push(chunk));
15488
- req2.on("end", resolve21);
15379
+ await new Promise((resolve20, reject) => {
15380
+ let received = 0;
15381
+ let refused = false;
15382
+ req2.on("data", (chunk) => {
15383
+ if (refused) return;
15384
+ received += chunk.length;
15385
+ if (received > TINA4_MAX_UPLOAD_SIZE) {
15386
+ refused = true;
15387
+ chunks.length = 0;
15388
+ reject(new PayloadTooLargeError(received, TINA4_MAX_UPLOAD_SIZE));
15389
+ return;
15390
+ }
15391
+ chunks.push(chunk);
15392
+ });
15393
+ req2.on("end", () => {
15394
+ if (!refused) resolve20();
15395
+ });
15489
15396
  req2.on("error", reject);
15490
15397
  });
15491
15398
  const raw = Buffer.concat(chunks);
15492
15399
  if (raw.length === 0) return;
15493
- if (raw.length > TINA4_MAX_UPLOAD_SIZE) {
15494
- throw new PayloadTooLargeError(raw.length, TINA4_MAX_UPLOAD_SIZE);
15495
- }
15496
15400
  if (contentType.includes("multipart/form-data")) {
15497
15401
  const boundary = extractBoundary(contentType);
15498
15402
  if (boundary) {
@@ -17441,10 +17345,17 @@ var init_session = __esm({
17441
17345
  *
17442
17346
  * session.flash("message", "Saved!") // set
17443
17347
  * session.flash("message") // get + auto-remove → "Saved!"
17348
+ * session.flash("message", null) // get + auto-remove (null is a GET sentinel)
17349
+ *
17350
+ * `null` — NOT just `undefined` — is the GET sentinel, so `flash(key, null)`
17351
+ * READS and clears rather than STORING null. This matches the Python master
17352
+ * (`if value is not None`), PHP (`if ($value !== null)`) and Ruby
17353
+ * (`if value.nil?`): passing the language's "no value" literal means GET. A
17354
+ * caller wanting to persist an explicit null should store it with `set()`.
17444
17355
  */
17445
17356
  flash(key, value) {
17446
17357
  const flashKey = `${FLASH_PREFIX}${key}`;
17447
- if (value !== void 0) {
17358
+ if (value !== void 0 && value !== null) {
17448
17359
  this.set(flashKey, value);
17449
17360
  return void 0;
17450
17361
  }
@@ -23315,14 +23226,14 @@ data: ${channel.buffer.shift()}
23315
23226
  `;
23316
23227
  continue;
23317
23228
  }
23318
- const gotMessage = await new Promise((resolve21) => {
23229
+ const gotMessage = await new Promise((resolve20) => {
23319
23230
  const timer = setTimeout(() => {
23320
23231
  channel.wake = null;
23321
- resolve21(false);
23232
+ resolve20(false);
23322
23233
  }, keepaliveMs);
23323
23234
  channel.wake = () => {
23324
23235
  clearTimeout(timer);
23325
- resolve21(true);
23236
+ resolve20(true);
23326
23237
  };
23327
23238
  });
23328
23239
  if (!gotMessage) yield `: keep-alive
@@ -25837,7 +25748,7 @@ var init_websocket = __esm({
25837
25748
  * Start the WebSocket server.
25838
25749
  */
25839
25750
  async start() {
25840
- return new Promise((resolve21, reject) => {
25751
+ return new Promise((resolve20, reject) => {
25841
25752
  this.server = createServer((req2, res) => {
25842
25753
  res.writeHead(426, { "Content-Type": "text/plain" });
25843
25754
  res.end("Upgrade Required");
@@ -25847,7 +25758,7 @@ var init_websocket = __esm({
25847
25758
  });
25848
25759
  this.server.listen(this.port, () => {
25849
25760
  this.startIdleReaper();
25850
- resolve21();
25761
+ resolve20();
25851
25762
  });
25852
25763
  this.server.on("error", (err) => {
25853
25764
  this.emit("error", err);
@@ -26411,7 +26322,7 @@ var init_websocket = __esm({
26411
26322
  client.trackerId = this.onAdd(socket.remoteAddress ?? "unknown", "/__dev_reload");
26412
26323
  }
26413
26324
  this.clients.add(client);
26414
- const cleanup2 = () => {
26325
+ const cleanup = () => {
26415
26326
  if (!this.clients.has(client)) return;
26416
26327
  this.clients.delete(client);
26417
26328
  if (client.trackerId && this.onRemove) this.onRemove(client.trackerId);
@@ -26434,13 +26345,13 @@ var init_websocket = __esm({
26434
26345
  socket.end();
26435
26346
  } catch {
26436
26347
  }
26437
- cleanup2();
26348
+ cleanup();
26438
26349
  return;
26439
26350
  }
26440
26351
  }
26441
26352
  });
26442
- socket.on("close", cleanup2);
26443
- socket.on("error", cleanup2);
26353
+ socket.on("close", cleanup);
26354
+ socket.on("error", cleanup);
26444
26355
  return true;
26445
26356
  }
26446
26357
  /**
@@ -27958,7 +27869,7 @@ var init_queue = __esm({
27958
27869
  const jobs = this.popBatch(resolvedBatchSize);
27959
27870
  if (jobs.length === 0) {
27960
27871
  if (resolvedPollInterval <= 0) break;
27961
- await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
27872
+ await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
27962
27873
  continue;
27963
27874
  }
27964
27875
  yield jobs;
@@ -27968,7 +27879,7 @@ var init_queue = __esm({
27968
27879
  const raw = this.pop();
27969
27880
  if (raw === null) {
27970
27881
  if (resolvedPollInterval <= 0) break;
27971
- await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
27882
+ await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
27972
27883
  continue;
27973
27884
  }
27974
27885
  yield createJob(raw, this);
@@ -32488,23 +32399,23 @@ var init_devAdmin = __esm({
32488
32399
  });
32489
32400
  };
32490
32401
  handleDevAdminJs = async (_req, res) => {
32491
- const { readFileSync: readFileSync27, existsSync: existsSync27 } = await import("node:fs");
32492
- const { dirname: dirname14, join: join30, resolve: resolve21 } = await import("node:path");
32402
+ const { readFileSync: readFileSync26, existsSync: existsSync26 } = await import("node:fs");
32403
+ const { dirname: dirname13, join: join29, resolve: resolve20 } = await import("node:path");
32493
32404
  const { fileURLToPath: fileURLToPath7 } = await import("node:url");
32494
- const dir = dirname14(fileURLToPath7(import.meta.url));
32405
+ const dir = dirname13(fileURLToPath7(import.meta.url));
32495
32406
  const candidates = [
32496
- join30(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
32407
+ join29(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
32497
32408
  // src/../public/js/
32498
- join30(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
32409
+ join29(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
32499
32410
  // deeper nesting
32500
- resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
32501
- resolve21(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
32411
+ resolve20(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
32412
+ resolve20(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
32502
32413
  // project public/
32503
32414
  ];
32504
32415
  for (const jsPath of candidates) {
32505
- if (existsSync27(jsPath)) {
32416
+ if (existsSync26(jsPath)) {
32506
32417
  try {
32507
- const content = readFileSync27(jsPath, "utf-8");
32418
+ const content = readFileSync26(jsPath, "utf-8");
32508
32419
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
32509
32420
  res.raw.end(content);
32510
32421
  return;
@@ -32965,8 +32876,12 @@ function sanitizeSecurity(reqs, schemes) {
32965
32876
  function generate(routes, models = []) {
32966
32877
  const info = {
32967
32878
  title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
32968
- version: process.env.TINA4_SWAGGER_VERSION ?? "0.0.1",
32969
- description: process.env.TINA4_SWAGGER_DESCRIPTION ?? "Auto-generated API documentation"
32879
+ // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
32880
+ // 0.0.1). description defaults to the empty string, not a canned sentence.
32881
+ // Both are the settled cross-framework defaults (parity with the Python
32882
+ // master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
32883
+ version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
32884
+ description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
32970
32885
  };
32971
32886
  const contactEmail = (process.env.TINA4_SWAGGER_CONTACT_EMAIL ?? "").trim();
32972
32887
  const contactName = (process.env.TINA4_SWAGGER_CONTACT_TEAM ?? "").trim();
@@ -32999,9 +32914,11 @@ function generate(routes, models = []) {
32999
32914
  const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
33000
32915
  const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
33001
32916
  const refSchemas = /* @__PURE__ */ new Set();
32917
+ const tableToSchema = /* @__PURE__ */ new Map();
33002
32918
  for (const model of models) {
33003
- const schema = modelToSchema(model);
33004
- spec.components.schemas[model.tableName] = schema;
32919
+ const schemaKey = schemaNameForModel(model);
32920
+ tableToSchema.set(model.tableName, schemaKey);
32921
+ spec.components.schemas[schemaKey] = modelToSchema(model);
33005
32922
  }
33006
32923
  const usedTags = [];
33007
32924
  const seenIds = /* @__PURE__ */ new Set();
@@ -33028,11 +32945,11 @@ function generate(routes, models = []) {
33028
32945
  if (route.meta?.deprecated) operation.deprecated = true;
33029
32946
  const pathParams = extractPathParams(route.pattern);
33030
32947
  if (pathParams.length > 0) {
33031
- operation.parameters = pathParams.map((name) => ({
32948
+ operation.parameters = pathParams.map(({ name, schema }) => ({
33032
32949
  name,
33033
32950
  in: "path",
33034
32951
  required: true,
33035
- schema: { type: "string" }
32952
+ schema
33036
32953
  }));
33037
32954
  }
33038
32955
  if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
@@ -33058,19 +32975,20 @@ function generate(routes, models = []) {
33058
32975
  };
33059
32976
  } else if (method === "post" || method === "put") {
33060
32977
  const modelName = inferModelFromPath(route.pattern);
33061
- if (modelName && models.some((m) => m.tableName === modelName)) {
33062
- const media = {
33063
- schema: { $ref: `#/components/schemas/${modelName}` }
33064
- };
32978
+ const schemaKey = modelName ? tableToSchema.get(modelName) : void 0;
32979
+ if (schemaKey) {
32980
+ const sref = `#/components/schemas/${schemaKey}`;
32981
+ const media = { schema: { $ref: sref } };
33065
32982
  if (route.meta?.example !== void 0) media.example = route.meta.example;
33066
32983
  operation.requestBody = {
33067
32984
  required: true,
33068
32985
  content: { "application/json": media }
33069
32986
  };
33070
- operation.responses = {
33071
- ...method === "post" ? { "201": { description: "Created", content: { "application/json": { schema: { $ref: `#/components/schemas/${modelName}` } } } } } : { "200": { description: "Updated", content: { "application/json": { schema: { $ref: `#/components/schemas/${modelName}` } } } } },
33072
- "422": { description: "Validation failed" }
33073
- };
32987
+ if (route.meta?.responses === void 0) {
32988
+ operation.responses = {
32989
+ "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
32990
+ };
32991
+ }
33074
32992
  } else if (route.meta?.example !== void 0) {
33075
32993
  operation.requestBody = {
33076
32994
  content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
@@ -33157,6 +33075,21 @@ function resolveServers() {
33157
33075
  const dev = (process.env.SWAGGER_DEV_URL ?? "").trim();
33158
33076
  return dev.length > 0 ? [{ url: dev }] : [{ url: "/" }];
33159
33077
  }
33078
+ function schemaNameForModel(model) {
33079
+ const explicit = model.className?.trim();
33080
+ if (explicit) return explicit;
33081
+ return deriveClassName(model.tableName);
33082
+ }
33083
+ function deriveClassName(tableName) {
33084
+ return singularize(tableName).split(/[_\s-]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || tableName;
33085
+ }
33086
+ function singularize(word) {
33087
+ if (/ies$/i.test(word) && word.length > 3) return word.slice(0, -3) + "y";
33088
+ if (/(ses|xes|zes|ches|shes)$/i.test(word)) return word.slice(0, -2);
33089
+ if (/ss$/i.test(word)) return word;
33090
+ if (/s$/i.test(word) && word.length > 1) return word.slice(0, -1);
33091
+ return word;
33092
+ }
33160
33093
  function modelToSchema(model) {
33161
33094
  const properties = {};
33162
33095
  const required = [];
@@ -33230,15 +33163,35 @@ function inferSchema(value) {
33230
33163
  if (typeof value === "number") return { type: Number.isInteger(value) ? "integer" : "number" };
33231
33164
  return { type: "string" };
33232
33165
  }
33166
+ function segmentParam(segment) {
33167
+ if (segment.startsWith("{") && segment.endsWith("}")) {
33168
+ const inner = segment.slice(1, -1);
33169
+ if (inner.startsWith("...")) return { name: inner.slice(3), type: "string" };
33170
+ const colon = inner.indexOf(":");
33171
+ if (colon >= 0) return { name: inner.slice(0, colon), type: inner.slice(colon + 1) };
33172
+ return { name: inner, type: "string" };
33173
+ }
33174
+ if (segment.startsWith("[") && segment.endsWith("]")) {
33175
+ const inner = segment.slice(1, -1);
33176
+ return { name: inner.startsWith("...") ? inner.slice(3) : inner, type: "string" };
33177
+ }
33178
+ if (segment.startsWith(":") && segment.length > 1) {
33179
+ return { name: segment.slice(1), type: "string" };
33180
+ }
33181
+ return null;
33182
+ }
33233
33183
  function patternToOpenAPI(pattern) {
33234
- return pattern.replace(/\[\.\.\.(\w+)\]/g, "{$1}").replace(/\[(\w+)\]/g, "{$1}");
33184
+ return pattern.split("/").map((segment) => {
33185
+ const p = segmentParam(segment);
33186
+ return p ? `{${p.name}}` : segment;
33187
+ }).join("/");
33235
33188
  }
33236
33189
  function extractPathParams(pattern) {
33237
33190
  const params = [];
33238
- const regex = /\[(?:\.\.\.)?(\w+)\]/g;
33239
- let match;
33240
- while ((match = regex.exec(pattern)) !== null) {
33241
- params.push(match[1]);
33191
+ for (const segment of pattern.split("/")) {
33192
+ const p = segmentParam(segment);
33193
+ if (!p) continue;
33194
+ params.push({ name: p.name, schema: { ...PARAM_TYPE_SCHEMA[p.type] ?? { type: "string" } } });
33242
33195
  }
33243
33196
  return params;
33244
33197
  }
@@ -33260,8 +33213,12 @@ function inferModelFromPath(pattern) {
33260
33213
  if (rest.length === 1 && /^[[{]\.{0,3}\w+[\]}]$/.test(rest[0])) return candidate;
33261
33214
  return null;
33262
33215
  }
33216
+ function operationIdBase(method, openApiPath) {
33217
+ const clean = openApiPath.replace(/^\/+|\/+$/g, "").replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
33218
+ return clean ? `${method}_${clean}` : method;
33219
+ }
33263
33220
  function uniqueOperationId(method, openApiPath, seen) {
33264
- const base = (method + openApiPath.replace(/[/{}]/g, "_")).replace(/_+/g, "_").replace(/_$/, "");
33221
+ const base = operationIdBase(method, openApiPath);
33265
33222
  let oid = base;
33266
33223
  let n = 2;
33267
33224
  while (seen.has(oid)) {
@@ -33271,13 +33228,25 @@ function uniqueOperationId(method, openApiPath, seen) {
33271
33228
  seen.add(oid);
33272
33229
  return oid;
33273
33230
  }
33274
- var WRITE_METHODS, registeredSchemes, registeredSchemas;
33231
+ var WRITE_METHODS, registeredSchemes, registeredSchemas, PARAM_TYPE_SCHEMA;
33275
33232
  var init_generator = __esm({
33276
33233
  "../swagger/src/generator.ts"() {
33277
33234
  "use strict";
33278
33235
  WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
33279
33236
  registeredSchemes = {};
33280
33237
  registeredSchemas = {};
33238
+ PARAM_TYPE_SCHEMA = {
33239
+ int: { type: "integer" },
33240
+ integer: { type: "integer" },
33241
+ float: { type: "number" },
33242
+ number: { type: "number" },
33243
+ uuid: { type: "string", format: "uuid" },
33244
+ slug: { type: "string", pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
33245
+ alpha: { type: "string", pattern: "^[A-Za-z]+$" },
33246
+ alnum: { type: "string", pattern: "^[A-Za-z0-9]+$" },
33247
+ path: { type: "string" },
33248
+ string: { type: "string" }
33249
+ };
33281
33250
  }
33282
33251
  });
33283
33252
 
@@ -33597,10 +33566,29 @@ function openBrowser(url) {
33597
33566
  }, 2e3);
33598
33567
  }
33599
33568
  function resolvePortAndHost(config) {
33600
- const port = config?.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? 7148;
33569
+ const tina4Port = process.env.TINA4_PORT;
33570
+ const legacyPort = process.env.PORT;
33571
+ let port;
33572
+ if (config?.port !== void 0) {
33573
+ port = config.port;
33574
+ } else if (tina4Port && /^\d+$/.test(tina4Port)) {
33575
+ port = parseInt(tina4Port, 10);
33576
+ } else if (legacyPort && /^\d+$/.test(legacyPort)) {
33577
+ port = parseInt(legacyPort, 10);
33578
+ warnDeprecatedPort(port);
33579
+ } else {
33580
+ port = 7148;
33581
+ }
33601
33582
  const host = config?.host ?? process.env.TINA4_HOST ?? process.env.HOST ?? "0.0.0.0";
33602
33583
  return { port, host };
33603
33584
  }
33585
+ function warnDeprecatedPort(port) {
33586
+ if (portDeprecationWarned) return;
33587
+ portDeprecationWarned = true;
33588
+ Log.warning(
33589
+ `PORT is deprecated and will be removed in 3.14 - use TINA4_PORT instead (binding port ${port} from PORT)`
33590
+ );
33591
+ }
33604
33592
  function isBannerSuppressed() {
33605
33593
  return isTruthy(process.env.TINA4_SUPPRESS);
33606
33594
  }
@@ -33872,6 +33860,29 @@ function deployGallery(name) {
33872
33860
  </body>
33873
33861
  </html>`;
33874
33862
  }
33863
+ function startLoopWatchdog() {
33864
+ const raw = (process.env.TINA4_LOOP_LAG_WARN_MS ?? "").trim();
33865
+ const threshold = /^\d+$/.test(raw) ? parseInt(raw, 10) : 250;
33866
+ if (threshold <= 0) {
33867
+ return { stop: () => {
33868
+ } };
33869
+ }
33870
+ let last = Date.now();
33871
+ let warned = 0;
33872
+ const timer = setInterval(() => {
33873
+ const now = Date.now();
33874
+ const lag = now - last - LOOP_WATCHDOG_TICK_MS;
33875
+ last = now;
33876
+ if (lag < threshold) return;
33877
+ warned++;
33878
+ if (warned > 5 && warned % 20 !== 0) return;
33879
+ Log.warning(
33880
+ `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.`
33881
+ );
33882
+ }, LOOP_WATCHDOG_TICK_MS);
33883
+ timer.unref();
33884
+ return { stop: () => clearInterval(timer) };
33885
+ }
33875
33886
  async function start(config) {
33876
33887
  const isManaged = process.argv.includes("--managed");
33877
33888
  if (!isManaged && process.env.TINA4_OVERRIDE_CLIENT !== "true") {
@@ -34121,7 +34132,9 @@ async function startServer(config) {
34121
34132
  const resolved = resolvePortAndHost(config);
34122
34133
  const host = resolved.host;
34123
34134
  let port = resolved.port;
34124
- port = findAvailablePort(port);
34135
+ if (!cluster.isWorker) {
34136
+ port = findAvailablePort(port);
34137
+ }
34125
34138
  const isProduction = (process.env.TINA4_PRODUCTION ?? "").toLowerCase() === "true";
34126
34139
  if (cluster.isPrimary && isProduction) {
34127
34140
  const numCPUs = os2.cpus().length;
@@ -34337,7 +34350,20 @@ ${reset2}
34337
34350
  await sessionAutoStart(rawReq, rawRes, req2);
34338
34351
  await middleware.run(req2, res);
34339
34352
  if (res.raw.writableEnded) return;
34340
- await req2.parseBody();
34353
+ try {
34354
+ await req2.parseBody();
34355
+ } catch (err) {
34356
+ const status2 = err?.statusCode;
34357
+ if (typeof status2 === "number" && status2 >= 400 && status2 < 500) {
34358
+ if (!rawRes.writableEnded) {
34359
+ rawRes.statusCode = status2;
34360
+ rawRes.setHeader("content-type", "application/json");
34361
+ rawRes.end(JSON.stringify({ error: err.message }));
34362
+ }
34363
+ return;
34364
+ }
34365
+ throw err;
34366
+ }
34341
34367
  const pathname = req2.path;
34342
34368
  const reqStartTime = DevAdmin.isEnabled() ? Date.now() : 0;
34343
34369
  const matchedPattern = { value: "" };
@@ -34536,8 +34562,10 @@ ${reset2}
34536
34562
  };
34537
34563
  process.on("SIGTERM", onSigterm);
34538
34564
  process.on("SIGINT", onSigint);
34565
+ const loopWatchdog = startLoopWatchdog();
34539
34566
  resolvePromise({
34540
34567
  close: () => {
34568
+ loopWatchdog.stop();
34541
34569
  process.off("SIGTERM", onSigterm);
34542
34570
  process.off("SIGINT", onSigint);
34543
34571
  stopAllBackgroundTasks();
@@ -34552,7 +34580,7 @@ ${reset2}
34552
34580
  });
34553
34581
  });
34554
34582
  }
34555
- 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;
34583
+ 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;
34556
34584
  var init_server = __esm({
34557
34585
  "src/server.ts"() {
34558
34586
  "use strict";
@@ -34605,6 +34633,7 @@ var init_server = __esm({
34605
34633
  SWAGGER_VERSION: "TINA4_SWAGGER_VERSION",
34606
34634
  ORM_PLURAL_TABLE_NAMES: "TINA4_ORM_PLURAL_TABLE_NAMES"
34607
34635
  };
34636
+ portDeprecationWarned = false;
34608
34637
  TEMPLATE_PAGES_DIR = "pages";
34609
34638
  HTTP_REASON_PHRASES = {
34610
34639
  100: "Continue",
@@ -34641,6 +34670,7 @@ var init_server = __esm({
34641
34670
  templateCache = null;
34642
34671
  _dispatchFn = null;
34643
34672
  _serverHandle = null;
34673
+ LOOP_WATCHDOG_TICK_MS = 100;
34644
34674
  FALLBACK_STAGES = [
34645
34675
  serveTemplateFallback,
34646
34676
  serveLandingPage,
@@ -34728,433 +34758,6 @@ var init_env = __esm({
34728
34758
  }
34729
34759
  });
34730
34760
 
34731
- // src/scss.ts
34732
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync16, existsSync as existsSync24, mkdirSync as mkdirSync18, readdirSync as readdirSync18 } from "node:fs";
34733
- import { join as join27, resolve as resolve19, dirname as dirname12 } from "node:path";
34734
- function compileString(scss, importPaths, variables) {
34735
- const imported = /* @__PURE__ */ new Set();
34736
- scss = resolveImports(scss, importPaths, imported);
34737
- scss = scss.replace(/(?<![:"'])\/\/[^\n]*/g, "");
34738
- scss = extractVariables(scss, variables);
34739
- const mixins = {};
34740
- scss = extractMixins(scss, mixins);
34741
- scss = resolveIncludes(scss, mixins);
34742
- scss = resolveInterpolation(scss, variables);
34743
- scss = substituteVariables(scss, variables);
34744
- scss = evalMath(scss);
34745
- scss = resolveColorFunctions(scss);
34746
- const css = flattenNesting(scss);
34747
- return cleanup(css);
34748
- }
34749
- function resolveImports(content, paths, imported) {
34750
- return content.replace(/@import\s+["']?([^"';\n]+)["']?\s*;/g, (_match, name) => {
34751
- name = name.trim();
34752
- const candidates = [];
34753
- for (const base of paths) {
34754
- candidates.push(
34755
- join27(base, `${name}.scss`),
34756
- join27(base, `_${name}.scss`),
34757
- join27(base, name)
34758
- );
34759
- }
34760
- for (const candidate of candidates) {
34761
- if (existsSync24(candidate) && !imported.has(candidate)) {
34762
- imported.add(candidate);
34763
- const fileContent = readFileSync23(candidate, "utf-8");
34764
- return resolveImports(fileContent, [dirname12(candidate), ...paths], imported);
34765
- }
34766
- }
34767
- return `/* IMPORT NOT FOUND: ${name} */`;
34768
- });
34769
- }
34770
- function stripVariableFlags(value) {
34771
- let declaresDefault = false;
34772
- for (; ; ) {
34773
- const match = VARIABLE_FLAG.exec(value);
34774
- if (match === null) return [value.trim(), declaresDefault];
34775
- if (match[1] === "default") declaresDefault = true;
34776
- value = value.slice(0, match.index);
34777
- }
34778
- }
34779
- function extractVariables(scss, variables) {
34780
- return scss.replace(/\$([a-zA-Z_][\w-]*)\s*:\s*([^;]+);/g, (_m, name, value) => {
34781
- const [stripped, declaresDefault] = stripVariableFlags(value.trim());
34782
- if (declaresDefault && (variables[name] ?? "null") !== "null") {
34783
- return "";
34784
- }
34785
- let resolved = stripped;
34786
- for (const [vName, vVal] of Object.entries(variables)) {
34787
- resolved = resolved.replaceAll(`$${vName}`, vVal);
34788
- }
34789
- variables[name] = resolved;
34790
- return "";
34791
- });
34792
- }
34793
- function substituteVariables(scss, variables) {
34794
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
34795
- for (const name of sorted) {
34796
- scss = scss.replaceAll(`$${name}`, variables[name]);
34797
- }
34798
- return scss;
34799
- }
34800
- function resolveInterpolation(scss, variables) {
34801
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
34802
- return scss.replace(/#\{([^{}]*)\}/g, (_m, inner) => {
34803
- let resolved = inner.trim();
34804
- for (const name of sorted) {
34805
- resolved = resolved.replaceAll(`$${name}`, variables[name]);
34806
- }
34807
- return resolved;
34808
- });
34809
- }
34810
- function extractMixins(scss, mixins) {
34811
- const pattern = /@mixin\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*\{/g;
34812
- let match;
34813
- const locations = [];
34814
- while ((match = pattern.exec(scss)) !== null) {
34815
- const name = match[1];
34816
- const paramsStr = match[2] ?? "";
34817
- const params = paramsStr.split(",").map((p) => p.trim().replace(/^\$/, "")).filter(Boolean);
34818
- const bodyStart = match.index + match[0].length;
34819
- const body = findBlock(scss, bodyStart);
34820
- if (body !== null) {
34821
- mixins[name] = { params, body };
34822
- locations.push({
34823
- start: match.index,
34824
- end: bodyStart + body.length + 1,
34825
- name
34826
- });
34827
- }
34828
- }
34829
- let result = scss;
34830
- for (const loc of locations.reverse()) {
34831
- result = result.slice(0, loc.start) + result.slice(loc.end);
34832
- }
34833
- return result;
34834
- }
34835
- function resolveIncludes(scss, mixins) {
34836
- return scss.replace(
34837
- /@include\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*;/g,
34838
- (_m, name, argsStr) => {
34839
- if (!(name in mixins)) {
34840
- return `/* MIXIN NOT FOUND: ${name} */`;
34841
- }
34842
- const mixin = mixins[name];
34843
- const args = argsStr ? argsStr.split(",").map((a) => a.trim()).filter(Boolean) : [];
34844
- let body = mixin.body;
34845
- for (let i = 0; i < mixin.params.length; i++) {
34846
- const paramName = mixin.params[i].split(":")[0].trim();
34847
- const defaultVal = mixin.params[i].includes(":") ? mixin.params[i].split(":").slice(1).join(":").trim() : "";
34848
- const value = i < args.length ? args[i] : defaultVal;
34849
- body = body.replaceAll(`$${paramName}`, value);
34850
- }
34851
- return body;
34852
- }
34853
- );
34854
- }
34855
- function evalMath(scss) {
34856
- const placeholders = [];
34857
- const masked = scss.replace(/calc\([^()]*\)/g, (m) => {
34858
- placeholders.push(m);
34859
- return `\0CALC${placeholders.length - 1}\0`;
34860
- });
34861
- const folded = masked.replace(
34862
- /([\d.]+)([a-z%]*)\s*([+\-*/])\s*([\d.]+)([a-z%]*)/g,
34863
- (full, n1, u1, op, n2, u2) => {
34864
- const num1 = parseFloat(n1);
34865
- const num2 = parseFloat(n2);
34866
- if (Number.isNaN(num1) || Number.isNaN(num2)) return full;
34867
- const unit1 = u1 || "";
34868
- const unit2 = u2 || "";
34869
- let unit;
34870
- if (unit1 === unit2) {
34871
- unit = unit1;
34872
- } else if ((op === "*" || op === "/") && unit1 === "") {
34873
- unit = unit2;
34874
- } else if ((op === "*" || op === "/") && unit2 === "") {
34875
- unit = unit1;
34876
- } else {
34877
- return full;
34878
- }
34879
- let result;
34880
- switch (op) {
34881
- case "+":
34882
- result = num1 + num2;
34883
- break;
34884
- case "-":
34885
- result = num1 - num2;
34886
- break;
34887
- case "*":
34888
- result = num1 * num2;
34889
- break;
34890
- case "/":
34891
- if (num2 === 0) return full;
34892
- result = num1 / num2;
34893
- break;
34894
- default:
34895
- return full;
34896
- }
34897
- if (result === Math.floor(result)) {
34898
- return `${Math.floor(result)}${unit}`;
34899
- }
34900
- return `${result.toFixed(2)}${unit}`;
34901
- }
34902
- );
34903
- return folded.replace(/\x00CALC(\d+)\x00/g, (_m, idx) => {
34904
- return placeholders[parseInt(idx, 10)];
34905
- });
34906
- }
34907
- function resolveColorFunctions(scss) {
34908
- scss = scss.replace(
34909
- /lighten\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g,
34910
- (_m, color, amt) => adjustLightness(color.trim(), parseFloat(amt.trim().replace(/%$/, "")) / 100)
34911
- );
34912
- scss = scss.replace(
34913
- /darken\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g,
34914
- (_m, color, amt) => adjustLightness(color.trim(), -(parseFloat(amt.trim().replace(/%$/, "")) / 100))
34915
- );
34916
- scss = scss.replace(/rgba\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*([\d.]+)\s*\)/g, (whole, hex, alpha) => {
34917
- const rgb = hexToRgb(hex);
34918
- return rgb === null ? whole : `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha.trim()})`;
34919
- });
34920
- scss = scss.replace(/rgb\(\s*(#[0-9a-fA-F]{3,8})\s*\)/g, (whole, hex) => {
34921
- const rgb = hexToRgb(hex);
34922
- return rgb === null ? whole : `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
34923
- });
34924
- scss = scss.replace(
34925
- /mix\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*(#[0-9a-fA-F]{3,8})\s*(?:,\s*([\d.]+%?)\s*)?\)/g,
34926
- (whole, h1, h2, weight) => {
34927
- const c1 = hexToRgb(h1);
34928
- const c2 = hexToRgb(h2);
34929
- if (c1 === null || c2 === null) return whole;
34930
- const w = weight ? parseFloat(weight.replace(/%$/, "")) / 100 : 0.5;
34931
- const mixed = [0, 1, 2].map((i) => Math.round(c1[i] * w + c2[i] * (1 - w)));
34932
- return `#${mixed.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
34933
- }
34934
- );
34935
- return scss;
34936
- }
34937
- function hexToRgb(color) {
34938
- let c = color.trim().replace(/^#/, "");
34939
- if (c.length === 3) {
34940
- c = c.split("").map((ch) => ch + ch).join("");
34941
- }
34942
- if (!/^[0-9a-fA-F]{6}$/.test(c)) return null;
34943
- return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
34944
- }
34945
- function adjustLightness(color, amount) {
34946
- const rgb = hexToRgb(color);
34947
- if (rgb === null) return color;
34948
- let [r, g, b] = rgb.map((v) => v / 255);
34949
- const max = Math.max(r, g, b);
34950
- const min = Math.min(r, g, b);
34951
- let l = (max + min) / 2;
34952
- const d = max - min;
34953
- let h = 0;
34954
- let s = 0;
34955
- if (d !== 0) {
34956
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
34957
- if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
34958
- else if (max === g) h = (b - r) / d + 2;
34959
- else h = (r - g) / d + 4;
34960
- h /= 6;
34961
- }
34962
- l = Math.max(0, Math.min(1, l + amount));
34963
- if (s === 0) {
34964
- r = g = b = l;
34965
- } else {
34966
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
34967
- const p = 2 * l - q;
34968
- r = hueToRgb(p, q, h + 1 / 3);
34969
- g = hueToRgb(p, q, h);
34970
- b = hueToRgb(p, q, h - 1 / 3);
34971
- }
34972
- const hex = (v) => Math.trunc(v * 255).toString(16).padStart(2, "0");
34973
- return `#${hex(r)}${hex(g)}${hex(b)}`;
34974
- }
34975
- function hueToRgb(p, q, t) {
34976
- if (t < 0) t += 1;
34977
- if (t > 1) t -= 1;
34978
- if (t < 1 / 6) return p + (q - p) * 6 * t;
34979
- if (t < 1 / 2) return q;
34980
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
34981
- return p;
34982
- }
34983
- function flattenNesting(scss) {
34984
- const output = [];
34985
- flattenBlock(scss, [], output);
34986
- return output.join("\n");
34987
- }
34988
- function flattenBlock(content, parentSelectors, output) {
34989
- let pos = 0;
34990
- const properties = [];
34991
- while (pos < content.length) {
34992
- while (pos < content.length && /[\s]/.test(content[pos])) {
34993
- pos++;
34994
- }
34995
- if (pos >= content.length) break;
34996
- if (content[pos] === "/" && content[pos + 1] === "*") {
34997
- const end = content.indexOf("*/", pos + 2);
34998
- if (end === -1) break;
34999
- output.push(content.slice(pos, end + 2));
35000
- pos = end + 2;
35001
- continue;
35002
- }
35003
- if (content.slice(pos, pos + 6) === "@media") {
35004
- const brace = content.indexOf("{", pos);
35005
- if (brace === -1) break;
35006
- const mediaQuery = content.slice(pos, brace).trim();
35007
- const body = findBlock(content, brace + 1);
35008
- if (body === null) break;
35009
- pos = brace + 1 + body.length + 1;
35010
- const innerOutput = [];
35011
- flattenBlock(body, parentSelectors, innerOutput);
35012
- if (innerOutput.length > 0) {
35013
- output.push(`${mediaQuery} {`);
35014
- for (const line of innerOutput) {
35015
- output.push(` ${line}`);
35016
- }
35017
- output.push("}");
35018
- }
35019
- continue;
35020
- }
35021
- const bracePos = content.indexOf("{", pos);
35022
- const semiPos = content.indexOf(";", pos);
35023
- if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
35024
- const prop = content.slice(pos, semiPos).trim();
35025
- if (prop && !prop.startsWith("@")) {
35026
- properties.push(prop);
35027
- }
35028
- pos = semiPos + 1;
35029
- continue;
35030
- }
35031
- if (bracePos !== -1) {
35032
- const selectorText = content.slice(pos, bracePos).trim();
35033
- const body = findBlock(content, bracePos + 1);
35034
- if (body === null) break;
35035
- pos = bracePos + 1 + body.length + 1;
35036
- if (!selectorText) continue;
35037
- const selectors = selectorText.split(",").map((s) => s.trim());
35038
- const newSelectors = [];
35039
- for (const sel of selectors) {
35040
- if (parentSelectors.length > 0) {
35041
- for (const parent of parentSelectors) {
35042
- if (sel.includes("&")) {
35043
- newSelectors.push(sel.replace(/&/g, parent));
35044
- } else {
35045
- newSelectors.push(`${parent} ${sel}`);
35046
- }
35047
- }
35048
- } else {
35049
- newSelectors.push(sel);
35050
- }
35051
- }
35052
- flattenBlock(body, newSelectors, output);
35053
- continue;
35054
- }
35055
- const remaining = content.slice(pos).trim();
35056
- if (remaining) {
35057
- properties.push(remaining);
35058
- }
35059
- break;
35060
- }
35061
- if (properties.length > 0 && parentSelectors.length > 0) {
35062
- const selectorStr = parentSelectors.join(", ");
35063
- output.push(`${selectorStr} {`);
35064
- for (const prop of properties) {
35065
- output.push(` ${prop};`);
35066
- }
35067
- output.push("}");
35068
- }
35069
- }
35070
- function findBlock(content, start2) {
35071
- let depth = 1;
35072
- let pos = start2;
35073
- while (pos < content.length && depth > 0) {
35074
- if (content[pos] === "{") depth++;
35075
- else if (content[pos] === "}") depth--;
35076
- if (depth > 0) pos++;
35077
- }
35078
- return depth === 0 ? content.slice(start2, pos) : null;
35079
- }
35080
- function cleanup(css) {
35081
- css = css.replace(/[^{}]+\{\s*\}/g, "");
35082
- css = css.replace(/\n{3,}/g, "\n\n");
35083
- css = css.split("\n").map((line) => line.trimEnd()).join("\n");
35084
- return css.trim() + "\n";
35085
- }
35086
- var ScssCompiler, VARIABLE_FLAG;
35087
- var init_scss = __esm({
35088
- "src/scss.ts"() {
35089
- "use strict";
35090
- ScssCompiler = class {
35091
- _importPaths;
35092
- _variables;
35093
- constructor(config) {
35094
- this._importPaths = config?.importPaths ? [...config.importPaths] : [];
35095
- this._variables = config?.variables ? { ...config.variables } : {};
35096
- }
35097
- /** Compile an SCSS string to CSS. */
35098
- compile(source) {
35099
- return compileString(source, this._importPaths, { ...this._variables });
35100
- }
35101
- /** Compile an SCSS file to CSS. */
35102
- compileFile(filePath) {
35103
- const absPath = resolve19(filePath);
35104
- const content = readFileSync23(absPath, "utf-8");
35105
- const paths = [dirname12(absPath), ...this._importPaths];
35106
- return compileString(content, paths, { ...this._variables });
35107
- }
35108
- /** Add a directory to the import resolution path. */
35109
- addImportPath(path8) {
35110
- this._importPaths.push(resolve19(path8));
35111
- }
35112
- /** Set or override an SCSS variable. */
35113
- setVariable(name, value) {
35114
- const key = name.startsWith("$") ? name.slice(1) : name;
35115
- this._variables[key] = value;
35116
- }
35117
- /** Compile all .scss files in a directory into a single CSS output file. */
35118
- compileScss(scssDir = "src/scss", output = "src/public/css/default.css", minify = false) {
35119
- const absDir = resolve19(scssDir);
35120
- if (!existsSync24(absDir)) return "";
35121
- const files = readdirSync18(absDir).filter((f) => f.endsWith(".scss") && !f.startsWith("_")).sort().map((f) => join27(absDir, f));
35122
- if (files.length === 0) return "";
35123
- const paths = [absDir, ...this._importPaths];
35124
- const imported = /* @__PURE__ */ new Set();
35125
- let merged = "";
35126
- for (const file of files) {
35127
- const content = readFileSync23(file, "utf-8");
35128
- imported.add(file);
35129
- merged += resolveImports(content, paths, imported) + "\n";
35130
- }
35131
- let css = compileString(merged, paths, { ...this._variables });
35132
- if (minify) {
35133
- css = css.replace(/\/\*.*?\*\//gs, "");
35134
- css = css.replace(/\s+/g, " ");
35135
- css = css.replace(/\s*([{}:;,])\s*/g, "$1");
35136
- css = css.replace(/;}/g, "}");
35137
- css = css.trim();
35138
- }
35139
- const absOutput = resolve19(output);
35140
- const outDir = dirname12(absOutput);
35141
- if (!existsSync24(outDir)) mkdirSync18(outDir, { recursive: true });
35142
- let existing = null;
35143
- try {
35144
- existing = existsSync24(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
35145
- } catch {
35146
- existing = null;
35147
- }
35148
- if (existing !== css) {
35149
- writeFileSync16(absOutput, css, "utf-8");
35150
- }
35151
- return css;
35152
- }
35153
- };
35154
- VARIABLE_FLAG = /\s*!(default|global)\s*$/;
35155
- }
35156
- });
35157
-
35158
34761
  // src/mqttMessage.ts
35159
34762
  var MqttMessage;
35160
34763
  var init_mqttMessage = __esm({
@@ -35228,7 +34831,7 @@ var init_mqttMessage = __esm({
35228
34831
  import net2 from "node:net";
35229
34832
  import tls from "node:tls";
35230
34833
  import { randomBytes as randomBytes7 } from "node:crypto";
35231
- import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
34834
+ import { existsSync as existsSync24, readFileSync as readFileSync23 } from "node:fs";
35232
34835
  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;
35233
34836
  var init_mqtt = __esm({
35234
34837
  "src/mqtt.ts"() {
@@ -35431,7 +35034,7 @@ var init_mqtt = __esm({
35431
35034
  */
35432
35035
  async connect() {
35433
35036
  this.closeSocket();
35434
- if (this.secure && this.tlsVerify && this.caFile && !existsSync25(this.caFile)) {
35037
+ if (this.secure && this.tlsVerify && this.caFile && !existsSync24(this.caFile)) {
35435
35038
  throw new MqttError(
35436
35039
  `MQTT CA file not found: ${this.caFile} -- TINA4_MQTT_CA_FILE (or caFile) must point at the broker's CA certificate in PEM form`
35437
35040
  );
@@ -35668,7 +35271,7 @@ var init_mqtt = __esm({
35668
35271
  * a later client.
35669
35272
  */
35670
35273
  openSocket() {
35671
- return new Promise((resolve21, reject) => {
35274
+ return new Promise((resolve20, reject) => {
35672
35275
  let settled = false;
35673
35276
  const settle = (fn) => {
35674
35277
  if (settled) return;
@@ -35693,10 +35296,10 @@ var init_mqtt = __esm({
35693
35296
  servername: this.host,
35694
35297
  rejectUnauthorized: this.tlsVerify
35695
35298
  };
35696
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
35697
- sock = tls.connect(opts, () => settle(() => resolve21(sock)));
35299
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync23(this.caFile);
35300
+ sock = tls.connect(opts, () => settle(() => resolve20(sock)));
35698
35301
  } else {
35699
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
35302
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
35700
35303
  }
35701
35304
  sock.once("error", (err) => {
35702
35305
  settle(() => {
@@ -35735,13 +35338,13 @@ var init_mqtt = __esm({
35735
35338
  writePacket(header, body) {
35736
35339
  if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
35737
35340
  const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
35738
- return new Promise((resolve21, reject) => {
35341
+ return new Promise((resolve20, reject) => {
35739
35342
  this.socket.write(packet, (err) => {
35740
35343
  if (err) {
35741
35344
  reject(new MqttError(`MQTT write failed: ${err.message}`));
35742
35345
  } else {
35743
35346
  this.lastWriteAt = Date.now();
35744
- resolve21();
35347
+ resolve20();
35745
35348
  }
35746
35349
  });
35747
35350
  });
@@ -35774,7 +35377,7 @@ var init_mqtt = __esm({
35774
35377
  if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
35775
35378
  if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
35776
35379
  if (this.socketError !== null) return Promise.reject(this.socketError);
35777
- return new Promise((resolve21, reject) => {
35380
+ return new Promise((resolve20, reject) => {
35778
35381
  let timer = null;
35779
35382
  if (deadline !== null) {
35780
35383
  const remaining = deadline - Date.now();
@@ -35789,7 +35392,7 @@ var init_mqtt = __esm({
35789
35392
  }
35790
35393
  }, remaining);
35791
35394
  }
35792
- this.waiter = { need, resolve: resolve21, reject, timer };
35395
+ this.waiter = { need, resolve: resolve20, reject, timer };
35793
35396
  this.serviceWaiter();
35794
35397
  });
35795
35398
  }
@@ -35913,8 +35516,8 @@ var init_mqtt = __esm({
35913
35516
  });
35914
35517
 
35915
35518
  // src/service.ts
35916
- import { readdirSync as readdirSync19, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
35917
- import { join as join28, extname as extname8 } from "node:path";
35519
+ import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
35520
+ import { join as join27, extname as extname8 } from "node:path";
35918
35521
  import { pathToFileURL } from "node:url";
35919
35522
  function matchCronField(field, value) {
35920
35523
  if (field === "*") return true;
@@ -36080,14 +35683,14 @@ var init_service = __esm({
36080
35683
  const discovered = [];
36081
35684
  let entries;
36082
35685
  try {
36083
- entries = readdirSync19(dir);
35686
+ entries = readdirSync18(dir);
36084
35687
  } catch {
36085
35688
  return discovered;
36086
35689
  }
36087
35690
  for (const entry of entries) {
36088
35691
  const ext = extname8(entry);
36089
35692
  if (ext !== ".ts" && ext !== ".js") continue;
36090
- const fullPath = join28(dir, entry);
35693
+ const fullPath = join27(dir, entry);
36091
35694
  const stat = statSync18(fullPath);
36092
35695
  if (!stat.isFile()) continue;
36093
35696
  try {
@@ -36196,14 +35799,14 @@ var init_service = __esm({
36196
35799
  const dir = serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
36197
35800
  let entries;
36198
35801
  try {
36199
- entries = readdirSync19(dir);
35802
+ entries = readdirSync18(dir);
36200
35803
  } catch {
36201
35804
  return;
36202
35805
  }
36203
35806
  for (const entry of entries) {
36204
35807
  const ext = extname8(entry);
36205
35808
  if (ext !== ".ts" && ext !== ".js") continue;
36206
- const fullPath = join28(dir, entry);
35809
+ const fullPath = join27(dir, entry);
36207
35810
  if (watchedFiles.has(fullPath)) continue;
36208
35811
  watchedFiles.add(fullPath);
36209
35812
  watchFile(fullPath, { interval: 1e3 }, async () => {
@@ -36247,7 +35850,7 @@ import https from "node:https";
36247
35850
  import { URL as URL2 } from "node:url";
36248
35851
  import { randomBytes as randomBytes8 } from "node:crypto";
36249
35852
  import { promises as fsp, createWriteStream } from "node:fs";
36250
- import { basename as basename5 } from "node:path";
35853
+ import { basename as basename4 } from "node:path";
36251
35854
  import { pipeline } from "node:stream/promises";
36252
35855
  function sameOrigin(urlA, urlB) {
36253
35856
  try {
@@ -36537,7 +36140,7 @@ var init_api = __esm({
36537
36140
  error: err instanceof Error ? err.message : String(err)
36538
36141
  };
36539
36142
  }
36540
- uploadName = filename || basename5(filePath);
36143
+ uploadName = filename || basename4(filePath);
36541
36144
  } else {
36542
36145
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
36543
36146
  }
@@ -36752,12 +36355,12 @@ var init_api = __esm({
36752
36355
  * authenticate to.
36753
36356
  */
36754
36357
  performRequest(method, url, headers, data, redirectsLeft) {
36755
- return new Promise((resolve21) => {
36358
+ return new Promise((resolve20) => {
36756
36359
  let parsed;
36757
36360
  try {
36758
36361
  parsed = new URL2(url);
36759
36362
  } catch (err) {
36760
- resolve21({ kind: "error", error: err instanceof Error ? err.message : String(err) });
36363
+ resolve20({ kind: "error", error: err instanceof Error ? err.message : String(err) });
36761
36364
  return;
36762
36365
  }
36763
36366
  const isHttps = parsed.protocol === "https:";
@@ -36782,7 +36385,7 @@ var init_api = __esm({
36782
36385
  try {
36783
36386
  nextUrl = new URL2(location, url).toString();
36784
36387
  } catch {
36785
- resolve21({ kind: "response", res });
36388
+ resolve20({ kind: "response", res });
36786
36389
  return;
36787
36390
  }
36788
36391
  const crossOrigin = !sameOrigin(url, nextUrl);
@@ -36800,17 +36403,17 @@ var init_api = __esm({
36800
36403
  deleteHeaderCaseInsensitive(nextHeaders, name);
36801
36404
  }
36802
36405
  }
36803
- this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve21);
36406
+ this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve20);
36804
36407
  return;
36805
36408
  }
36806
- resolve21({ kind: "response", res });
36409
+ resolve20({ kind: "response", res });
36807
36410
  });
36808
36411
  req2.on("timeout", () => {
36809
36412
  req2.destroy();
36810
- resolve21({ kind: "error", error: `Request timed out after ${this.timeout}s` });
36413
+ resolve20({ kind: "error", error: `Request timed out after ${this.timeout}s` });
36811
36414
  });
36812
36415
  req2.on("error", (err) => {
36813
- resolve21({ kind: "error", error: err.message });
36416
+ resolve20({ kind: "error", error: err.message });
36814
36417
  });
36815
36418
  if (data) {
36816
36419
  req2.write(data);
@@ -36820,7 +36423,7 @@ var init_api = __esm({
36820
36423
  }
36821
36424
  /** Buffer a response body, parse JSON if possible, and store cookies. */
36822
36425
  readResponse(res) {
36823
- return new Promise((resolve21) => {
36426
+ return new Promise((resolve20) => {
36824
36427
  const chunks = [];
36825
36428
  res.on("data", (chunk) => {
36826
36429
  chunks.push(chunk);
@@ -36835,7 +36438,7 @@ var init_api = __esm({
36835
36438
  } catch {
36836
36439
  parsed = raw;
36837
36440
  }
36838
- resolve21({
36441
+ resolve20({
36839
36442
  http_code: res.statusCode ?? null,
36840
36443
  body: parsed,
36841
36444
  headers: respHeaders,
@@ -36843,7 +36446,7 @@ var init_api = __esm({
36843
36446
  });
36844
36447
  });
36845
36448
  res.on("error", (err) => {
36846
- resolve21({ http_code: null, body: null, headers: {}, error: err.message });
36449
+ resolve20({ http_code: null, body: null, headers: {}, error: err.message });
36847
36450
  });
36848
36451
  });
36849
36452
  }
@@ -36896,14 +36499,14 @@ var init_api = __esm({
36896
36499
  // src/messenger.ts
36897
36500
  import net3 from "node:net";
36898
36501
  import tls2 from "node:tls";
36899
- import { readFileSync as readFileSync25 } from "node:fs";
36900
- import { basename as basename6 } from "node:path";
36502
+ import { readFileSync as readFileSync24 } from "node:fs";
36503
+ import { basename as basename5 } from "node:path";
36901
36504
  import { randomUUID as randomUUID7 } from "node:crypto";
36902
36505
  function tlsRejectUnauthorized() {
36903
36506
  return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
36904
36507
  }
36905
36508
  function readResponse(socket) {
36906
- return new Promise((resolve21, reject) => {
36509
+ return new Promise((resolve20, reject) => {
36907
36510
  let buffer = "";
36908
36511
  const onData = (chunk) => {
36909
36512
  buffer += chunk.toString("utf-8");
@@ -36915,7 +36518,7 @@ function readResponse(socket) {
36915
36518
  if (line.length >= 4 && line[3] === " ") {
36916
36519
  socket.removeListener("data", onData);
36917
36520
  socket.removeListener("error", onError);
36918
- resolve21({ code, text: buffer.trim() });
36521
+ resolve20({ code, text: buffer.trim() });
36919
36522
  return;
36920
36523
  }
36921
36524
  }
@@ -36929,10 +36532,10 @@ function readResponse(socket) {
36929
36532
  });
36930
36533
  }
36931
36534
  function sendCommand(socket, command) {
36932
- return new Promise((resolve21, reject) => {
36535
+ return new Promise((resolve20, reject) => {
36933
36536
  socket.write(command + "\r\n", "utf-8", (err) => {
36934
36537
  if (err) return reject(err);
36935
- readResponse(socket).then(resolve21, reject);
36538
+ readResponse(socket).then(resolve20, reject);
36936
36539
  });
36937
36540
  });
36938
36541
  }
@@ -36988,8 +36591,8 @@ function buildMimeMessage(options) {
36988
36591
  lines.push(options.body);
36989
36592
  }
36990
36593
  for (const filePath of options.attachments) {
36991
- const fileName = basename6(filePath);
36992
- const fileData = readFileSync25(filePath);
36594
+ const fileName = basename5(filePath);
36595
+ const fileData = readFileSync24(filePath);
36993
36596
  const base64Data = fileData.toString("base64");
36994
36597
  lines.push("");
36995
36598
  lines.push(`--${boundary}`);
@@ -37032,7 +36635,7 @@ function imapQuote(s) {
37032
36635
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
37033
36636
  }
37034
36637
  function imapReadLine(socket) {
37035
- return new Promise((resolve21, reject) => {
36638
+ return new Promise((resolve20, reject) => {
37036
36639
  let buffer = "";
37037
36640
  const onData = (chunk) => {
37038
36641
  buffer += chunk.toString("utf-8");
@@ -37040,7 +36643,7 @@ function imapReadLine(socket) {
37040
36643
  if (nlIndex !== -1) {
37041
36644
  socket.removeListener("data", onData);
37042
36645
  socket.removeListener("error", onError);
37043
- resolve21(buffer);
36646
+ resolve20(buffer);
37044
36647
  }
37045
36648
  };
37046
36649
  const onError = (err) => {
@@ -37052,7 +36655,7 @@ function imapReadLine(socket) {
37052
36655
  });
37053
36656
  }
37054
36657
  function imapCommand(socket, command) {
37055
- return new Promise((resolve21, reject) => {
36658
+ return new Promise((resolve20, reject) => {
37056
36659
  imapTagCounter++;
37057
36660
  const tag = `T${imapTagCounter}`;
37058
36661
  const fullCommand = `${tag} ${command}\r
@@ -37063,7 +36666,7 @@ function imapCommand(socket, command) {
37063
36666
  if (buffer.includes(`${tag} OK`)) {
37064
36667
  socket.removeListener("data", onData);
37065
36668
  socket.removeListener("error", onError);
37066
- resolve21(buffer);
36669
+ resolve20(buffer);
37067
36670
  return;
37068
36671
  }
37069
36672
  if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
@@ -37092,91 +36695,149 @@ function parseSearchResponse(response) {
37092
36695
  if (!match) return [];
37093
36696
  return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
37094
36697
  }
37095
- function parseHeaderResponse(uid, response) {
37096
- const headers = {};
37097
- const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
37098
- if (headerBlock) {
37099
- const lines = headerBlock[1].split(/\r\n/);
37100
- let currentKey = "";
37101
- for (const line of lines) {
37102
- if (/^\s/.test(line) && currentKey) {
37103
- headers[currentKey] += " " + line.trim();
37104
- } else {
37105
- const colonIdx = line.indexOf(":");
37106
- if (colonIdx > 0) {
37107
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
37108
- headers[currentKey] = line.substring(colonIdx + 1).trim();
37109
- }
37110
- }
37111
- }
37112
- }
37113
- const seen = /\\Seen/i.test(response);
37114
- return {
37115
- uid,
37116
- subject: headers["subject"] ?? "",
37117
- from: headers["from"] ?? "",
37118
- to: headers["to"] ?? "",
37119
- date: headers["date"] ?? "",
37120
- snippet: "",
37121
- seen
37122
- };
36698
+ function extractRawMessage(response) {
36699
+ const m = response.match(/\{(\d+)\}\r\n/);
36700
+ if (!m) return response;
36701
+ const start2 = (m.index ?? 0) + m[0].length;
36702
+ return response.slice(start2, start2 + parseInt(m[1], 10));
37123
36703
  }
37124
- function parseFullMessage(uid, response) {
37125
- const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
37126
- const rawMessage = bodyMatch ? bodyMatch[2] : response;
37127
- const headerEnd = rawMessage.indexOf("\r\n\r\n");
37128
- const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
37129
- const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
36704
+ function parseMimeHeaders(section) {
37130
36705
  const headers = {};
37131
- const headerLines = headerSection.split(/\r\n/);
37132
36706
  let currentKey = "";
37133
- for (const line of headerLines) {
36707
+ for (const line of section.split(/\r\n/)) {
37134
36708
  if (/^\s/.test(line) && currentKey) {
37135
36709
  headers[currentKey] += " " + line.trim();
37136
36710
  } else {
37137
- const colonIdx = line.indexOf(":");
37138
- if (colonIdx > 0) {
37139
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
37140
- headers[currentKey] = line.substring(colonIdx + 1).trim();
36711
+ const idx = line.indexOf(":");
36712
+ if (idx > 0) {
36713
+ currentKey = line.substring(0, idx).trim().toLowerCase();
36714
+ headers[currentKey] = line.substring(idx + 1).trim();
36715
+ }
36716
+ }
36717
+ }
36718
+ return headers;
36719
+ }
36720
+ function decodeTransfer(body, encoding) {
36721
+ const enc = encoding.toLowerCase().trim();
36722
+ if (enc === "base64") {
36723
+ try {
36724
+ return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf-8");
36725
+ } catch {
36726
+ return body;
36727
+ }
36728
+ }
36729
+ if (enc === "quoted-printable") {
36730
+ return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
36731
+ }
36732
+ return body;
36733
+ }
36734
+ function decodeAttachmentBytes(body, encoding) {
36735
+ const enc = encoding.toLowerCase().trim();
36736
+ if (enc === "base64") {
36737
+ return Buffer.from(body.replace(/\s+/g, ""), "base64");
36738
+ }
36739
+ const trimmed = body.replace(/\r\n$/, "");
36740
+ if (enc === "quoted-printable") {
36741
+ const collapsed = trimmed.replace(/=\r?\n/g, "");
36742
+ const bytes = [];
36743
+ for (let i = 0; i < collapsed.length; i++) {
36744
+ const hex = collapsed.substring(i + 1, i + 3);
36745
+ if (collapsed[i] === "=" && /^[0-9A-Fa-f]{2}$/.test(hex)) {
36746
+ bytes.push(parseInt(hex, 16));
36747
+ i += 2;
36748
+ } else {
36749
+ bytes.push(collapsed.charCodeAt(i) & 255);
37141
36750
  }
37142
36751
  }
36752
+ return Buffer.from(bytes);
37143
36753
  }
36754
+ return Buffer.from(trimmed, "utf-8");
36755
+ }
36756
+ function attachmentFilename(disposition, contentType) {
36757
+ const d = disposition.match(/filename="?([^";\r\n]+)"?/i);
36758
+ if (d) return d[1].trim();
36759
+ const c = contentType.match(/name="?([^";\r\n]+)"?/i);
36760
+ if (c) return c[1].trim();
36761
+ return "attachment";
36762
+ }
36763
+ function makeSnippet(bodyText, bodyHtml) {
36764
+ return (bodyText || bodyHtml || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200);
36765
+ }
36766
+ function toIsoDate(raw) {
36767
+ if (!raw) return "";
36768
+ const d = new Date(raw);
36769
+ return Number.isNaN(d.getTime()) ? raw : d.toISOString();
36770
+ }
36771
+ function parseMessage(response) {
36772
+ const raw = extractRawMessage(response);
36773
+ const headerEnd = raw.indexOf("\r\n\r\n");
36774
+ const headerSection = headerEnd >= 0 ? raw.substring(0, headerEnd) : raw;
36775
+ const bodySection = headerEnd >= 0 ? raw.substring(headerEnd + 4) : "";
36776
+ const headers = parseMimeHeaders(headerSection);
37144
36777
  const contentType = headers["content-type"] ?? "text/plain";
37145
36778
  let bodyText = "";
37146
36779
  let bodyHtml = "";
36780
+ const attachments = [];
37147
36781
  if (contentType.includes("multipart")) {
37148
36782
  const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
37149
36783
  if (boundaryMatch) {
37150
- const boundary = boundaryMatch[1];
37151
- const parts = bodySection.split("--" + boundary);
37152
- for (const part of parts) {
37153
- if (part.trim() === "" || part.trim() === "--") continue;
37154
- const partHeaderEnd = part.indexOf("\r\n\r\n");
37155
- const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
37156
- const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
37157
- if (partHeaders.includes("text/html")) {
37158
- bodyHtml = partBody;
37159
- } else if (partHeaders.includes("text/plain")) {
37160
- bodyText = partBody;
36784
+ const boundary = "--" + boundaryMatch[1];
36785
+ for (const part of bodySection.split(boundary)) {
36786
+ const trimmed = part.trim();
36787
+ if (trimmed === "" || trimmed === "--") continue;
36788
+ const pEnd = part.indexOf("\r\n\r\n");
36789
+ if (pEnd < 0) continue;
36790
+ const pHeaders = parseMimeHeaders(part.substring(0, pEnd));
36791
+ const pBody = part.substring(pEnd + 4);
36792
+ const cte = pHeaders["content-transfer-encoding"] ?? "";
36793
+ const pType = pHeaders["content-type"] ?? "text/plain";
36794
+ const disposition = pHeaders["content-disposition"] ?? "";
36795
+ if (/attachment/i.test(disposition)) {
36796
+ const content = decodeAttachmentBytes(pBody, cte);
36797
+ attachments.push({
36798
+ filename: attachmentFilename(disposition, pType),
36799
+ contentType: pType.split(";")[0].trim(),
36800
+ size: content.length,
36801
+ content
36802
+ });
36803
+ } else if (pType.includes("text/html")) {
36804
+ bodyHtml = decodeTransfer(pBody, cte).trim();
36805
+ } else if (pType.includes("text/plain")) {
36806
+ bodyText = decodeTransfer(pBody, cte).trim();
37161
36807
  }
37162
36808
  }
37163
36809
  }
37164
36810
  } else if (contentType.includes("text/html")) {
37165
- bodyHtml = bodySection;
36811
+ bodyHtml = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
37166
36812
  } else {
37167
- bodyText = bodySection;
36813
+ bodyText = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
37168
36814
  }
37169
- bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
37170
- bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
36815
+ return { headers, bodyText, bodyHtml, attachments };
36816
+ }
36817
+ function parseSummary(uid, response) {
36818
+ const { headers, bodyText, bodyHtml } = parseMessage(response);
36819
+ return {
36820
+ uid,
36821
+ subject: headers["subject"] ?? "",
36822
+ from: headers["from"] ?? "",
36823
+ to: headers["to"] ?? "",
36824
+ date: toIsoDate(headers["date"] ?? ""),
36825
+ snippet: makeSnippet(bodyText, bodyHtml),
36826
+ seen: /\\Seen/i.test(response)
36827
+ };
36828
+ }
36829
+ function parseFullMessage(uid, response) {
36830
+ const { headers, bodyText, bodyHtml, attachments } = parseMessage(response);
37171
36831
  return {
37172
36832
  uid,
37173
36833
  subject: headers["subject"] ?? "",
37174
36834
  from: headers["from"] ?? "",
37175
36835
  to: headers["to"] ?? "",
37176
36836
  cc: headers["cc"] ?? "",
37177
- date: headers["date"] ?? "",
36837
+ date: toIsoDate(headers["date"] ?? ""),
37178
36838
  bodyText,
37179
36839
  bodyHtml,
36840
+ attachments,
37180
36841
  headers
37181
36842
  };
37182
36843
  }
@@ -37292,89 +36953,89 @@ var init_messenger = __esm({
37292
36953
  }
37293
36954
  const messageId = `${randomUUID7()}@${this.host}`;
37294
36955
  if (allRecipients.length === 0) {
37295
- return { success: false, message: "No recipients specified" };
36956
+ return { success: false, message: "No recipients specified", id: null };
37296
36957
  }
37297
36958
  if (!this.fromAddress) {
37298
- return { success: false, message: "No from address configured" };
36959
+ return { success: false, message: "No from address configured", id: null };
37299
36960
  }
37300
36961
  try {
37301
36962
  let socket;
37302
36963
  if (this.port === 465) {
37303
36964
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
37304
- await new Promise((resolve21, reject) => {
37305
- socket.once("secureConnect", resolve21);
36965
+ await new Promise((resolve20, reject) => {
36966
+ socket.once("secureConnect", resolve20);
37306
36967
  socket.once("error", reject);
37307
36968
  });
37308
36969
  } else {
37309
36970
  socket = net3.createConnection({ host: this.host, port: this.port });
37310
- await new Promise((resolve21, reject) => {
37311
- socket.once("connect", resolve21);
36971
+ await new Promise((resolve20, reject) => {
36972
+ socket.once("connect", resolve20);
37312
36973
  socket.once("error", reject);
37313
36974
  });
37314
36975
  }
37315
36976
  const greeting = await readResponse(socket);
37316
36977
  if (greeting.code !== 220) {
37317
36978
  socket.destroy();
37318
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
36979
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}`, id: null };
37319
36980
  }
37320
36981
  const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
37321
36982
  if (ehlo.code !== 250) {
37322
36983
  socket.destroy();
37323
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
36984
+ return { success: false, message: `EHLO failed: ${ehlo.text}`, id: null };
37324
36985
  }
37325
36986
  if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
37326
36987
  const starttls = await sendCommand(socket, "STARTTLS");
37327
36988
  if (starttls.code !== 220) {
37328
36989
  socket.destroy();
37329
- return { success: false, message: `STARTTLS failed: ${starttls.text}` };
36990
+ return { success: false, message: `STARTTLS failed: ${starttls.text}`, id: null };
37330
36991
  }
37331
36992
  const plainSocket = socket;
37332
36993
  socket = tls2.connect(
37333
36994
  { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
37334
36995
  );
37335
- await new Promise((resolve21, reject) => {
37336
- socket.once("secureConnect", resolve21);
36996
+ await new Promise((resolve20, reject) => {
36997
+ socket.once("secureConnect", resolve20);
37337
36998
  socket.once("error", reject);
37338
36999
  });
37339
37000
  const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
37340
37001
  if (ehlo2.code !== 250) {
37341
37002
  socket.destroy();
37342
- return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
37003
+ return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}`, id: null };
37343
37004
  }
37344
37005
  }
37345
37006
  if (this.username && this.password) {
37346
37007
  const auth = await sendCommand(socket, "AUTH LOGIN");
37347
37008
  if (auth.code !== 334) {
37348
37009
  socket.destroy();
37349
- return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
37010
+ return { success: false, message: `AUTH LOGIN failed: ${auth.text}`, id: null };
37350
37011
  }
37351
37012
  const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
37352
37013
  if (userResp.code !== 334) {
37353
37014
  socket.destroy();
37354
- return { success: false, message: `AUTH username failed: ${userResp.text}` };
37015
+ return { success: false, message: `AUTH username failed: ${userResp.text}`, id: null };
37355
37016
  }
37356
37017
  const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
37357
37018
  if (passResp.code !== 235) {
37358
37019
  socket.destroy();
37359
- return { success: false, message: `AUTH password failed: ${passResp.text}` };
37020
+ return { success: false, message: `AUTH password failed: ${passResp.text}`, id: null };
37360
37021
  }
37361
37022
  }
37362
37023
  const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
37363
37024
  if (mailFrom.code !== 250) {
37364
37025
  socket.destroy();
37365
- return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
37026
+ return { success: false, message: `MAIL FROM failed: ${mailFrom.text}`, id: null };
37366
37027
  }
37367
37028
  for (const recipient of allRecipients) {
37368
37029
  const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
37369
37030
  if (rcpt.code !== 250 && rcpt.code !== 251) {
37370
37031
  socket.destroy();
37371
- return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
37032
+ return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}`, id: null };
37372
37033
  }
37373
37034
  }
37374
37035
  const dataCmd = await sendCommand(socket, "DATA");
37375
37036
  if (dataCmd.code !== 354) {
37376
37037
  socket.destroy();
37377
- return { success: false, message: `DATA failed: ${dataCmd.text}` };
37038
+ return { success: false, message: `DATA failed: ${dataCmd.text}`, id: null };
37378
37039
  }
37379
37040
  const mimeMessage = buildMimeMessage({
37380
37041
  from: this.fromAddress,
@@ -37393,16 +37054,31 @@ var init_messenger = __esm({
37393
37054
  const endData = await sendCommand(socket, mimeMessage + "\r\n.");
37394
37055
  if (endData.code !== 250) {
37395
37056
  socket.destroy();
37396
- return { success: false, message: `Message delivery failed: ${endData.text}` };
37057
+ return { success: false, message: `Message delivery failed: ${endData.text}`, id: null };
37397
37058
  }
37398
37059
  await sendCommand(socket, "QUIT");
37399
37060
  socket.destroy();
37400
37061
  return { success: true, message: "Email sent successfully", id: messageId };
37401
37062
  } catch (err) {
37402
37063
  const errMsg = err instanceof Error ? err.message : String(err);
37403
- return { success: false, message: `SMTP error: ${errMsg}` };
37064
+ return { success: false, message: `SMTP error: ${errMsg}`, id: null };
37404
37065
  }
37405
37066
  }
37067
+ /**
37068
+ * Render a Frond template STRING and send it as an HTML email (G7, parity with
37069
+ * Python's send_template). Extra send() options (cc, bcc, replyTo, attachments,
37070
+ * headers) pass through. If the Frond package cannot be loaded the raw template
37071
+ * is sent verbatim (matches Python's ImportError fallback) rather than failing.
37072
+ */
37073
+ async sendTemplate(to, subject, template, data = {}, cc, bcc, replyTo, attachments, headers) {
37074
+ let body = template;
37075
+ try {
37076
+ const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
37077
+ body = new Frond2().renderString(template, data);
37078
+ } catch {
37079
+ }
37080
+ return this.send(to, subject, body, true, void 0, cc, bcc, replyTo, attachments, headers);
37081
+ }
37406
37082
  /**
37407
37083
  * Test the SMTP connection without sending an email.
37408
37084
  */
@@ -37411,14 +37087,14 @@ var init_messenger = __esm({
37411
37087
  let socket;
37412
37088
  if (this.port === 465) {
37413
37089
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
37414
- await new Promise((resolve21, reject) => {
37415
- socket.once("secureConnect", resolve21);
37090
+ await new Promise((resolve20, reject) => {
37091
+ socket.once("secureConnect", resolve20);
37416
37092
  socket.once("error", reject);
37417
37093
  });
37418
37094
  } else {
37419
37095
  socket = net3.createConnection({ host: this.host, port: this.port });
37420
- await new Promise((resolve21, reject) => {
37421
- socket.once("connect", resolve21);
37096
+ await new Promise((resolve20, reject) => {
37097
+ socket.once("connect", resolve20);
37422
37098
  socket.once("error", reject);
37423
37099
  });
37424
37100
  }
@@ -37453,14 +37129,14 @@ var init_messenger = __esm({
37453
37129
  const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
37454
37130
  if (useTls) {
37455
37131
  socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
37456
- await new Promise((resolve21, reject) => {
37457
- socket.once("secureConnect", resolve21);
37132
+ await new Promise((resolve20, reject) => {
37133
+ socket.once("secureConnect", resolve20);
37458
37134
  socket.once("error", reject);
37459
37135
  });
37460
37136
  } else {
37461
37137
  socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
37462
- await new Promise((resolve21, reject) => {
37463
- socket.once("connect", resolve21);
37138
+ await new Promise((resolve20, reject) => {
37139
+ socket.once("connect", resolve20);
37464
37140
  socket.once("error", reject);
37465
37141
  });
37466
37142
  }
@@ -37497,7 +37173,7 @@ var init_messenger = __esm({
37497
37173
  }
37498
37174
  try {
37499
37175
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37500
- const searchResp = await imapCommand(socket, "SEARCH ALL");
37176
+ const searchResp = await imapCommand(socket, "UID SEARCH ALL");
37501
37177
  const uids = parseSearchResponse(searchResp);
37502
37178
  if (uids.length === 0) return [];
37503
37179
  uids.reverse();
@@ -37505,8 +37181,8 @@ var init_messenger = __esm({
37505
37181
  if (selected.length === 0) return [];
37506
37182
  const messages = [];
37507
37183
  for (const uid of selected) {
37508
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
37509
- messages.push(parseHeaderResponse(uid, fetchResp));
37184
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
37185
+ messages.push(parseSummary(uid, fetchResp));
37510
37186
  }
37511
37187
  return messages;
37512
37188
  } catch (err) {
@@ -37516,7 +37192,7 @@ var init_messenger = __esm({
37516
37192
  }
37517
37193
  }
37518
37194
  /**
37519
- * Read a single message by sequence number or UID.
37195
+ * Read a single message by its IMAP UID.
37520
37196
  */
37521
37197
  async read(uid, folder = "INBOX") {
37522
37198
  let socket;
@@ -37527,11 +37203,11 @@ var init_messenger = __esm({
37527
37203
  }
37528
37204
  try {
37529
37205
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37530
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
37206
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY[])`);
37531
37207
  if (!/\{\d+\}/.test(fetchResp)) {
37532
37208
  return null;
37533
37209
  }
37534
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
37210
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
37535
37211
  return parseFullMessage(uid, fetchResp);
37536
37212
  } catch (err) {
37537
37213
  throw imapFail("read", err);
@@ -37558,14 +37234,14 @@ var init_messenger = __esm({
37558
37234
  }
37559
37235
  try {
37560
37236
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37561
- const searchResp = await imapCommand(socket, `SEARCH ${query}`);
37237
+ const searchResp = await imapCommand(socket, `UID SEARCH ${query}`);
37562
37238
  const uids = parseSearchResponse(searchResp);
37563
37239
  if (uids.length === 0) return [];
37564
37240
  uids.reverse();
37565
37241
  const messages = [];
37566
37242
  for (const uid of uids.slice(0, limit)) {
37567
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
37568
- messages.push(parseHeaderResponse(uid, fetchResp));
37243
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
37244
+ messages.push(parseSummary(uid, fetchResp));
37569
37245
  }
37570
37246
  return messages;
37571
37247
  } catch (err) {
@@ -37575,26 +37251,46 @@ var init_messenger = __esm({
37575
37251
  }
37576
37252
  }
37577
37253
  /**
37578
- * Delete a message by UID.
37254
+ * Delete a message by UID (mark \Deleted, then EXPUNGE).
37255
+ *
37256
+ * `delete` is the one cross-framework name (python/php/ruby/node all spell it
37257
+ * `delete`). `deleteMessage` remains as a DEPRECATED alias for one release.
37579
37258
  */
37580
- async deleteMessage(uid, folder = "INBOX") {
37259
+ async delete(uid, folder = "INBOX") {
37581
37260
  const socket = await this.imapConnect();
37582
37261
  try {
37583
37262
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37584
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
37263
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Deleted)`);
37585
37264
  await imapCommand(socket, "EXPUNGE");
37586
37265
  } finally {
37587
37266
  await this.imapDisconnect(socket);
37588
37267
  }
37589
37268
  }
37269
+ /** @deprecated Use {@link delete} — kept as an alias for one release (G7). */
37270
+ async deleteMessage(uid, folder = "INBOX") {
37271
+ return this.delete(uid, folder);
37272
+ }
37590
37273
  /**
37591
- * Mark a message as read.
37274
+ * Mark a message as read (+FLAGS \Seen).
37592
37275
  */
37593
37276
  async markRead(uid, folder = "INBOX") {
37594
37277
  const socket = await this.imapConnect();
37595
37278
  try {
37596
37279
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37597
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
37280
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
37281
+ } finally {
37282
+ await this.imapDisconnect(socket);
37283
+ }
37284
+ }
37285
+ /**
37286
+ * Mark a message as unread (-FLAGS \Seen) — the inverse of markRead (G7,
37287
+ * parity with Python's mark_unread).
37288
+ */
37289
+ async markUnread(uid, folder = "INBOX") {
37290
+ const socket = await this.imapConnect();
37291
+ try {
37292
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37293
+ await imapCommand(socket, `UID STORE ${uid} -FLAGS (\\Seen)`);
37598
37294
  } finally {
37599
37295
  await this.imapDisconnect(socket);
37600
37296
  }
@@ -37611,7 +37307,7 @@ var init_messenger = __esm({
37611
37307
  }
37612
37308
  try {
37613
37309
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37614
- const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
37310
+ const searchResp = await imapCommand(socket, "UID SEARCH UNSEEN");
37615
37311
  return parseSearchResponse(searchResp).length;
37616
37312
  } catch (err) {
37617
37313
  throw imapFail("unread", err);
@@ -38350,17 +38046,17 @@ var init_htmlElement = __esm({
38350
38046
  });
38351
38047
 
38352
38048
  // src/ai.ts
38353
- import { existsSync as existsSync26, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17, readFileSync as readFileSync26 } from "node:fs";
38049
+ import { existsSync as existsSync25, mkdirSync as mkdirSync18, writeFileSync as writeFileSync16, readFileSync as readFileSync25 } from "node:fs";
38354
38050
  import { homedir } from "node:os";
38355
- import { join as join29, resolve as resolve20, relative as relative10, dirname as dirname13 } from "node:path";
38051
+ import { join as join28, resolve as resolve19, relative as relative10, dirname as dirname12 } from "node:path";
38356
38052
  import { fileURLToPath as fileURLToPath6 } from "node:url";
38357
38053
  import { execSync, execFileSync as execFileSync3 } from "node:child_process";
38358
38054
  import { createInterface } from "node:readline";
38359
38055
  function readVersion() {
38360
38056
  try {
38361
- const thisDir = dirname13(fileURLToPath6(import.meta.url));
38362
- const rootPkg = resolve20(thisDir, "..", "..", "..", "package.json");
38363
- const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
38057
+ const thisDir = dirname12(fileURLToPath6(import.meta.url));
38058
+ const rootPkg = resolve19(thisDir, "..", "..", "..", "package.json");
38059
+ const pkg = JSON.parse(readFileSync25(rootPkg, "utf-8"));
38364
38060
  return pkg.version ?? "0.0.0";
38365
38061
  } catch {
38366
38062
  return "0.0.0";
@@ -38409,8 +38105,8 @@ function downloadSkillsSync(jobs) {
38409
38105
  function installSkills(root = ".", targets) {
38410
38106
  const ref = skillsRef();
38411
38107
  const dests = targets ?? [
38412
- join29(resolve20(root), ".claude", "skills"),
38413
- join29(homedir(), ".claude", "skills")
38108
+ join28(resolve19(root), ".claude", "skills"),
38109
+ join28(homedir(), ".claude", "skills")
38414
38110
  ];
38415
38111
  const jobs = [];
38416
38112
  const index = /* @__PURE__ */ new Map();
@@ -38428,9 +38124,9 @@ function installSkills(root = ".", targets) {
38428
38124
  const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
38429
38125
  skillMdUrl[skill] = `${base}/SKILL.md`;
38430
38126
  for (const dest of dests) {
38431
- add(`${base}/SKILL.md`, join29(dest, skill, "SKILL.md"));
38127
+ add(`${base}/SKILL.md`, join28(dest, skill, "SKILL.md"));
38432
38128
  for (const r of spec.references) {
38433
- add(`${base}/references/${r}`, join29(dest, skill, "references", r));
38129
+ add(`${base}/references/${r}`, join28(dest, skill, "references", r));
38434
38130
  }
38435
38131
  }
38436
38132
  }
@@ -38442,10 +38138,10 @@ function installSkills(root = ".", targets) {
38442
38138
  return installed;
38443
38139
  }
38444
38140
  function isInstalled(root, tool) {
38445
- return existsSync26(join29(resolve20(root), tool.contextFile));
38141
+ return existsSync25(join28(resolve19(root), tool.contextFile));
38446
38142
  }
38447
38143
  function showMenu(root = ".") {
38448
- const r = resolve20(root);
38144
+ const r = resolve19(root);
38449
38145
  console.log("\n Tina4 AI Context Installer\n");
38450
38146
  for (let i = 0; i < AI_TOOLS.length; i++) {
38451
38147
  const tool = AI_TOOLS[i];
@@ -38463,16 +38159,16 @@ function showMenu(root = ".") {
38463
38159
  const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
38464
38160
  console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
38465
38161
  console.log();
38466
- return new Promise((resolve21) => {
38162
+ return new Promise((resolve20) => {
38467
38163
  const rl = createInterface({ input: process.stdin, output: process.stdout });
38468
38164
  rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
38469
38165
  rl.close();
38470
- resolve21(answer.trim());
38166
+ resolve20(answer.trim());
38471
38167
  });
38472
38168
  });
38473
38169
  }
38474
38170
  function installSelected(root, selection) {
38475
- const rootPath = resolve20(root);
38171
+ const rootPath = resolve19(root);
38476
38172
  const created = [];
38477
38173
  let indices;
38478
38174
  let doInstallTina4Ai = false;
@@ -38565,33 +38261,33 @@ function looksLikeOldFrameworkInstall(existing) {
38565
38261
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
38566
38262
  const block = skillBlock(contextFile);
38567
38263
  const [start2, end] = markersFor(contextFile);
38568
- if (!existsSync26(contextPath)) {
38569
- writeFileSync17(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38264
+ if (!existsSync25(contextPath)) {
38265
+ writeFileSync16(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38570
38266
  return "Installed";
38571
38267
  }
38572
- const existing = readFileSync26(contextPath, "utf-8");
38268
+ const existing = readFileSync25(contextPath, "utf-8");
38573
38269
  if (hasMarkers(existing, start2, end)) {
38574
- writeFileSync17(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
38270
+ writeFileSync16(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
38575
38271
  return "Refreshed skill block in";
38576
38272
  }
38577
38273
  if (looksLikeOldFrameworkInstall(existing)) {
38578
38274
  const head = existing.replace(/^\s+/, "");
38579
38275
  const preamble = existing.slice(0, existing.length - head.length);
38580
38276
  const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
38581
- writeFileSync17(contextPath, newContent, "utf-8");
38277
+ writeFileSync16(contextPath, newContent, "utf-8");
38582
38278
  return "Migrated (replaced old framework dump in)";
38583
38279
  }
38584
- writeFileSync17(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38280
+ writeFileSync16(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38585
38281
  return "Appended skill block to";
38586
38282
  }
38587
38283
  function installForTool(root, tool, context) {
38588
38284
  const created = [];
38589
- const contextPath = join29(root, tool.contextFile);
38285
+ const contextPath = join28(root, tool.contextFile);
38590
38286
  if (tool.configDir) {
38591
- mkdirSync19(join29(root, tool.configDir), { recursive: true });
38287
+ mkdirSync18(join28(root, tool.configDir), { recursive: true });
38592
38288
  }
38593
- const parentDir = dirname13(contextPath);
38594
- mkdirSync19(parentDir, { recursive: true });
38289
+ const parentDir = dirname12(contextPath);
38290
+ mkdirSync18(parentDir, { recursive: true });
38595
38291
  const action = writeOrMerge(contextPath, tool.contextFile, context);
38596
38292
  const rel = relative10(root, contextPath);
38597
38293
  created.push(rel);
@@ -38626,7 +38322,7 @@ function installTina4Ai() {
38626
38322
  function installClaudeSkills(root) {
38627
38323
  const created = [];
38628
38324
  for (const skill of installSkills(root)) {
38629
- created.push(join29(".claude", "skills", skill));
38325
+ created.push(join28(".claude", "skills", skill));
38630
38326
  console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
38631
38327
  }
38632
38328
  return created;
@@ -38967,11 +38663,11 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
38967
38663
  }
38968
38664
  function generateClaudeCodeContext() {
38969
38665
  try {
38970
- const thisDir = dirname13(fileURLToPath6(import.meta.url));
38971
- const repoRoot = resolve20(thisDir, "..", "..", "..");
38972
- const claudeMdPath = join29(repoRoot, "CLAUDE.md");
38973
- if (existsSync26(claudeMdPath)) {
38974
- return readFileSync26(claudeMdPath, "utf-8");
38666
+ const thisDir = dirname12(fileURLToPath6(import.meta.url));
38667
+ const repoRoot = resolve19(thisDir, "..", "..", "..");
38668
+ const claudeMdPath = join28(repoRoot, "CLAUDE.md");
38669
+ if (existsSync25(claudeMdPath)) {
38670
+ return readFileSync25(claudeMdPath, "utf-8");
38975
38671
  }
38976
38672
  } catch {
38977
38673
  }
@@ -39487,16 +39183,8 @@ var init_rabbitmqBackend = __esm({
39487
39183
  process.stdout.write(String(msgCount));
39488
39184
  closeConnection();
39489
39185
  }
39490
- else if (operation === "purge") {
39491
- // Queue.Purge
39492
- const qBuf = Buffer.from(queueName, "utf-8");
39493
- const purgePayload = Buffer.alloc(4 + qBuf.length);
39494
- purgePayload.writeUInt16BE(0, 0);
39495
- purgePayload.writeUInt8(qBuf.length, 2);
39496
- qBuf.copy(purgePayload, 3);
39497
- purgePayload.writeUInt8(0, 3 + qBuf.length); // no-wait=false
39498
- sendMethod(1, 50, 30, purgePayload);
39499
- }
39186
+ // No "purge" operation: clear()/purge() refuse by name (ADR-0022),
39187
+ // so nothing ever sends Queue.Purge and the drain path is gone.
39500
39188
  }
39501
39189
  else if (classId === 60 && methodId === 71) {
39502
39190
  // Basic.Get-Ok \u2014 message body will follow in content frames
@@ -39507,11 +39195,6 @@ var init_rabbitmqBackend = __esm({
39507
39195
  process.stdout.write("__EMPTY__");
39508
39196
  closeConnection();
39509
39197
  }
39510
- else if (classId === 50 && methodId === 31) {
39511
- // Queue.Purge-Ok
39512
- process.stdout.write("__PURGED__");
39513
- closeConnection();
39514
- }
39515
39198
  else if (classId === 10 && methodId === 50) {
39516
39199
  // Connection.Close (server-initiated, e.g. a channel/protocol error)
39517
39200
  // \u2192 send Connection.Close-Ok and exit non-zero so the caller sees the
@@ -39641,8 +39324,15 @@ var init_rabbitmqBackend = __esm({
39641
39324
  const num = parseInt(result, 10);
39642
39325
  return isNaN(num) ? 0 : num;
39643
39326
  }
39644
- clear(queue) {
39645
- this.execSync("purge", queue);
39327
+ clear(_queue) {
39328
+ throw new Error(
39329
+ "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."
39330
+ );
39331
+ }
39332
+ purge(_queue, _status) {
39333
+ throw new Error(
39334
+ "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."
39335
+ );
39646
39336
  }
39647
39337
  };
39648
39338
  }
@@ -40204,6 +39894,14 @@ var init_kafkaBackend = __esm({
40204
39894
  return 0;
40205
39895
  }
40206
39896
  clear(_queue) {
39897
+ throw new Error(
39898
+ "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."
39899
+ );
39900
+ }
39901
+ purge(_queue, _status) {
39902
+ throw new Error(
39903
+ "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."
39904
+ );
40207
39905
  }
40208
39906
  };
40209
39907
  }
@@ -40963,7 +40661,6 @@ __export(index_exports, {
40963
40661
  RouteRef: () => RouteRef,
40964
40662
  Router: () => Router,
40965
40663
  SafeString: () => SafeString2,
40966
- ScssCompiler: () => ScssCompiler,
40967
40664
  SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
40968
40665
  ServiceRunner: () => ServiceRunner,
40969
40666
  Session: () => Session,
@@ -41159,7 +40856,6 @@ var init_index = __esm({
41159
40856
  init_session();
41160
40857
  init_i18n();
41161
40858
  init_fakeData();
41162
- init_scss();
41163
40859
  init_queue();
41164
40860
  init_job();
41165
40861
  init_mqtt();
@@ -41291,7 +40987,6 @@ export {
41291
40987
  RouteRef,
41292
40988
  Router,
41293
40989
  SafeString2 as SafeString,
41294
- ScssCompiler,
41295
40990
  SecurityHeadersMiddleware,
41296
40991
  ServiceRunner,
41297
40992
  Session,