tina4-nodejs 3.13.95 → 3.13.96

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.
@@ -1829,64 +1829,6 @@ var init_middleware = __esm({
1829
1829
  }
1830
1830
  });
1831
1831
 
1832
- // ../orm/src/types.ts
1833
- var FetchResult;
1834
- var init_types = __esm({
1835
- "../orm/src/types.ts"() {
1836
- "use strict";
1837
- FetchResult = class {
1838
- records;
1839
- count;
1840
- sql;
1841
- constructor(records, sql = "") {
1842
- this.records = records;
1843
- this.count = records.length;
1844
- this.sql = sql;
1845
- }
1846
- /** Paginate the in-memory result set. */
1847
- toPaginate(page = 1, perPage = 20) {
1848
- const total = this.count;
1849
- const totalPages = Math.max(1, Math.ceil(total / perPage));
1850
- const offset = (page - 1) * perPage;
1851
- const data = this.records.slice(offset, offset + perPage);
1852
- return {
1853
- data,
1854
- page,
1855
- perPage,
1856
- total,
1857
- totalPages,
1858
- hasNext: page < totalPages,
1859
- hasPrev: page > 1
1860
- };
1861
- }
1862
- /** Return the first record or null. */
1863
- first() {
1864
- return this.records[0] ?? null;
1865
- }
1866
- /** Return the last record or null. */
1867
- last() {
1868
- return this.records[this.records.length - 1] ?? null;
1869
- }
1870
- /** Check if result is empty. */
1871
- isEmpty() {
1872
- return this.records.length === 0;
1873
- }
1874
- /** Convert to plain array. */
1875
- toArray() {
1876
- return [...this.records];
1877
- }
1878
- /** Convert to JSON string. */
1879
- toJSON() {
1880
- return JSON.stringify(this.records);
1881
- }
1882
- /** Iterate over records. */
1883
- [Symbol.iterator]() {
1884
- return this.records[Symbol.iterator]();
1885
- }
1886
- };
1887
- }
1888
- });
1889
-
1890
1832
  // ../orm/src/databaseResult.ts
1891
1833
  var DatabaseResult;
1892
1834
  var init_databaseResult = __esm({
@@ -1950,75 +1892,52 @@ var init_databaseResult = __esm({
1950
1892
  toArray() {
1951
1893
  return this.records;
1952
1894
  }
1953
- /** Pagination envelope — accepts either (page, perPage) or (offset, limit) style.
1895
+ /**
1896
+ * Describe the page this result IS — the canonical pagination envelope.
1954
1897
  *
1955
- * When called with two arguments both >= 0 and the first >= the second
1956
- * (i.e. offset-style), pass `{ offset, limit }` as the first argument.
1957
- * The simplest way is to always use the default (page, perPage) form and
1958
- * let the autoCRUD layer supply offset/limit from the query string.
1898
+ * Takes NO arguments and derives every field from the query that produced this
1899
+ * result (ADR-0043). Passing ANY argument RAISES: a DatabaseResult holds no
1900
+ * connection, so an argument could only re-slice the rows already in memory and
1901
+ * then report total_pages for pages it can never reach. To read page N, FETCH
1902
+ * page N (limit + offset) and call this with no arguments.
1959
1903
  *
1960
- * Returns a superset of keys for backwards-compatibility across all clients.
1961
- */
1962
- /**
1963
- * Describe the page this result actually IS. Takes no arguments.
1904
+ * The envelope is EXACTLY seven snake_case keys, identical across all four
1905
+ * frameworks: `records, total, page, per_page, total_pages, limit, offset`.
1964
1906
  *
1965
- * MEASURED 2026-08-05 on a real 250-row table read with limit=20 offset=40
1966
- * (page 3 of 13): this reported page 1 of 2 and returned 10 of the 20 rows.
1967
- * It ignored the query entirely - defaulting page to 1 and perPage to 10 -
1968
- * then re-sliced the rows it was handed, which were already just that page.
1969
- * So a caller who paginated correctly at the SQL level had the answer
1970
- * silently re-paginated underneath them, with a page number that was simply
1971
- * wrong.
1907
+ * per_page = the query's limit
1908
+ * page = floor(offset / limit) + 1
1909
+ * total = the TRUE total for the filter Database.fetch (and
1910
+ * QueryBuilder.get) run a COUNT probe whenever a limit was
1911
+ * applied NEVER the number of rows returned
1912
+ * total_pages = ceil(total / per_page)
1913
+ * records = the rows the query returned, VERBATIM (never re-sliced)
1914
+ * limit = the SQL limit actually applied
1915
+ * offset = the SQL offset actually applied
1972
1916
  *
1973
- * WITH page/perPage it slices this result in memory, the behaviour GitHub
1974
- * issue #106 asked for. Valid ONLY when the result holds the WHOLE set
1975
- * (records.length >= count). A PARTIAL result cannot be sliced by page number
1976
- * without lying: MEASURED on 100,000 rows read under the default cap of 100,
1977
- * pages 1-5 of 20 were right and every page from 6 onward came back EMPTY
1978
- * while totalPages reported 5,000.
1917
+ * The JSON payload is snake_case even though the method name is camelCase — a
1918
+ * JSON key is data, not a language surface (ADR-0043). The old duplicate and
1919
+ * camelCase keys (`data`, `count`, `perPage`, `totalPages`, `has_next`,
1920
+ * `has_prev`) are removed: Node emitted 13 keys, the worst offender of the four.
1979
1921
  *
1980
- * `total` is `count`, and `count` is now the TRUE total for the filter in
1981
- * all four frameworks - Database.fetch runs a COUNT probe whenever it applied
1982
- * a limit. It used to be ROWS RETURNED here and in Ruby while Python and PHP
1983
- * probed, so one query answered 20 in two frameworks and 250 in the other
1984
- * two.
1922
+ * @throws {TypeError} if called with any argument.
1985
1923
  */
1986
- toPaginate(page, perPage) {
1987
- if ((page !== void 0 || perPage !== void 0) && this.records.length < this.count) {
1924
+ toPaginate() {
1925
+ if (arguments.length > 0) {
1988
1926
  throw new TypeError(
1989
- `toPaginate(page, perPage) slices the rows this result holds, but this result holds only ${this.records.length} of ${this.count} rows - it is a PARTIAL result, so any page past the rows it holds comes back empty while totalPages claims it exists. MEASURED on 100,000 rows read under the default cap of 100: pages 1-5 of 20 were right and pages 6 onward returned NOTHING. Fetch the page you want instead: fetch(sql, params, perPage, (page - 1) * perPage), then call toPaginate() with no arguments.`
1927
+ "toPaginate() takes no arguments and derives the page from the query that ran (ADR-0043). A DatabaseResult holds no connection, so an argument could only re-slice the rows already in memory and report total_pages for pages it can never reach. To read a page, FETCH it: db.fetch(sql, params, perPage, (page - 1) * perPage), then call toPaginate() with no arguments."
1990
1928
  );
1991
1929
  }
1992
- let resolvedPerPage;
1993
- let resolvedPage;
1994
- let offset;
1995
- let rows;
1996
- if (page === void 0 && perPage === void 0) {
1997
- resolvedPerPage = this.limit > 0 ? this.limit : this.records.length;
1998
- resolvedPage = resolvedPerPage > 0 ? Math.floor(this.offset / resolvedPerPage) + 1 : 1;
1999
- offset = this.offset;
2000
- rows = this.records;
2001
- } else {
2002
- resolvedPage = page ?? 1;
2003
- resolvedPerPage = perPage ?? (this.limit > 0 ? this.limit : 10);
2004
- offset = (resolvedPage - 1) * resolvedPerPage;
2005
- rows = this.records.slice(offset, offset + resolvedPerPage);
2006
- }
2007
- const totalPages = resolvedPerPage > 0 ? Math.max(1, Math.ceil(this.count / resolvedPerPage)) : 1;
1930
+ const perPage = this.limit > 0 ? this.limit : this.records.length;
1931
+ const page = perPage > 0 ? Math.floor(this.offset / perPage) + 1 : 1;
1932
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this.count / perPage)) : 1;
2008
1933
  return {
2009
- records: rows,
2010
- data: rows,
2011
- count: this.count,
1934
+ records: this.records,
2012
1935
  total: this.count,
2013
- limit: resolvedPerPage,
2014
- offset,
2015
- page: resolvedPage,
2016
- per_page: resolvedPerPage,
2017
- perPage: resolvedPerPage,
2018
- totalPages,
1936
+ page,
1937
+ per_page: perPage,
2019
1938
  total_pages: totalPages,
2020
- has_next: resolvedPage < totalPages,
2021
- has_prev: resolvedPage > 1
1939
+ limit: perPage,
1940
+ offset: this.offset
2022
1941
  };
2023
1942
  }
2024
1943
  /** Iterable — for (const row of result) */
@@ -3862,7 +3781,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
3862
3781
  const elapsedMs = () => performance.now() - startedAt;
3863
3782
  if (budgetMs === null) return attempt();
3864
3783
  const started = attempt();
3865
- return new Promise((resolve31, reject) => {
3784
+ return new Promise((resolve30, reject) => {
3866
3785
  let expired = false;
3867
3786
  const timer = setTimeout(() => {
3868
3787
  expired = true;
@@ -3872,7 +3791,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
3872
3791
  (arrived) => {
3873
3792
  clearTimeout(timer);
3874
3793
  if (expired) abandon?.(arrived);
3875
- else resolve31(arrived);
3794
+ else resolve30(arrived);
3876
3795
  },
3877
3796
  (failure) => {
3878
3797
  clearTimeout(timer);
@@ -4429,10 +4348,10 @@ var init_mysql = __esm({
4429
4348
  ...timeoutOption
4430
4349
  });
4431
4350
  }
4432
- return new Promise((resolve31, reject) => {
4351
+ return new Promise((resolve30, reject) => {
4433
4352
  this.connection.connect((err) => {
4434
4353
  if (err) reject(err);
4435
- else resolve31();
4354
+ else resolve30();
4436
4355
  });
4437
4356
  });
4438
4357
  },
@@ -4454,10 +4373,10 @@ var init_mysql = __esm({
4454
4373
  }
4455
4374
  }
4456
4375
  queryPromise(sql, params) {
4457
- return new Promise((resolve31, reject) => {
4376
+ return new Promise((resolve30, reject) => {
4458
4377
  this.connection.query(sql, params ?? [], (err, results) => {
4459
4378
  if (err) reject(err);
4460
- else resolve31(results);
4379
+ else resolve30(results);
4461
4380
  });
4462
4381
  });
4463
4382
  }
@@ -4827,11 +4746,11 @@ var init_mssql = __esm({
4827
4746
  };
4828
4747
  }
4829
4748
  await withConnectTimeout(
4830
- () => new Promise((resolve31, reject) => {
4749
+ () => new Promise((resolve30, reject) => {
4831
4750
  this.connection = new Connection(tediousConfig);
4832
4751
  this.connection.on("connect", (err) => {
4833
4752
  if (err) reject(err);
4834
- else resolve31();
4753
+ else resolve30();
4835
4754
  });
4836
4755
  this.connection.connect();
4837
4756
  }),
@@ -4876,11 +4795,11 @@ var init_mssql = __esm({
4876
4795
  const tediousModule = requireTedious();
4877
4796
  const Request = tediousModule.Request;
4878
4797
  const TYPES = tediousModule.TYPES;
4879
- return new Promise((resolve31, reject) => {
4798
+ return new Promise((resolve30, reject) => {
4880
4799
  const rows = [];
4881
4800
  const request = new Request(sql, (err, rowCount) => {
4882
4801
  if (err) reject(err);
4883
- else resolve31({ rows, rowCount });
4802
+ else resolve30({ rows, rowCount });
4884
4803
  });
4885
4804
  if (params) {
4886
4805
  params.forEach((p, i) => {
@@ -5083,8 +5002,8 @@ var init_mssql = __esm({
5083
5002
  throw new Error("Use startTransactionAsync() for MSSQL.");
5084
5003
  }
5085
5004
  async startTransactionAsync() {
5086
- await new Promise((resolve31, reject) => {
5087
- this.connection.beginTransaction((err) => err ? reject(err) : resolve31());
5005
+ await new Promise((resolve30, reject) => {
5006
+ this.connection.beginTransaction((err) => err ? reject(err) : resolve30());
5088
5007
  });
5089
5008
  this._inTransaction = true;
5090
5009
  }
@@ -5092,8 +5011,8 @@ var init_mssql = __esm({
5092
5011
  throw new Error("Use commitAsync() for MSSQL.");
5093
5012
  }
5094
5013
  async commitAsync() {
5095
- await new Promise((resolve31, reject) => {
5096
- this.connection.commitTransaction((err) => err ? reject(err) : resolve31());
5014
+ await new Promise((resolve30, reject) => {
5015
+ this.connection.commitTransaction((err) => err ? reject(err) : resolve30());
5097
5016
  });
5098
5017
  this._inTransaction = false;
5099
5018
  }
@@ -5101,8 +5020,8 @@ var init_mssql = __esm({
5101
5020
  throw new Error("Use rollbackAsync() for MSSQL.");
5102
5021
  }
5103
5022
  async rollbackAsync() {
5104
- await new Promise((resolve31, reject) => {
5105
- this.connection.rollbackTransaction((err) => err ? reject(err) : resolve31());
5023
+ await new Promise((resolve30, reject) => {
5024
+ this.connection.rollbackTransaction((err) => err ? reject(err) : resolve30());
5106
5025
  });
5107
5026
  this._inTransaction = false;
5108
5027
  }
@@ -5372,8 +5291,8 @@ var init_firebird = __esm({
5372
5291
  fbConfig.database = normalizeFirebirdDbIdentifier(fbConfig.database);
5373
5292
  }
5374
5293
  this.db = await withConnectTimeout(
5375
- () => new Promise((resolve31, reject) => {
5376
- fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve31(db));
5294
+ () => new Promise((resolve30, reject) => {
5295
+ fb.attach(fbConfig, (err, db) => err ? reject(err) : resolve30(db));
5377
5296
  }),
5378
5297
  connectTimeoutMillis(),
5379
5298
  fbConfig.host,
@@ -5440,20 +5359,20 @@ var init_firebird = __esm({
5440
5359
  return this.transaction ?? this.db;
5441
5360
  }
5442
5361
  queryPromise(sql, params) {
5443
- return new Promise((resolve31, reject) => {
5362
+ return new Promise((resolve30, reject) => {
5444
5363
  const translated = this.translateSql(sql);
5445
5364
  this.statementHandle().query(translated, params ?? [], (err, result) => {
5446
5365
  if (err) reject(err);
5447
- else resolve31(result ?? []);
5366
+ else resolve30(result ?? []);
5448
5367
  });
5449
5368
  });
5450
5369
  }
5451
5370
  executePromise(sql, params) {
5452
- return new Promise((resolve31, reject) => {
5371
+ return new Promise((resolve30, reject) => {
5453
5372
  const translated = this.translateSql(sql);
5454
5373
  this.statementHandle().execute(translated, params ?? [], (err) => {
5455
5374
  if (err) reject(err);
5456
- else resolve31();
5375
+ else resolve30();
5457
5376
  });
5458
5377
  });
5459
5378
  }
@@ -5569,12 +5488,12 @@ var init_firebird = __esm({
5569
5488
  }
5570
5489
  async startTransactionAsync() {
5571
5490
  this.ensureConnected();
5572
- await new Promise((resolve31, reject) => {
5491
+ await new Promise((resolve30, reject) => {
5573
5492
  this.db.transaction(0, (err, transaction) => {
5574
5493
  if (err) reject(err);
5575
5494
  else {
5576
5495
  this.transaction = transaction;
5577
- resolve31();
5496
+ resolve30();
5578
5497
  }
5579
5498
  });
5580
5499
  });
@@ -5584,12 +5503,12 @@ var init_firebird = __esm({
5584
5503
  }
5585
5504
  async commitAsync() {
5586
5505
  if (!this.transaction) throw new Error("No active transaction to commit.");
5587
- await new Promise((resolve31, reject) => {
5506
+ await new Promise((resolve30, reject) => {
5588
5507
  this.transaction.commit((err) => {
5589
5508
  if (err) reject(err);
5590
5509
  else {
5591
5510
  this.transaction = null;
5592
- resolve31();
5511
+ resolve30();
5593
5512
  }
5594
5513
  });
5595
5514
  });
@@ -5599,12 +5518,12 @@ var init_firebird = __esm({
5599
5518
  }
5600
5519
  async rollbackAsync() {
5601
5520
  if (!this.transaction) throw new Error("No active transaction to rollback.");
5602
- await new Promise((resolve31, reject) => {
5521
+ await new Promise((resolve30, reject) => {
5603
5522
  this.transaction.rollback((err) => {
5604
5523
  if (err) reject(err);
5605
5524
  else {
5606
5525
  this.transaction = null;
5607
- resolve31();
5526
+ resolve30();
5608
5527
  }
5609
5528
  });
5610
5529
  });
@@ -6665,6 +6584,7 @@ __export(database_exports, {
6665
6584
  getNamedAdapter: () => getNamedAdapter,
6666
6585
  initDatabase: () => initDatabase,
6667
6586
  parseDatabaseUrl: () => parseDatabaseUrl,
6587
+ probeTotal: () => probeTotal,
6668
6588
  resetRequestCaches: () => resetRequestCaches,
6669
6589
  resolveDbPool: () => resolveDbPool,
6670
6590
  setAdapter: () => setAdapter,
@@ -6718,6 +6638,29 @@ async function adapterCreateTable(adapter, name, columns) {
6718
6638
  if (adapter.createTableAsync) await adapter.createTableAsync(name, columns);
6719
6639
  else adapter.createTable(name, columns);
6720
6640
  }
6641
+ async function probeTotal(adapter, sql, params, limit) {
6642
+ if (limit === void 0 || limit <= 0) return void 0;
6643
+ try {
6644
+ const alias = adapter.countSubqueryAlias;
6645
+ const suffix = alias ? ` AS ${alias}` : "";
6646
+ const rows = await adapterFetch(
6647
+ adapter,
6648
+ `SELECT COUNT(*) AS tina4_total FROM (${sql}
6649
+ )${suffix}`,
6650
+ params,
6651
+ void 0,
6652
+ void 0,
6653
+ true
6654
+ );
6655
+ const row = Array.isArray(rows) ? rows[0] : void 0;
6656
+ if (!row) return void 0;
6657
+ const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
6658
+ const n = Number(value);
6659
+ return Number.isFinite(n) ? n : void 0;
6660
+ } catch {
6661
+ return void 0;
6662
+ }
6663
+ }
6721
6664
  function extractLastInsertId(result) {
6722
6665
  if (result && typeof result === "object") {
6723
6666
  const r = result;
@@ -7176,65 +7119,13 @@ var init_database = __esm({
7176
7119
  try {
7177
7120
  const rows = await adapterFetch(adapter, sql, params, limit, offset, opts?.noCache);
7178
7121
  this.lastError = null;
7179
- const total = await this.countProbe(adapter, sql, params, limit);
7122
+ const total = await probeTotal(adapter, sql, params, limit);
7180
7123
  return new DatabaseResult(rows, void 0, total, limit, offset, adapter, sql);
7181
7124
  } catch (e) {
7182
7125
  this.lastError = e?.message ?? String(e);
7183
7126
  throw e;
7184
7127
  }
7185
7128
  }
7186
- /**
7187
- * The true row count for `sql`, ignoring the pagination we appended.
7188
- *
7189
- * `count` is the TRUE TOTAL for the filter, not the number of rows this page
7190
- * returned. Node and Ruby used to populate it with `records.length` while
7191
- * Python and PHP populated it from a probe, so `db.fetch(sql).count` answered
7192
- * 20 here and 250 there for one query against one table, and every paginated
7193
- * response built on it under-reported. MEASURED 2026-08-05 on a 250-row table
7194
- * read with limit=20: Node reported total 20 over 2 pages against Python's
7195
- * 250 over 13.
7196
- *
7197
- * Only probed when a limit was actually applied. With no limit the rows
7198
- * returned ARE the whole answer for this SQL, so `records.length` is already
7199
- * the true total and a second round-trip would buy nothing — which is also
7200
- * what keeps `fetchAll()` at one query.
7201
- *
7202
- * BEST EFFORT, and it can never mask a real failure: it runs AFTER the main
7203
- * query (which has already thrown on bad SQL) and returns undefined on any
7204
- * error. `undefined` — not 0 — is the miss value, so DatabaseResult falls
7205
- * back to records.length, a true lower bound. Reporting 0 next to 100 real
7206
- * records would be the same "states a wrong number authoritatively" defect
7207
- * this change exists to remove.
7208
- *
7209
- * The closing paren goes on its OWN LINE: appended inline, a trailing
7210
- * `-- comment` in the caller's SQL comments it out and the probe dies with
7211
- * "incomplete input". Postgres, MySQL and MSSQL additionally require a name
7212
- * for the derived table; SQLite and Firebird do not, and Firebird rejects
7213
- * `AS` there — so the alias comes from the adapter, not an assumption.
7214
- */
7215
- async countProbe(adapter, sql, params, limit) {
7216
- if (limit === void 0 || limit <= 0) return void 0;
7217
- try {
7218
- const alias = adapter.countSubqueryAlias;
7219
- const suffix = alias ? ` AS ${alias}` : "";
7220
- const rows = await adapterFetch(
7221
- adapter,
7222
- `SELECT COUNT(*) AS tina4_total FROM (${sql}
7223
- )${suffix}`,
7224
- params,
7225
- void 0,
7226
- void 0,
7227
- true
7228
- );
7229
- const row = Array.isArray(rows) ? rows[0] : void 0;
7230
- if (!row) return void 0;
7231
- const value = row["tina4_total"] ?? row["TINA4_TOTAL"] ?? Object.values(row)[0];
7232
- const n = Number(value);
7233
- return Number.isFinite(n) ? n : void 0;
7234
- } catch {
7235
- return void 0;
7236
- }
7237
- }
7238
7129
  /**
7239
7130
  * Fetch a single row or null.
7240
7131
  *
@@ -7946,6 +7837,10 @@ async function discoverModels(modelsDir) {
7946
7837
  }
7947
7838
  const definition = {
7948
7839
  tableName: ModelClass.tableName,
7840
+ // The class name is the type name a generated OpenAPI client wants
7841
+ // (`Item`, not `items`). Carry it so Swagger keys components.schemas by
7842
+ // it. A model exported as `default` keeps its declared class name here.
7843
+ className: typeof ModelClass.name === "string" && ModelClass.name ? ModelClass.name : void 0,
7949
7844
  fields: ModelClass.fields,
7950
7845
  fieldMapping: ModelClass.fieldMapping,
7951
7846
  softDelete: ModelClass.softDelete ?? false,
@@ -8560,7 +8455,13 @@ async function status(adapter, options) {
8560
8455
  return result;
8561
8456
  }
8562
8457
  async function createMigration(description, options) {
8563
- if (options?.kind === "class") {
8458
+ const kind = (options?.kind ?? "sql").trim().toLowerCase();
8459
+ if (!["sql", "code", "class"].includes(kind)) {
8460
+ throw new Error(
8461
+ `Unknown migration kind "${kind}". Use "sql" (default) or "code" (alias: "class"). An unrecognised kind used to produce a .sql file silently, which is why this now throws.`
8462
+ );
8463
+ }
8464
+ if (kind === "code" || kind === "class") {
8564
8465
  return createClassMigration(description, options);
8565
8466
  }
8566
8467
  const dir = resolve4(options?.migrationsDir ?? "migrations");
@@ -8687,15 +8588,13 @@ var init_migration = __esm({
8687
8588
  * Scaffold a new migration file.
8688
8589
  *
8689
8590
  * kind="sql" — creates {timestamp}_{description}.sql + .down.sql (default)
8690
- * kind="class" — creates {timestamp}_{description}.ts with a TypeScript class template
8591
+ * kind="code" — creates {timestamp}_{description}.ts with a TypeScript class
8592
+ * template. "class" is accepted as a legacy alias.
8691
8593
  *
8692
8594
  * Returns the path to the created up file (or class file).
8693
8595
  */
8694
8596
  async create(description, kind = "sql") {
8695
- if (kind === "class") {
8696
- return createClassMigration(description, { migrationsDir: this.dir });
8697
- }
8698
- return createMigration(description, { migrationsDir: this.dir });
8597
+ return createMigration(description, { migrationsDir: this.dir, kind });
8699
8598
  }
8700
8599
  /** Return list of completed (applied) migration filenames. */
8701
8600
  async getApplied() {
@@ -8938,15 +8837,8 @@ function generateCrudRoutes(models, options = {}) {
8938
8837
  const total = Number(countRow[0]?.total ?? 0);
8939
8838
  const limit = qp.limit ?? 100;
8940
8839
  const page = qp.page ?? 1;
8941
- res.json({
8942
- data: rows,
8943
- meta: {
8944
- total,
8945
- page,
8946
- limit,
8947
- totalPages: Math.ceil(total / limit)
8948
- }
8949
- });
8840
+ const offset = (page - 1) * limit;
8841
+ res.json(new DatabaseResult(rows, void 0, total, limit, offset).toPaginate());
8950
8842
  }
8951
8843
  });
8952
8844
  routes.push({
@@ -9100,6 +8992,7 @@ var init_autoCrud = __esm({
9100
8992
  "../orm/src/autoCrud.ts"() {
9101
8993
  "use strict";
9102
8994
  init_database();
8995
+ init_databaseResult();
9103
8996
  init_query();
9104
8997
  init_validation();
9105
8998
  AutoCrud = class _AutoCrud {
@@ -9354,17 +9247,19 @@ var init_queryBuilder = __esm({
9354
9247
  this.ensureDb();
9355
9248
  const sql = this.toSql();
9356
9249
  const allParams = [...this.params, ...this.havingParams];
9250
+ const queryParams = allParams.length > 0 ? allParams : void 0;
9357
9251
  const rows = await adapterFetch(
9358
9252
  this.db,
9359
9253
  sql,
9360
- allParams.length > 0 ? allParams : void 0,
9254
+ queryParams,
9361
9255
  this.limitVal,
9362
9256
  this.offsetVal
9363
9257
  );
9258
+ const total = await probeTotal(this.db, sql, queryParams, this.limitVal);
9364
9259
  return new DatabaseResult(
9365
9260
  rows,
9366
9261
  void 0,
9367
- void 0,
9262
+ total,
9368
9263
  this.limitVal,
9369
9264
  this.offsetVal,
9370
9265
  this.db,
@@ -12941,7 +12836,6 @@ __export(src_exports, {
12941
12836
  DatabaseUrl: () => DatabaseUrl,
12942
12837
  DocStoreDriverMissing: () => DocStoreDriverMissing,
12943
12838
  FakeData: () => FakeData2,
12944
- FetchResult: () => FetchResult,
12945
12839
  FirebirdAdapter: () => FirebirdAdapter,
12946
12840
  InvalidId: () => InvalidId,
12947
12841
  LocalStorage: () => LocalStorage,
@@ -13041,7 +12935,6 @@ __export(src_exports, {
13041
12935
  var init_src = __esm({
13042
12936
  "../orm/src/index.ts"() {
13043
12937
  "use strict";
13044
- init_types();
13045
12938
  init_databaseResult();
13046
12939
  init_database();
13047
12940
  init_database();
@@ -13451,7 +13344,7 @@ ${s}\r
13451
13344
  connect() {
13452
13345
  if (this.connected) return Promise.resolve();
13453
13346
  if (this.connecting) return this.connecting;
13454
- this.connecting = new Promise((resolve31, reject) => {
13347
+ this.connecting = new Promise((resolve30, reject) => {
13455
13348
  const sock = net.createConnection({ host: this.host, port: this.port });
13456
13349
  sock.setNoDelay(true);
13457
13350
  const onError = (err) => {
@@ -13488,7 +13381,7 @@ ${s}\r
13488
13381
  sock.on("error", (e) => {
13489
13382
  this.brokenError = e;
13490
13383
  });
13491
- resolve31();
13384
+ resolve30();
13492
13385
  } catch (e) {
13493
13386
  onError(e);
13494
13387
  }
@@ -13551,12 +13444,12 @@ ${s}\r
13551
13444
  }
13552
13445
  /** Send one command and await its reply (assumes socket is up). */
13553
13446
  raw(args) {
13554
- return new Promise((resolve31, reject) => {
13447
+ return new Promise((resolve30, reject) => {
13555
13448
  if (!this.sock || this.sock.destroyed) {
13556
13449
  reject(this.brokenError ?? new Error("redis socket not connected"));
13557
13450
  return;
13558
13451
  }
13559
- this.waiters.push({ resolve: resolve31, reject });
13452
+ this.waiters.push({ resolve: resolve30, reject });
13560
13453
  this.sock.write(_RespClient.encode(args));
13561
13454
  });
13562
13455
  }
@@ -13898,7 +13791,7 @@ ${s}\r
13898
13791
  connect() {
13899
13792
  if (this.connected) return Promise.resolve();
13900
13793
  if (this.connecting) return this.connecting;
13901
- this.connecting = new Promise((resolve31, reject) => {
13794
+ this.connecting = new Promise((resolve30, reject) => {
13902
13795
  const sock = net.createConnection({ host: this.host, port: this.port });
13903
13796
  sock.setNoDelay(true);
13904
13797
  sock.once("error", (err) => {
@@ -13919,7 +13812,7 @@ ${s}\r
13919
13812
  p.resolve(this.buffer.toString("utf-8"));
13920
13813
  }
13921
13814
  });
13922
- resolve31();
13815
+ resolve30();
13923
13816
  });
13924
13817
  });
13925
13818
  return this.connecting;
@@ -13952,13 +13845,13 @@ ${s}\r
13952
13845
  async send(payload, terminator) {
13953
13846
  await this.connect();
13954
13847
  if (!this.sock || this.sock.destroyed) return "";
13955
- return new Promise((resolve31) => {
13848
+ return new Promise((resolve30) => {
13956
13849
  this.buffer = Buffer.alloc(0);
13957
- this.pending = { terminator, resolve: resolve31 };
13850
+ this.pending = { terminator, resolve: resolve30 };
13958
13851
  const timer = setTimeout(() => {
13959
- if (this.pending && this.pending.resolve === resolve31) {
13852
+ if (this.pending && this.pending.resolve === resolve30) {
13960
13853
  this.pending = null;
13961
- resolve31(this.buffer.toString("utf-8"));
13854
+ resolve30(this.buffer.toString("utf-8"));
13962
13855
  }
13963
13856
  }, 4e3);
13964
13857
  if (timer.unref) timer.unref();
@@ -15484,16 +15377,27 @@ async function parseBody(req2) {
15484
15377
  }
15485
15378
  const contentType = req2.headers["content-type"] ?? "";
15486
15379
  const chunks = [];
15487
- await new Promise((resolve31, reject) => {
15488
- req2.on("data", (chunk) => chunks.push(chunk));
15489
- req2.on("end", resolve31);
15380
+ await new Promise((resolve30, reject) => {
15381
+ let received = 0;
15382
+ let refused = false;
15383
+ req2.on("data", (chunk) => {
15384
+ if (refused) return;
15385
+ received += chunk.length;
15386
+ if (received > TINA4_MAX_UPLOAD_SIZE) {
15387
+ refused = true;
15388
+ chunks.length = 0;
15389
+ reject(new PayloadTooLargeError(received, TINA4_MAX_UPLOAD_SIZE));
15390
+ return;
15391
+ }
15392
+ chunks.push(chunk);
15393
+ });
15394
+ req2.on("end", () => {
15395
+ if (!refused) resolve30();
15396
+ });
15490
15397
  req2.on("error", reject);
15491
15398
  });
15492
15399
  const raw = Buffer.concat(chunks);
15493
15400
  if (raw.length === 0) return;
15494
- if (raw.length > TINA4_MAX_UPLOAD_SIZE) {
15495
- throw new PayloadTooLargeError(raw.length, TINA4_MAX_UPLOAD_SIZE);
15496
- }
15497
15401
  if (contentType.includes("multipart/form-data")) {
15498
15402
  const boundary = extractBoundary(contentType);
15499
15403
  if (boundary) {
@@ -23327,14 +23231,14 @@ data: ${channel.buffer.shift()}
23327
23231
  `;
23328
23232
  continue;
23329
23233
  }
23330
- const gotMessage = await new Promise((resolve31) => {
23234
+ const gotMessage = await new Promise((resolve30) => {
23331
23235
  const timer = setTimeout(() => {
23332
23236
  channel.wake = null;
23333
- resolve31(false);
23237
+ resolve30(false);
23334
23238
  }, keepaliveMs);
23335
23239
  channel.wake = () => {
23336
23240
  clearTimeout(timer);
23337
- resolve31(true);
23241
+ resolve30(true);
23338
23242
  };
23339
23243
  });
23340
23244
  if (!gotMessage) yield `: keep-alive
@@ -25849,7 +25753,7 @@ var init_websocket = __esm({
25849
25753
  * Start the WebSocket server.
25850
25754
  */
25851
25755
  async start() {
25852
- return new Promise((resolve31, reject) => {
25756
+ return new Promise((resolve30, reject) => {
25853
25757
  this.server = createServer((req2, res) => {
25854
25758
  res.writeHead(426, { "Content-Type": "text/plain" });
25855
25759
  res.end("Upgrade Required");
@@ -25859,7 +25763,7 @@ var init_websocket = __esm({
25859
25763
  });
25860
25764
  this.server.listen(this.port, () => {
25861
25765
  this.startIdleReaper();
25862
- resolve31();
25766
+ resolve30();
25863
25767
  });
25864
25768
  this.server.on("error", (err) => {
25865
25769
  this.emit("error", err);
@@ -26423,7 +26327,7 @@ var init_websocket = __esm({
26423
26327
  client.trackerId = this.onAdd(socket.remoteAddress ?? "unknown", "/__dev_reload");
26424
26328
  }
26425
26329
  this.clients.add(client);
26426
- const cleanup2 = () => {
26330
+ const cleanup = () => {
26427
26331
  if (!this.clients.has(client)) return;
26428
26332
  this.clients.delete(client);
26429
26333
  if (client.trackerId && this.onRemove) this.onRemove(client.trackerId);
@@ -26446,13 +26350,13 @@ var init_websocket = __esm({
26446
26350
  socket.end();
26447
26351
  } catch {
26448
26352
  }
26449
- cleanup2();
26353
+ cleanup();
26450
26354
  return;
26451
26355
  }
26452
26356
  }
26453
26357
  });
26454
- socket.on("close", cleanup2);
26455
- socket.on("error", cleanup2);
26358
+ socket.on("close", cleanup);
26359
+ socket.on("error", cleanup);
26456
26360
  return true;
26457
26361
  }
26458
26362
  /**
@@ -27970,7 +27874,7 @@ var init_queue = __esm({
27970
27874
  const jobs = this.popBatch(resolvedBatchSize);
27971
27875
  if (jobs.length === 0) {
27972
27876
  if (resolvedPollInterval <= 0) break;
27973
- await new Promise((resolve31) => setTimeout(resolve31, resolvedPollInterval));
27877
+ await new Promise((resolve30) => setTimeout(resolve30, resolvedPollInterval));
27974
27878
  continue;
27975
27879
  }
27976
27880
  yield jobs;
@@ -27980,7 +27884,7 @@ var init_queue = __esm({
27980
27884
  const raw = this.pop();
27981
27885
  if (raw === null) {
27982
27886
  if (resolvedPollInterval <= 0) break;
27983
- await new Promise((resolve31) => setTimeout(resolve31, resolvedPollInterval));
27887
+ await new Promise((resolve30) => setTimeout(resolve30, resolvedPollInterval));
27984
27888
  continue;
27985
27889
  }
27986
27890
  yield createJob(raw, this);
@@ -32500,23 +32404,23 @@ var init_devAdmin = __esm({
32500
32404
  });
32501
32405
  };
32502
32406
  handleDevAdminJs = async (_req, res) => {
32503
- const { readFileSync: readFileSync29, existsSync: existsSync37 } = await import("node:fs");
32504
- const { dirname: dirname16, join: join39, resolve: resolve31 } = await import("node:path");
32407
+ const { readFileSync: readFileSync28, existsSync: existsSync36 } = await import("node:fs");
32408
+ const { dirname: dirname15, join: join38, resolve: resolve30 } = await import("node:path");
32505
32409
  const { fileURLToPath: fileURLToPath9 } = await import("node:url");
32506
- const dir = dirname16(fileURLToPath9(import.meta.url));
32410
+ const dir = dirname15(fileURLToPath9(import.meta.url));
32507
32411
  const candidates = [
32508
- join39(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
32412
+ join38(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
32509
32413
  // src/../public/js/
32510
- join39(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
32414
+ join38(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
32511
32415
  // deeper nesting
32512
- resolve31(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
32513
- resolve31(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
32416
+ resolve30(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
32417
+ resolve30(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
32514
32418
  // project public/
32515
32419
  ];
32516
32420
  for (const jsPath of candidates) {
32517
- if (existsSync37(jsPath)) {
32421
+ if (existsSync36(jsPath)) {
32518
32422
  try {
32519
- const content = readFileSync29(jsPath, "utf-8");
32423
+ const content = readFileSync28(jsPath, "utf-8");
32520
32424
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
32521
32425
  res.raw.end(content);
32522
32426
  return;
@@ -32977,8 +32881,12 @@ function sanitizeSecurity(reqs, schemes) {
32977
32881
  function generate(routes, models = []) {
32978
32882
  const info = {
32979
32883
  title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
32980
- version: process.env.TINA4_SWAGGER_VERSION ?? "0.0.1",
32981
- description: process.env.TINA4_SWAGGER_DESCRIPTION ?? "Auto-generated API documentation"
32884
+ // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
32885
+ // 0.0.1). description defaults to the empty string, not a canned sentence.
32886
+ // Both are the settled cross-framework defaults (parity with the Python
32887
+ // master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
32888
+ version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
32889
+ description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
32982
32890
  };
32983
32891
  const contactEmail = (process.env.TINA4_SWAGGER_CONTACT_EMAIL ?? "").trim();
32984
32892
  const contactName = (process.env.TINA4_SWAGGER_CONTACT_TEAM ?? "").trim();
@@ -33011,9 +32919,11 @@ function generate(routes, models = []) {
33011
32919
  const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
33012
32920
  const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
33013
32921
  const refSchemas = /* @__PURE__ */ new Set();
32922
+ const tableToSchema = /* @__PURE__ */ new Map();
33014
32923
  for (const model of models) {
33015
- const schema = modelToSchema(model);
33016
- spec.components.schemas[model.tableName] = schema;
32924
+ const schemaKey = schemaNameForModel(model);
32925
+ tableToSchema.set(model.tableName, schemaKey);
32926
+ spec.components.schemas[schemaKey] = modelToSchema(model);
33017
32927
  }
33018
32928
  const usedTags = [];
33019
32929
  const seenIds = /* @__PURE__ */ new Set();
@@ -33040,11 +32950,11 @@ function generate(routes, models = []) {
33040
32950
  if (route.meta?.deprecated) operation.deprecated = true;
33041
32951
  const pathParams = extractPathParams(route.pattern);
33042
32952
  if (pathParams.length > 0) {
33043
- operation.parameters = pathParams.map((name) => ({
32953
+ operation.parameters = pathParams.map(({ name, schema }) => ({
33044
32954
  name,
33045
32955
  in: "path",
33046
32956
  required: true,
33047
- schema: { type: "string" }
32957
+ schema
33048
32958
  }));
33049
32959
  }
33050
32960
  if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
@@ -33070,19 +32980,20 @@ function generate(routes, models = []) {
33070
32980
  };
33071
32981
  } else if (method === "post" || method === "put") {
33072
32982
  const modelName = inferModelFromPath(route.pattern);
33073
- if (modelName && models.some((m) => m.tableName === modelName)) {
33074
- const media = {
33075
- schema: { $ref: `#/components/schemas/${modelName}` }
33076
- };
32983
+ const schemaKey = modelName ? tableToSchema.get(modelName) : void 0;
32984
+ if (schemaKey) {
32985
+ const sref = `#/components/schemas/${schemaKey}`;
32986
+ const media = { schema: { $ref: sref } };
33077
32987
  if (route.meta?.example !== void 0) media.example = route.meta.example;
33078
32988
  operation.requestBody = {
33079
32989
  required: true,
33080
32990
  content: { "application/json": media }
33081
32991
  };
33082
- operation.responses = {
33083
- ...method === "post" ? { "201": { description: "Created", content: { "application/json": { schema: { $ref: `#/components/schemas/${modelName}` } } } } } : { "200": { description: "Updated", content: { "application/json": { schema: { $ref: `#/components/schemas/${modelName}` } } } } },
33084
- "422": { description: "Validation failed" }
33085
- };
32992
+ if (route.meta?.responses === void 0) {
32993
+ operation.responses = {
32994
+ "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
32995
+ };
32996
+ }
33086
32997
  } else if (route.meta?.example !== void 0) {
33087
32998
  operation.requestBody = {
33088
32999
  content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
@@ -33169,6 +33080,21 @@ function resolveServers() {
33169
33080
  const dev = (process.env.SWAGGER_DEV_URL ?? "").trim();
33170
33081
  return dev.length > 0 ? [{ url: dev }] : [{ url: "/" }];
33171
33082
  }
33083
+ function schemaNameForModel(model) {
33084
+ const explicit = model.className?.trim();
33085
+ if (explicit) return explicit;
33086
+ return deriveClassName(model.tableName);
33087
+ }
33088
+ function deriveClassName(tableName) {
33089
+ return singularize(tableName).split(/[_\s-]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || tableName;
33090
+ }
33091
+ function singularize(word) {
33092
+ if (/ies$/i.test(word) && word.length > 3) return word.slice(0, -3) + "y";
33093
+ if (/(ses|xes|zes|ches|shes)$/i.test(word)) return word.slice(0, -2);
33094
+ if (/ss$/i.test(word)) return word;
33095
+ if (/s$/i.test(word) && word.length > 1) return word.slice(0, -1);
33096
+ return word;
33097
+ }
33172
33098
  function modelToSchema(model) {
33173
33099
  const properties = {};
33174
33100
  const required = [];
@@ -33242,15 +33168,35 @@ function inferSchema(value) {
33242
33168
  if (typeof value === "number") return { type: Number.isInteger(value) ? "integer" : "number" };
33243
33169
  return { type: "string" };
33244
33170
  }
33171
+ function segmentParam(segment) {
33172
+ if (segment.startsWith("{") && segment.endsWith("}")) {
33173
+ const inner = segment.slice(1, -1);
33174
+ if (inner.startsWith("...")) return { name: inner.slice(3), type: "string" };
33175
+ const colon = inner.indexOf(":");
33176
+ if (colon >= 0) return { name: inner.slice(0, colon), type: inner.slice(colon + 1) };
33177
+ return { name: inner, type: "string" };
33178
+ }
33179
+ if (segment.startsWith("[") && segment.endsWith("]")) {
33180
+ const inner = segment.slice(1, -1);
33181
+ return { name: inner.startsWith("...") ? inner.slice(3) : inner, type: "string" };
33182
+ }
33183
+ if (segment.startsWith(":") && segment.length > 1) {
33184
+ return { name: segment.slice(1), type: "string" };
33185
+ }
33186
+ return null;
33187
+ }
33245
33188
  function patternToOpenAPI(pattern) {
33246
- return pattern.replace(/\[\.\.\.(\w+)\]/g, "{$1}").replace(/\[(\w+)\]/g, "{$1}");
33189
+ return pattern.split("/").map((segment) => {
33190
+ const p = segmentParam(segment);
33191
+ return p ? `{${p.name}}` : segment;
33192
+ }).join("/");
33247
33193
  }
33248
33194
  function extractPathParams(pattern) {
33249
33195
  const params = [];
33250
- const regex = /\[(?:\.\.\.)?(\w+)\]/g;
33251
- let match;
33252
- while ((match = regex.exec(pattern)) !== null) {
33253
- params.push(match[1]);
33196
+ for (const segment of pattern.split("/")) {
33197
+ const p = segmentParam(segment);
33198
+ if (!p) continue;
33199
+ params.push({ name: p.name, schema: { ...PARAM_TYPE_SCHEMA[p.type] ?? { type: "string" } } });
33254
33200
  }
33255
33201
  return params;
33256
33202
  }
@@ -33272,8 +33218,12 @@ function inferModelFromPath(pattern) {
33272
33218
  if (rest.length === 1 && /^[[{]\.{0,3}\w+[\]}]$/.test(rest[0])) return candidate;
33273
33219
  return null;
33274
33220
  }
33221
+ function operationIdBase(method, openApiPath) {
33222
+ const clean = openApiPath.replace(/^\/+|\/+$/g, "").replace(/\//g, "_").replace(/\.\.\./g, "").replace(/[{}]/g, "").replace(/\*/g, "wildcard");
33223
+ return clean ? `${method}_${clean}` : method;
33224
+ }
33275
33225
  function uniqueOperationId(method, openApiPath, seen) {
33276
- const base = (method + openApiPath.replace(/[/{}]/g, "_")).replace(/_+/g, "_").replace(/_$/, "");
33226
+ const base = operationIdBase(method, openApiPath);
33277
33227
  let oid = base;
33278
33228
  let n = 2;
33279
33229
  while (seen.has(oid)) {
@@ -33283,13 +33233,25 @@ function uniqueOperationId(method, openApiPath, seen) {
33283
33233
  seen.add(oid);
33284
33234
  return oid;
33285
33235
  }
33286
- var WRITE_METHODS, registeredSchemes, registeredSchemas;
33236
+ var WRITE_METHODS, registeredSchemes, registeredSchemas, PARAM_TYPE_SCHEMA;
33287
33237
  var init_generator = __esm({
33288
33238
  "../swagger/src/generator.ts"() {
33289
33239
  "use strict";
33290
33240
  WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
33291
33241
  registeredSchemes = {};
33292
33242
  registeredSchemas = {};
33243
+ PARAM_TYPE_SCHEMA = {
33244
+ int: { type: "integer" },
33245
+ integer: { type: "integer" },
33246
+ float: { type: "number" },
33247
+ number: { type: "number" },
33248
+ uuid: { type: "string", format: "uuid" },
33249
+ slug: { type: "string", pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
33250
+ alpha: { type: "string", pattern: "^[A-Za-z]+$" },
33251
+ alnum: { type: "string", pattern: "^[A-Za-z0-9]+$" },
33252
+ path: { type: "string" },
33253
+ string: { type: "string" }
33254
+ };
33293
33255
  }
33294
33256
  });
33295
33257
 
@@ -33609,10 +33571,29 @@ function openBrowser(url) {
33609
33571
  }, 2e3);
33610
33572
  }
33611
33573
  function resolvePortAndHost(config) {
33612
- const port = config?.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? 7148;
33574
+ const tina4Port = process.env.TINA4_PORT;
33575
+ const legacyPort = process.env.PORT;
33576
+ let port;
33577
+ if (config?.port !== void 0) {
33578
+ port = config.port;
33579
+ } else if (tina4Port && /^\d+$/.test(tina4Port)) {
33580
+ port = parseInt(tina4Port, 10);
33581
+ } else if (legacyPort && /^\d+$/.test(legacyPort)) {
33582
+ port = parseInt(legacyPort, 10);
33583
+ warnDeprecatedPort(port);
33584
+ } else {
33585
+ port = 7148;
33586
+ }
33613
33587
  const host = config?.host ?? process.env.TINA4_HOST ?? process.env.HOST ?? "0.0.0.0";
33614
33588
  return { port, host };
33615
33589
  }
33590
+ function warnDeprecatedPort(port) {
33591
+ if (portDeprecationWarned) return;
33592
+ portDeprecationWarned = true;
33593
+ Log.warning(
33594
+ `PORT is deprecated and will be removed in 3.14 - use TINA4_PORT instead (binding port ${port} from PORT)`
33595
+ );
33596
+ }
33616
33597
  function isBannerSuppressed() {
33617
33598
  return isTruthy(process.env.TINA4_SUPPRESS);
33618
33599
  }
@@ -33884,6 +33865,29 @@ function deployGallery(name) {
33884
33865
  </body>
33885
33866
  </html>`;
33886
33867
  }
33868
+ function startLoopWatchdog() {
33869
+ const raw = (process.env.TINA4_LOOP_LAG_WARN_MS ?? "").trim();
33870
+ const threshold = /^\d+$/.test(raw) ? parseInt(raw, 10) : 250;
33871
+ if (threshold <= 0) {
33872
+ return { stop: () => {
33873
+ } };
33874
+ }
33875
+ let last = Date.now();
33876
+ let warned = 0;
33877
+ const timer = setInterval(() => {
33878
+ const now = Date.now();
33879
+ const lag = now - last - LOOP_WATCHDOG_TICK_MS;
33880
+ last = now;
33881
+ if (lag < threshold) return;
33882
+ warned++;
33883
+ if (warned > 5 && warned % 20 !== 0) return;
33884
+ Log.warning(
33885
+ `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.`
33886
+ );
33887
+ }, LOOP_WATCHDOG_TICK_MS);
33888
+ timer.unref();
33889
+ return { stop: () => clearInterval(timer) };
33890
+ }
33887
33891
  async function start(config) {
33888
33892
  const isManaged = process.argv.includes("--managed");
33889
33893
  if (!isManaged && process.env.TINA4_OVERRIDE_CLIENT !== "true") {
@@ -34133,7 +34137,9 @@ async function startServer(config) {
34133
34137
  const resolved = resolvePortAndHost(config);
34134
34138
  const host = resolved.host;
34135
34139
  let port = resolved.port;
34136
- port = findAvailablePort(port);
34140
+ if (!cluster.isWorker) {
34141
+ port = findAvailablePort(port);
34142
+ }
34137
34143
  const isProduction = (process.env.TINA4_PRODUCTION ?? "").toLowerCase() === "true";
34138
34144
  if (cluster.isPrimary && isProduction) {
34139
34145
  const numCPUs = os2.cpus().length;
@@ -34349,7 +34355,20 @@ ${reset2}
34349
34355
  await sessionAutoStart(rawReq, rawRes, req2);
34350
34356
  await middleware.run(req2, res);
34351
34357
  if (res.raw.writableEnded) return;
34352
- await req2.parseBody();
34358
+ try {
34359
+ await req2.parseBody();
34360
+ } catch (err) {
34361
+ const status2 = err?.statusCode;
34362
+ if (typeof status2 === "number" && status2 >= 400 && status2 < 500) {
34363
+ if (!rawRes.writableEnded) {
34364
+ rawRes.statusCode = status2;
34365
+ rawRes.setHeader("content-type", "application/json");
34366
+ rawRes.end(JSON.stringify({ error: err.message }));
34367
+ }
34368
+ return;
34369
+ }
34370
+ throw err;
34371
+ }
34353
34372
  const pathname = req2.path;
34354
34373
  const reqStartTime = DevAdmin.isEnabled() ? Date.now() : 0;
34355
34374
  const matchedPattern = { value: "" };
@@ -34548,8 +34567,10 @@ ${reset2}
34548
34567
  };
34549
34568
  process.on("SIGTERM", onSigterm);
34550
34569
  process.on("SIGINT", onSigint);
34570
+ const loopWatchdog = startLoopWatchdog();
34551
34571
  resolvePromise({
34552
34572
  close: () => {
34573
+ loopWatchdog.stop();
34553
34574
  process.off("SIGTERM", onSigterm);
34554
34575
  process.off("SIGINT", onSigint);
34555
34576
  stopAllBackgroundTasks();
@@ -34564,7 +34585,7 @@ ${reset2}
34564
34585
  });
34565
34586
  });
34566
34587
  }
34567
- var __filename, __dirname, BUILTIN_ERROR_TEMPLATES_DIR, BUILTIN_PUBLIC_DIR, swaggerAssetsEnabled, DEFAULT_SHUTDOWN_TIMEOUT_SECONDS, TINA4_VERSION2, frondCache, _LEGACY_ENV_VARS, TEMPLATE_PAGES_DIR, HTTP_REASON_PHRASES, templateCache, _dispatchFn, _serverHandle, FALLBACK_STAGES;
34588
+ var __filename, __dirname, BUILTIN_ERROR_TEMPLATES_DIR, BUILTIN_PUBLIC_DIR, swaggerAssetsEnabled, DEFAULT_SHUTDOWN_TIMEOUT_SECONDS, TINA4_VERSION2, frondCache, _LEGACY_ENV_VARS, portDeprecationWarned, TEMPLATE_PAGES_DIR, HTTP_REASON_PHRASES, templateCache, _dispatchFn, _serverHandle, LOOP_WATCHDOG_TICK_MS, FALLBACK_STAGES;
34568
34589
  var init_server = __esm({
34569
34590
  "../core/src/server.ts"() {
34570
34591
  "use strict";
@@ -34617,6 +34638,7 @@ var init_server = __esm({
34617
34638
  SWAGGER_VERSION: "TINA4_SWAGGER_VERSION",
34618
34639
  ORM_PLURAL_TABLE_NAMES: "TINA4_ORM_PLURAL_TABLE_NAMES"
34619
34640
  };
34641
+ portDeprecationWarned = false;
34620
34642
  TEMPLATE_PAGES_DIR = "pages";
34621
34643
  HTTP_REASON_PHRASES = {
34622
34644
  100: "Continue",
@@ -34653,6 +34675,7 @@ var init_server = __esm({
34653
34675
  templateCache = null;
34654
34676
  _dispatchFn = null;
34655
34677
  _serverHandle = null;
34678
+ LOOP_WATCHDOG_TICK_MS = 100;
34656
34679
  FALLBACK_STAGES = [
34657
34680
  serveTemplateFallback,
34658
34681
  serveLandingPage,
@@ -34740,433 +34763,6 @@ var init_env = __esm({
34740
34763
  }
34741
34764
  });
34742
34765
 
34743
- // ../core/src/scss.ts
34744
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync19, readdirSync as readdirSync18 } from "node:fs";
34745
- import { join as join28, resolve as resolve20, dirname as dirname13 } from "node:path";
34746
- function compileString(scss, importPaths, variables) {
34747
- const imported = /* @__PURE__ */ new Set();
34748
- scss = resolveImports(scss, importPaths, imported);
34749
- scss = scss.replace(/(?<![:"'])\/\/[^\n]*/g, "");
34750
- scss = extractVariables(scss, variables);
34751
- const mixins = {};
34752
- scss = extractMixins(scss, mixins);
34753
- scss = resolveIncludes(scss, mixins);
34754
- scss = resolveInterpolation(scss, variables);
34755
- scss = substituteVariables(scss, variables);
34756
- scss = evalMath(scss);
34757
- scss = resolveColorFunctions(scss);
34758
- const css = flattenNesting(scss);
34759
- return cleanup(css);
34760
- }
34761
- function resolveImports(content, paths, imported) {
34762
- return content.replace(/@import\s+["']?([^"';\n]+)["']?\s*;/g, (_match, name) => {
34763
- name = name.trim();
34764
- const candidates = [];
34765
- for (const base of paths) {
34766
- candidates.push(
34767
- join28(base, `${name}.scss`),
34768
- join28(base, `_${name}.scss`),
34769
- join28(base, name)
34770
- );
34771
- }
34772
- for (const candidate of candidates) {
34773
- if (existsSync25(candidate) && !imported.has(candidate)) {
34774
- imported.add(candidate);
34775
- const fileContent = readFileSync23(candidate, "utf-8");
34776
- return resolveImports(fileContent, [dirname13(candidate), ...paths], imported);
34777
- }
34778
- }
34779
- return `/* IMPORT NOT FOUND: ${name} */`;
34780
- });
34781
- }
34782
- function stripVariableFlags(value) {
34783
- let declaresDefault = false;
34784
- for (; ; ) {
34785
- const match = VARIABLE_FLAG.exec(value);
34786
- if (match === null) return [value.trim(), declaresDefault];
34787
- if (match[1] === "default") declaresDefault = true;
34788
- value = value.slice(0, match.index);
34789
- }
34790
- }
34791
- function extractVariables(scss, variables) {
34792
- return scss.replace(/\$([a-zA-Z_][\w-]*)\s*:\s*([^;]+);/g, (_m, name, value) => {
34793
- const [stripped, declaresDefault] = stripVariableFlags(value.trim());
34794
- if (declaresDefault && (variables[name] ?? "null") !== "null") {
34795
- return "";
34796
- }
34797
- let resolved = stripped;
34798
- for (const [vName, vVal] of Object.entries(variables)) {
34799
- resolved = resolved.replaceAll(`$${vName}`, vVal);
34800
- }
34801
- variables[name] = resolved;
34802
- return "";
34803
- });
34804
- }
34805
- function substituteVariables(scss, variables) {
34806
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
34807
- for (const name of sorted) {
34808
- scss = scss.replaceAll(`$${name}`, variables[name]);
34809
- }
34810
- return scss;
34811
- }
34812
- function resolveInterpolation(scss, variables) {
34813
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
34814
- return scss.replace(/#\{([^{}]*)\}/g, (_m, inner) => {
34815
- let resolved = inner.trim();
34816
- for (const name of sorted) {
34817
- resolved = resolved.replaceAll(`$${name}`, variables[name]);
34818
- }
34819
- return resolved;
34820
- });
34821
- }
34822
- function extractMixins(scss, mixins) {
34823
- const pattern = /@mixin\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*\{/g;
34824
- let match;
34825
- const locations = [];
34826
- while ((match = pattern.exec(scss)) !== null) {
34827
- const name = match[1];
34828
- const paramsStr = match[2] ?? "";
34829
- const params = paramsStr.split(",").map((p) => p.trim().replace(/^\$/, "")).filter(Boolean);
34830
- const bodyStart = match.index + match[0].length;
34831
- const body = findBlock(scss, bodyStart);
34832
- if (body !== null) {
34833
- mixins[name] = { params, body };
34834
- locations.push({
34835
- start: match.index,
34836
- end: bodyStart + body.length + 1,
34837
- name
34838
- });
34839
- }
34840
- }
34841
- let result = scss;
34842
- for (const loc of locations.reverse()) {
34843
- result = result.slice(0, loc.start) + result.slice(loc.end);
34844
- }
34845
- return result;
34846
- }
34847
- function resolveIncludes(scss, mixins) {
34848
- return scss.replace(
34849
- /@include\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*;/g,
34850
- (_m, name, argsStr) => {
34851
- if (!(name in mixins)) {
34852
- return `/* MIXIN NOT FOUND: ${name} */`;
34853
- }
34854
- const mixin = mixins[name];
34855
- const args = argsStr ? argsStr.split(",").map((a) => a.trim()).filter(Boolean) : [];
34856
- let body = mixin.body;
34857
- for (let i = 0; i < mixin.params.length; i++) {
34858
- const paramName = mixin.params[i].split(":")[0].trim();
34859
- const defaultVal = mixin.params[i].includes(":") ? mixin.params[i].split(":").slice(1).join(":").trim() : "";
34860
- const value = i < args.length ? args[i] : defaultVal;
34861
- body = body.replaceAll(`$${paramName}`, value);
34862
- }
34863
- return body;
34864
- }
34865
- );
34866
- }
34867
- function evalMath(scss) {
34868
- const placeholders = [];
34869
- const masked = scss.replace(/calc\([^()]*\)/g, (m) => {
34870
- placeholders.push(m);
34871
- return `\0CALC${placeholders.length - 1}\0`;
34872
- });
34873
- const folded = masked.replace(
34874
- /([\d.]+)([a-z%]*)\s*([+\-*/])\s*([\d.]+)([a-z%]*)/g,
34875
- (full, n1, u1, op, n2, u2) => {
34876
- const num1 = parseFloat(n1);
34877
- const num2 = parseFloat(n2);
34878
- if (Number.isNaN(num1) || Number.isNaN(num2)) return full;
34879
- const unit1 = u1 || "";
34880
- const unit2 = u2 || "";
34881
- let unit;
34882
- if (unit1 === unit2) {
34883
- unit = unit1;
34884
- } else if ((op === "*" || op === "/") && unit1 === "") {
34885
- unit = unit2;
34886
- } else if ((op === "*" || op === "/") && unit2 === "") {
34887
- unit = unit1;
34888
- } else {
34889
- return full;
34890
- }
34891
- let result;
34892
- switch (op) {
34893
- case "+":
34894
- result = num1 + num2;
34895
- break;
34896
- case "-":
34897
- result = num1 - num2;
34898
- break;
34899
- case "*":
34900
- result = num1 * num2;
34901
- break;
34902
- case "/":
34903
- if (num2 === 0) return full;
34904
- result = num1 / num2;
34905
- break;
34906
- default:
34907
- return full;
34908
- }
34909
- if (result === Math.floor(result)) {
34910
- return `${Math.floor(result)}${unit}`;
34911
- }
34912
- return `${result.toFixed(2)}${unit}`;
34913
- }
34914
- );
34915
- return folded.replace(/\x00CALC(\d+)\x00/g, (_m, idx) => {
34916
- return placeholders[parseInt(idx, 10)];
34917
- });
34918
- }
34919
- function resolveColorFunctions(scss) {
34920
- scss = scss.replace(
34921
- /lighten\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g,
34922
- (_m, color, amt) => adjustLightness(color.trim(), parseFloat(amt.trim().replace(/%$/, "")) / 100)
34923
- );
34924
- scss = scss.replace(
34925
- /darken\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g,
34926
- (_m, color, amt) => adjustLightness(color.trim(), -(parseFloat(amt.trim().replace(/%$/, "")) / 100))
34927
- );
34928
- scss = scss.replace(/rgba\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*([\d.]+)\s*\)/g, (whole, hex, alpha) => {
34929
- const rgb = hexToRgb(hex);
34930
- return rgb === null ? whole : `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha.trim()})`;
34931
- });
34932
- scss = scss.replace(/rgb\(\s*(#[0-9a-fA-F]{3,8})\s*\)/g, (whole, hex) => {
34933
- const rgb = hexToRgb(hex);
34934
- return rgb === null ? whole : `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
34935
- });
34936
- scss = scss.replace(
34937
- /mix\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*(#[0-9a-fA-F]{3,8})\s*(?:,\s*([\d.]+%?)\s*)?\)/g,
34938
- (whole, h1, h2, weight) => {
34939
- const c1 = hexToRgb(h1);
34940
- const c2 = hexToRgb(h2);
34941
- if (c1 === null || c2 === null) return whole;
34942
- const w = weight ? parseFloat(weight.replace(/%$/, "")) / 100 : 0.5;
34943
- const mixed = [0, 1, 2].map((i) => Math.round(c1[i] * w + c2[i] * (1 - w)));
34944
- return `#${mixed.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
34945
- }
34946
- );
34947
- return scss;
34948
- }
34949
- function hexToRgb(color) {
34950
- let c = color.trim().replace(/^#/, "");
34951
- if (c.length === 3) {
34952
- c = c.split("").map((ch) => ch + ch).join("");
34953
- }
34954
- if (!/^[0-9a-fA-F]{6}$/.test(c)) return null;
34955
- return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
34956
- }
34957
- function adjustLightness(color, amount) {
34958
- const rgb = hexToRgb(color);
34959
- if (rgb === null) return color;
34960
- let [r, g, b] = rgb.map((v) => v / 255);
34961
- const max = Math.max(r, g, b);
34962
- const min = Math.min(r, g, b);
34963
- let l = (max + min) / 2;
34964
- const d = max - min;
34965
- let h = 0;
34966
- let s = 0;
34967
- if (d !== 0) {
34968
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
34969
- if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
34970
- else if (max === g) h = (b - r) / d + 2;
34971
- else h = (r - g) / d + 4;
34972
- h /= 6;
34973
- }
34974
- l = Math.max(0, Math.min(1, l + amount));
34975
- if (s === 0) {
34976
- r = g = b = l;
34977
- } else {
34978
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
34979
- const p = 2 * l - q;
34980
- r = hueToRgb(p, q, h + 1 / 3);
34981
- g = hueToRgb(p, q, h);
34982
- b = hueToRgb(p, q, h - 1 / 3);
34983
- }
34984
- const hex = (v) => Math.trunc(v * 255).toString(16).padStart(2, "0");
34985
- return `#${hex(r)}${hex(g)}${hex(b)}`;
34986
- }
34987
- function hueToRgb(p, q, t) {
34988
- if (t < 0) t += 1;
34989
- if (t > 1) t -= 1;
34990
- if (t < 1 / 6) return p + (q - p) * 6 * t;
34991
- if (t < 1 / 2) return q;
34992
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
34993
- return p;
34994
- }
34995
- function flattenNesting(scss) {
34996
- const output = [];
34997
- flattenBlock(scss, [], output);
34998
- return output.join("\n");
34999
- }
35000
- function flattenBlock(content, parentSelectors, output) {
35001
- let pos = 0;
35002
- const properties = [];
35003
- while (pos < content.length) {
35004
- while (pos < content.length && /[\s]/.test(content[pos])) {
35005
- pos++;
35006
- }
35007
- if (pos >= content.length) break;
35008
- if (content[pos] === "/" && content[pos + 1] === "*") {
35009
- const end = content.indexOf("*/", pos + 2);
35010
- if (end === -1) break;
35011
- output.push(content.slice(pos, end + 2));
35012
- pos = end + 2;
35013
- continue;
35014
- }
35015
- if (content.slice(pos, pos + 6) === "@media") {
35016
- const brace = content.indexOf("{", pos);
35017
- if (brace === -1) break;
35018
- const mediaQuery = content.slice(pos, brace).trim();
35019
- const body = findBlock(content, brace + 1);
35020
- if (body === null) break;
35021
- pos = brace + 1 + body.length + 1;
35022
- const innerOutput = [];
35023
- flattenBlock(body, parentSelectors, innerOutput);
35024
- if (innerOutput.length > 0) {
35025
- output.push(`${mediaQuery} {`);
35026
- for (const line of innerOutput) {
35027
- output.push(` ${line}`);
35028
- }
35029
- output.push("}");
35030
- }
35031
- continue;
35032
- }
35033
- const bracePos = content.indexOf("{", pos);
35034
- const semiPos = content.indexOf(";", pos);
35035
- if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
35036
- const prop = content.slice(pos, semiPos).trim();
35037
- if (prop && !prop.startsWith("@")) {
35038
- properties.push(prop);
35039
- }
35040
- pos = semiPos + 1;
35041
- continue;
35042
- }
35043
- if (bracePos !== -1) {
35044
- const selectorText = content.slice(pos, bracePos).trim();
35045
- const body = findBlock(content, bracePos + 1);
35046
- if (body === null) break;
35047
- pos = bracePos + 1 + body.length + 1;
35048
- if (!selectorText) continue;
35049
- const selectors = selectorText.split(",").map((s) => s.trim());
35050
- const newSelectors = [];
35051
- for (const sel of selectors) {
35052
- if (parentSelectors.length > 0) {
35053
- for (const parent of parentSelectors) {
35054
- if (sel.includes("&")) {
35055
- newSelectors.push(sel.replace(/&/g, parent));
35056
- } else {
35057
- newSelectors.push(`${parent} ${sel}`);
35058
- }
35059
- }
35060
- } else {
35061
- newSelectors.push(sel);
35062
- }
35063
- }
35064
- flattenBlock(body, newSelectors, output);
35065
- continue;
35066
- }
35067
- const remaining = content.slice(pos).trim();
35068
- if (remaining) {
35069
- properties.push(remaining);
35070
- }
35071
- break;
35072
- }
35073
- if (properties.length > 0 && parentSelectors.length > 0) {
35074
- const selectorStr = parentSelectors.join(", ");
35075
- output.push(`${selectorStr} {`);
35076
- for (const prop of properties) {
35077
- output.push(` ${prop};`);
35078
- }
35079
- output.push("}");
35080
- }
35081
- }
35082
- function findBlock(content, start2) {
35083
- let depth = 1;
35084
- let pos = start2;
35085
- while (pos < content.length && depth > 0) {
35086
- if (content[pos] === "{") depth++;
35087
- else if (content[pos] === "}") depth--;
35088
- if (depth > 0) pos++;
35089
- }
35090
- return depth === 0 ? content.slice(start2, pos) : null;
35091
- }
35092
- function cleanup(css) {
35093
- css = css.replace(/[^{}]+\{\s*\}/g, "");
35094
- css = css.replace(/\n{3,}/g, "\n\n");
35095
- css = css.split("\n").map((line) => line.trimEnd()).join("\n");
35096
- return css.trim() + "\n";
35097
- }
35098
- var ScssCompiler, VARIABLE_FLAG;
35099
- var init_scss = __esm({
35100
- "../core/src/scss.ts"() {
35101
- "use strict";
35102
- ScssCompiler = class {
35103
- _importPaths;
35104
- _variables;
35105
- constructor(config) {
35106
- this._importPaths = config?.importPaths ? [...config.importPaths] : [];
35107
- this._variables = config?.variables ? { ...config.variables } : {};
35108
- }
35109
- /** Compile an SCSS string to CSS. */
35110
- compile(source) {
35111
- return compileString(source, this._importPaths, { ...this._variables });
35112
- }
35113
- /** Compile an SCSS file to CSS. */
35114
- compileFile(filePath) {
35115
- const absPath = resolve20(filePath);
35116
- const content = readFileSync23(absPath, "utf-8");
35117
- const paths = [dirname13(absPath), ...this._importPaths];
35118
- return compileString(content, paths, { ...this._variables });
35119
- }
35120
- /** Add a directory to the import resolution path. */
35121
- addImportPath(path8) {
35122
- this._importPaths.push(resolve20(path8));
35123
- }
35124
- /** Set or override an SCSS variable. */
35125
- setVariable(name, value) {
35126
- const key = name.startsWith("$") ? name.slice(1) : name;
35127
- this._variables[key] = value;
35128
- }
35129
- /** Compile all .scss files in a directory into a single CSS output file. */
35130
- compileScss(scssDir = "src/scss", output = "src/public/css/default.css", minify = false) {
35131
- const absDir = resolve20(scssDir);
35132
- if (!existsSync25(absDir)) return "";
35133
- const files = readdirSync18(absDir).filter((f) => f.endsWith(".scss") && !f.startsWith("_")).sort().map((f) => join28(absDir, f));
35134
- if (files.length === 0) return "";
35135
- const paths = [absDir, ...this._importPaths];
35136
- const imported = /* @__PURE__ */ new Set();
35137
- let merged = "";
35138
- for (const file of files) {
35139
- const content = readFileSync23(file, "utf-8");
35140
- imported.add(file);
35141
- merged += resolveImports(content, paths, imported) + "\n";
35142
- }
35143
- let css = compileString(merged, paths, { ...this._variables });
35144
- if (minify) {
35145
- css = css.replace(/\/\*.*?\*\//gs, "");
35146
- css = css.replace(/\s+/g, " ");
35147
- css = css.replace(/\s*([{}:;,])\s*/g, "$1");
35148
- css = css.replace(/;}/g, "}");
35149
- css = css.trim();
35150
- }
35151
- const absOutput = resolve20(output);
35152
- const outDir = dirname13(absOutput);
35153
- if (!existsSync25(outDir)) mkdirSync19(outDir, { recursive: true });
35154
- let existing = null;
35155
- try {
35156
- existing = existsSync25(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
35157
- } catch {
35158
- existing = null;
35159
- }
35160
- if (existing !== css) {
35161
- writeFileSync17(absOutput, css, "utf-8");
35162
- }
35163
- return css;
35164
- }
35165
- };
35166
- VARIABLE_FLAG = /\s*!(default|global)\s*$/;
35167
- }
35168
- });
35169
-
35170
34766
  // ../core/src/mqttMessage.ts
35171
34767
  var MqttMessage;
35172
34768
  var init_mqttMessage = __esm({
@@ -35240,7 +34836,7 @@ var init_mqttMessage = __esm({
35240
34836
  import net2 from "node:net";
35241
34837
  import tls from "node:tls";
35242
34838
  import { randomBytes as randomBytes7 } from "node:crypto";
35243
- import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
34839
+ import { existsSync as existsSync25, readFileSync as readFileSync23 } from "node:fs";
35244
34840
  var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
35245
34841
  var init_mqtt = __esm({
35246
34842
  "../core/src/mqtt.ts"() {
@@ -35443,7 +35039,7 @@ var init_mqtt = __esm({
35443
35039
  */
35444
35040
  async connect() {
35445
35041
  this.closeSocket();
35446
- if (this.secure && this.tlsVerify && this.caFile && !existsSync26(this.caFile)) {
35042
+ if (this.secure && this.tlsVerify && this.caFile && !existsSync25(this.caFile)) {
35447
35043
  throw new MqttError(
35448
35044
  `MQTT CA file not found: ${this.caFile} -- TINA4_MQTT_CA_FILE (or caFile) must point at the broker's CA certificate in PEM form`
35449
35045
  );
@@ -35680,7 +35276,7 @@ var init_mqtt = __esm({
35680
35276
  * a later client.
35681
35277
  */
35682
35278
  openSocket() {
35683
- return new Promise((resolve31, reject) => {
35279
+ return new Promise((resolve30, reject) => {
35684
35280
  let settled = false;
35685
35281
  const settle = (fn) => {
35686
35282
  if (settled) return;
@@ -35705,10 +35301,10 @@ var init_mqtt = __esm({
35705
35301
  servername: this.host,
35706
35302
  rejectUnauthorized: this.tlsVerify
35707
35303
  };
35708
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
35709
- sock = tls.connect(opts, () => settle(() => resolve31(sock)));
35304
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync23(this.caFile);
35305
+ sock = tls.connect(opts, () => settle(() => resolve30(sock)));
35710
35306
  } else {
35711
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve31(sock)));
35307
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve30(sock)));
35712
35308
  }
35713
35309
  sock.once("error", (err) => {
35714
35310
  settle(() => {
@@ -35747,13 +35343,13 @@ var init_mqtt = __esm({
35747
35343
  writePacket(header, body) {
35748
35344
  if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
35749
35345
  const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
35750
- return new Promise((resolve31, reject) => {
35346
+ return new Promise((resolve30, reject) => {
35751
35347
  this.socket.write(packet, (err) => {
35752
35348
  if (err) {
35753
35349
  reject(new MqttError(`MQTT write failed: ${err.message}`));
35754
35350
  } else {
35755
35351
  this.lastWriteAt = Date.now();
35756
- resolve31();
35352
+ resolve30();
35757
35353
  }
35758
35354
  });
35759
35355
  });
@@ -35786,7 +35382,7 @@ var init_mqtt = __esm({
35786
35382
  if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
35787
35383
  if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
35788
35384
  if (this.socketError !== null) return Promise.reject(this.socketError);
35789
- return new Promise((resolve31, reject) => {
35385
+ return new Promise((resolve30, reject) => {
35790
35386
  let timer = null;
35791
35387
  if (deadline !== null) {
35792
35388
  const remaining = deadline - Date.now();
@@ -35801,7 +35397,7 @@ var init_mqtt = __esm({
35801
35397
  }
35802
35398
  }, remaining);
35803
35399
  }
35804
- this.waiter = { need, resolve: resolve31, reject, timer };
35400
+ this.waiter = { need, resolve: resolve30, reject, timer };
35805
35401
  this.serviceWaiter();
35806
35402
  });
35807
35403
  }
@@ -35925,8 +35521,8 @@ var init_mqtt = __esm({
35925
35521
  });
35926
35522
 
35927
35523
  // ../core/src/service.ts
35928
- import { readdirSync as readdirSync19, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
35929
- import { join as join29, extname as extname8 } from "node:path";
35524
+ import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
35525
+ import { join as join28, extname as extname8 } from "node:path";
35930
35526
  import { pathToFileURL } from "node:url";
35931
35527
  function matchCronField(field, value) {
35932
35528
  if (field === "*") return true;
@@ -36092,14 +35688,14 @@ var init_service = __esm({
36092
35688
  const discovered = [];
36093
35689
  let entries;
36094
35690
  try {
36095
- entries = readdirSync19(dir);
35691
+ entries = readdirSync18(dir);
36096
35692
  } catch {
36097
35693
  return discovered;
36098
35694
  }
36099
35695
  for (const entry of entries) {
36100
35696
  const ext = extname8(entry);
36101
35697
  if (ext !== ".ts" && ext !== ".js") continue;
36102
- const fullPath = join29(dir, entry);
35698
+ const fullPath = join28(dir, entry);
36103
35699
  const stat = statSync18(fullPath);
36104
35700
  if (!stat.isFile()) continue;
36105
35701
  try {
@@ -36208,14 +35804,14 @@ var init_service = __esm({
36208
35804
  const dir = serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
36209
35805
  let entries;
36210
35806
  try {
36211
- entries = readdirSync19(dir);
35807
+ entries = readdirSync18(dir);
36212
35808
  } catch {
36213
35809
  return;
36214
35810
  }
36215
35811
  for (const entry of entries) {
36216
35812
  const ext = extname8(entry);
36217
35813
  if (ext !== ".ts" && ext !== ".js") continue;
36218
- const fullPath = join29(dir, entry);
35814
+ const fullPath = join28(dir, entry);
36219
35815
  if (watchedFiles.has(fullPath)) continue;
36220
35816
  watchedFiles.add(fullPath);
36221
35817
  watchFile(fullPath, { interval: 1e3 }, async () => {
@@ -36259,7 +35855,7 @@ import https from "node:https";
36259
35855
  import { URL as URL2 } from "node:url";
36260
35856
  import { randomBytes as randomBytes8 } from "node:crypto";
36261
35857
  import { promises as fsp, createWriteStream } from "node:fs";
36262
- import { basename as basename6 } from "node:path";
35858
+ import { basename as basename5 } from "node:path";
36263
35859
  import { pipeline } from "node:stream/promises";
36264
35860
  function sameOrigin(urlA, urlB) {
36265
35861
  try {
@@ -36549,7 +36145,7 @@ var init_api = __esm({
36549
36145
  error: err instanceof Error ? err.message : String(err)
36550
36146
  };
36551
36147
  }
36552
- uploadName = filename || basename6(filePath);
36148
+ uploadName = filename || basename5(filePath);
36553
36149
  } else {
36554
36150
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
36555
36151
  }
@@ -36764,12 +36360,12 @@ var init_api = __esm({
36764
36360
  * authenticate to.
36765
36361
  */
36766
36362
  performRequest(method, url, headers, data, redirectsLeft) {
36767
- return new Promise((resolve31) => {
36363
+ return new Promise((resolve30) => {
36768
36364
  let parsed;
36769
36365
  try {
36770
36366
  parsed = new URL2(url);
36771
36367
  } catch (err) {
36772
- resolve31({ kind: "error", error: err instanceof Error ? err.message : String(err) });
36368
+ resolve30({ kind: "error", error: err instanceof Error ? err.message : String(err) });
36773
36369
  return;
36774
36370
  }
36775
36371
  const isHttps = parsed.protocol === "https:";
@@ -36794,7 +36390,7 @@ var init_api = __esm({
36794
36390
  try {
36795
36391
  nextUrl = new URL2(location, url).toString();
36796
36392
  } catch {
36797
- resolve31({ kind: "response", res });
36393
+ resolve30({ kind: "response", res });
36798
36394
  return;
36799
36395
  }
36800
36396
  const crossOrigin = !sameOrigin(url, nextUrl);
@@ -36812,17 +36408,17 @@ var init_api = __esm({
36812
36408
  deleteHeaderCaseInsensitive(nextHeaders, name);
36813
36409
  }
36814
36410
  }
36815
- this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve31);
36411
+ this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve30);
36816
36412
  return;
36817
36413
  }
36818
- resolve31({ kind: "response", res });
36414
+ resolve30({ kind: "response", res });
36819
36415
  });
36820
36416
  req2.on("timeout", () => {
36821
36417
  req2.destroy();
36822
- resolve31({ kind: "error", error: `Request timed out after ${this.timeout}s` });
36418
+ resolve30({ kind: "error", error: `Request timed out after ${this.timeout}s` });
36823
36419
  });
36824
36420
  req2.on("error", (err) => {
36825
- resolve31({ kind: "error", error: err.message });
36421
+ resolve30({ kind: "error", error: err.message });
36826
36422
  });
36827
36423
  if (data) {
36828
36424
  req2.write(data);
@@ -36832,7 +36428,7 @@ var init_api = __esm({
36832
36428
  }
36833
36429
  /** Buffer a response body, parse JSON if possible, and store cookies. */
36834
36430
  readResponse(res) {
36835
- return new Promise((resolve31) => {
36431
+ return new Promise((resolve30) => {
36836
36432
  const chunks = [];
36837
36433
  res.on("data", (chunk) => {
36838
36434
  chunks.push(chunk);
@@ -36847,7 +36443,7 @@ var init_api = __esm({
36847
36443
  } catch {
36848
36444
  parsed = raw;
36849
36445
  }
36850
- resolve31({
36446
+ resolve30({
36851
36447
  http_code: res.statusCode ?? null,
36852
36448
  body: parsed,
36853
36449
  headers: respHeaders,
@@ -36855,7 +36451,7 @@ var init_api = __esm({
36855
36451
  });
36856
36452
  });
36857
36453
  res.on("error", (err) => {
36858
- resolve31({ http_code: null, body: null, headers: {}, error: err.message });
36454
+ resolve30({ http_code: null, body: null, headers: {}, error: err.message });
36859
36455
  });
36860
36456
  });
36861
36457
  }
@@ -36908,14 +36504,14 @@ var init_api = __esm({
36908
36504
  // ../core/src/messenger.ts
36909
36505
  import net3 from "node:net";
36910
36506
  import tls2 from "node:tls";
36911
- import { readFileSync as readFileSync25 } from "node:fs";
36912
- import { basename as basename7 } from "node:path";
36507
+ import { readFileSync as readFileSync24 } from "node:fs";
36508
+ import { basename as basename6 } from "node:path";
36913
36509
  import { randomUUID as randomUUID7 } from "node:crypto";
36914
36510
  function tlsRejectUnauthorized() {
36915
36511
  return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
36916
36512
  }
36917
36513
  function readResponse(socket) {
36918
- return new Promise((resolve31, reject) => {
36514
+ return new Promise((resolve30, reject) => {
36919
36515
  let buffer = "";
36920
36516
  const onData = (chunk) => {
36921
36517
  buffer += chunk.toString("utf-8");
@@ -36927,7 +36523,7 @@ function readResponse(socket) {
36927
36523
  if (line.length >= 4 && line[3] === " ") {
36928
36524
  socket.removeListener("data", onData);
36929
36525
  socket.removeListener("error", onError);
36930
- resolve31({ code, text: buffer.trim() });
36526
+ resolve30({ code, text: buffer.trim() });
36931
36527
  return;
36932
36528
  }
36933
36529
  }
@@ -36941,10 +36537,10 @@ function readResponse(socket) {
36941
36537
  });
36942
36538
  }
36943
36539
  function sendCommand(socket, command) {
36944
- return new Promise((resolve31, reject) => {
36540
+ return new Promise((resolve30, reject) => {
36945
36541
  socket.write(command + "\r\n", "utf-8", (err) => {
36946
36542
  if (err) return reject(err);
36947
- readResponse(socket).then(resolve31, reject);
36543
+ readResponse(socket).then(resolve30, reject);
36948
36544
  });
36949
36545
  });
36950
36546
  }
@@ -37000,8 +36596,8 @@ function buildMimeMessage(options) {
37000
36596
  lines.push(options.body);
37001
36597
  }
37002
36598
  for (const filePath of options.attachments) {
37003
- const fileName = basename7(filePath);
37004
- const fileData = readFileSync25(filePath);
36599
+ const fileName = basename6(filePath);
36600
+ const fileData = readFileSync24(filePath);
37005
36601
  const base64Data = fileData.toString("base64");
37006
36602
  lines.push("");
37007
36603
  lines.push(`--${boundary}`);
@@ -37044,7 +36640,7 @@ function imapQuote(s) {
37044
36640
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
37045
36641
  }
37046
36642
  function imapReadLine(socket) {
37047
- return new Promise((resolve31, reject) => {
36643
+ return new Promise((resolve30, reject) => {
37048
36644
  let buffer = "";
37049
36645
  const onData = (chunk) => {
37050
36646
  buffer += chunk.toString("utf-8");
@@ -37052,7 +36648,7 @@ function imapReadLine(socket) {
37052
36648
  if (nlIndex !== -1) {
37053
36649
  socket.removeListener("data", onData);
37054
36650
  socket.removeListener("error", onError);
37055
- resolve31(buffer);
36651
+ resolve30(buffer);
37056
36652
  }
37057
36653
  };
37058
36654
  const onError = (err) => {
@@ -37064,7 +36660,7 @@ function imapReadLine(socket) {
37064
36660
  });
37065
36661
  }
37066
36662
  function imapCommand(socket, command) {
37067
- return new Promise((resolve31, reject) => {
36663
+ return new Promise((resolve30, reject) => {
37068
36664
  imapTagCounter++;
37069
36665
  const tag = `T${imapTagCounter}`;
37070
36666
  const fullCommand = `${tag} ${command}\r
@@ -37075,7 +36671,7 @@ function imapCommand(socket, command) {
37075
36671
  if (buffer.includes(`${tag} OK`)) {
37076
36672
  socket.removeListener("data", onData);
37077
36673
  socket.removeListener("error", onError);
37078
- resolve31(buffer);
36674
+ resolve30(buffer);
37079
36675
  return;
37080
36676
  }
37081
36677
  if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
@@ -37104,91 +36700,149 @@ function parseSearchResponse(response) {
37104
36700
  if (!match) return [];
37105
36701
  return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
37106
36702
  }
37107
- function parseHeaderResponse(uid, response) {
37108
- const headers = {};
37109
- const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
37110
- if (headerBlock) {
37111
- const lines = headerBlock[1].split(/\r\n/);
37112
- let currentKey = "";
37113
- for (const line of lines) {
37114
- if (/^\s/.test(line) && currentKey) {
37115
- headers[currentKey] += " " + line.trim();
37116
- } else {
37117
- const colonIdx = line.indexOf(":");
37118
- if (colonIdx > 0) {
37119
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
37120
- headers[currentKey] = line.substring(colonIdx + 1).trim();
37121
- }
37122
- }
37123
- }
37124
- }
37125
- const seen = /\\Seen/i.test(response);
37126
- return {
37127
- uid,
37128
- subject: headers["subject"] ?? "",
37129
- from: headers["from"] ?? "",
37130
- to: headers["to"] ?? "",
37131
- date: headers["date"] ?? "",
37132
- snippet: "",
37133
- seen
37134
- };
36703
+ function extractRawMessage(response) {
36704
+ const m = response.match(/\{(\d+)\}\r\n/);
36705
+ if (!m) return response;
36706
+ const start2 = (m.index ?? 0) + m[0].length;
36707
+ return response.slice(start2, start2 + parseInt(m[1], 10));
37135
36708
  }
37136
- function parseFullMessage(uid, response) {
37137
- const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
37138
- const rawMessage = bodyMatch ? bodyMatch[2] : response;
37139
- const headerEnd = rawMessage.indexOf("\r\n\r\n");
37140
- const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
37141
- const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
36709
+ function parseMimeHeaders(section) {
37142
36710
  const headers = {};
37143
- const headerLines = headerSection.split(/\r\n/);
37144
36711
  let currentKey = "";
37145
- for (const line of headerLines) {
36712
+ for (const line of section.split(/\r\n/)) {
37146
36713
  if (/^\s/.test(line) && currentKey) {
37147
36714
  headers[currentKey] += " " + line.trim();
37148
36715
  } else {
37149
- const colonIdx = line.indexOf(":");
37150
- if (colonIdx > 0) {
37151
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
37152
- headers[currentKey] = line.substring(colonIdx + 1).trim();
36716
+ const idx = line.indexOf(":");
36717
+ if (idx > 0) {
36718
+ currentKey = line.substring(0, idx).trim().toLowerCase();
36719
+ headers[currentKey] = line.substring(idx + 1).trim();
36720
+ }
36721
+ }
36722
+ }
36723
+ return headers;
36724
+ }
36725
+ function decodeTransfer(body, encoding) {
36726
+ const enc = encoding.toLowerCase().trim();
36727
+ if (enc === "base64") {
36728
+ try {
36729
+ return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf-8");
36730
+ } catch {
36731
+ return body;
36732
+ }
36733
+ }
36734
+ if (enc === "quoted-printable") {
36735
+ return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
36736
+ }
36737
+ return body;
36738
+ }
36739
+ function decodeAttachmentBytes(body, encoding) {
36740
+ const enc = encoding.toLowerCase().trim();
36741
+ if (enc === "base64") {
36742
+ return Buffer.from(body.replace(/\s+/g, ""), "base64");
36743
+ }
36744
+ const trimmed = body.replace(/\r\n$/, "");
36745
+ if (enc === "quoted-printable") {
36746
+ const collapsed = trimmed.replace(/=\r?\n/g, "");
36747
+ const bytes = [];
36748
+ for (let i = 0; i < collapsed.length; i++) {
36749
+ const hex = collapsed.substring(i + 1, i + 3);
36750
+ if (collapsed[i] === "=" && /^[0-9A-Fa-f]{2}$/.test(hex)) {
36751
+ bytes.push(parseInt(hex, 16));
36752
+ i += 2;
36753
+ } else {
36754
+ bytes.push(collapsed.charCodeAt(i) & 255);
37153
36755
  }
37154
36756
  }
36757
+ return Buffer.from(bytes);
37155
36758
  }
36759
+ return Buffer.from(trimmed, "utf-8");
36760
+ }
36761
+ function attachmentFilename(disposition, contentType) {
36762
+ const d = disposition.match(/filename="?([^";\r\n]+)"?/i);
36763
+ if (d) return d[1].trim();
36764
+ const c = contentType.match(/name="?([^";\r\n]+)"?/i);
36765
+ if (c) return c[1].trim();
36766
+ return "attachment";
36767
+ }
36768
+ function makeSnippet(bodyText, bodyHtml) {
36769
+ return (bodyText || bodyHtml || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200);
36770
+ }
36771
+ function toIsoDate(raw) {
36772
+ if (!raw) return "";
36773
+ const d = new Date(raw);
36774
+ return Number.isNaN(d.getTime()) ? raw : d.toISOString();
36775
+ }
36776
+ function parseMessage(response) {
36777
+ const raw = extractRawMessage(response);
36778
+ const headerEnd = raw.indexOf("\r\n\r\n");
36779
+ const headerSection = headerEnd >= 0 ? raw.substring(0, headerEnd) : raw;
36780
+ const bodySection = headerEnd >= 0 ? raw.substring(headerEnd + 4) : "";
36781
+ const headers = parseMimeHeaders(headerSection);
37156
36782
  const contentType = headers["content-type"] ?? "text/plain";
37157
36783
  let bodyText = "";
37158
36784
  let bodyHtml = "";
36785
+ const attachments = [];
37159
36786
  if (contentType.includes("multipart")) {
37160
36787
  const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
37161
36788
  if (boundaryMatch) {
37162
- const boundary = boundaryMatch[1];
37163
- const parts = bodySection.split("--" + boundary);
37164
- for (const part of parts) {
37165
- if (part.trim() === "" || part.trim() === "--") continue;
37166
- const partHeaderEnd = part.indexOf("\r\n\r\n");
37167
- const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
37168
- const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
37169
- if (partHeaders.includes("text/html")) {
37170
- bodyHtml = partBody;
37171
- } else if (partHeaders.includes("text/plain")) {
37172
- bodyText = partBody;
36789
+ const boundary = "--" + boundaryMatch[1];
36790
+ for (const part of bodySection.split(boundary)) {
36791
+ const trimmed = part.trim();
36792
+ if (trimmed === "" || trimmed === "--") continue;
36793
+ const pEnd = part.indexOf("\r\n\r\n");
36794
+ if (pEnd < 0) continue;
36795
+ const pHeaders = parseMimeHeaders(part.substring(0, pEnd));
36796
+ const pBody = part.substring(pEnd + 4);
36797
+ const cte = pHeaders["content-transfer-encoding"] ?? "";
36798
+ const pType = pHeaders["content-type"] ?? "text/plain";
36799
+ const disposition = pHeaders["content-disposition"] ?? "";
36800
+ if (/attachment/i.test(disposition)) {
36801
+ const content = decodeAttachmentBytes(pBody, cte);
36802
+ attachments.push({
36803
+ filename: attachmentFilename(disposition, pType),
36804
+ contentType: pType.split(";")[0].trim(),
36805
+ size: content.length,
36806
+ content
36807
+ });
36808
+ } else if (pType.includes("text/html")) {
36809
+ bodyHtml = decodeTransfer(pBody, cte).trim();
36810
+ } else if (pType.includes("text/plain")) {
36811
+ bodyText = decodeTransfer(pBody, cte).trim();
37173
36812
  }
37174
36813
  }
37175
36814
  }
37176
36815
  } else if (contentType.includes("text/html")) {
37177
- bodyHtml = bodySection;
36816
+ bodyHtml = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
37178
36817
  } else {
37179
- bodyText = bodySection;
36818
+ bodyText = decodeTransfer(bodySection, headers["content-transfer-encoding"] ?? "").trim();
37180
36819
  }
37181
- bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
37182
- bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
36820
+ return { headers, bodyText, bodyHtml, attachments };
36821
+ }
36822
+ function parseSummary(uid, response) {
36823
+ const { headers, bodyText, bodyHtml } = parseMessage(response);
36824
+ return {
36825
+ uid,
36826
+ subject: headers["subject"] ?? "",
36827
+ from: headers["from"] ?? "",
36828
+ to: headers["to"] ?? "",
36829
+ date: toIsoDate(headers["date"] ?? ""),
36830
+ snippet: makeSnippet(bodyText, bodyHtml),
36831
+ seen: /\\Seen/i.test(response)
36832
+ };
36833
+ }
36834
+ function parseFullMessage(uid, response) {
36835
+ const { headers, bodyText, bodyHtml, attachments } = parseMessage(response);
37183
36836
  return {
37184
36837
  uid,
37185
36838
  subject: headers["subject"] ?? "",
37186
36839
  from: headers["from"] ?? "",
37187
36840
  to: headers["to"] ?? "",
37188
36841
  cc: headers["cc"] ?? "",
37189
- date: headers["date"] ?? "",
36842
+ date: toIsoDate(headers["date"] ?? ""),
37190
36843
  bodyText,
37191
36844
  bodyHtml,
36845
+ attachments,
37192
36846
  headers
37193
36847
  };
37194
36848
  }
@@ -37304,89 +36958,89 @@ var init_messenger = __esm({
37304
36958
  }
37305
36959
  const messageId = `${randomUUID7()}@${this.host}`;
37306
36960
  if (allRecipients.length === 0) {
37307
- return { success: false, message: "No recipients specified" };
36961
+ return { success: false, message: "No recipients specified", id: null };
37308
36962
  }
37309
36963
  if (!this.fromAddress) {
37310
- return { success: false, message: "No from address configured" };
36964
+ return { success: false, message: "No from address configured", id: null };
37311
36965
  }
37312
36966
  try {
37313
36967
  let socket;
37314
36968
  if (this.port === 465) {
37315
36969
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
37316
- await new Promise((resolve31, reject) => {
37317
- socket.once("secureConnect", resolve31);
36970
+ await new Promise((resolve30, reject) => {
36971
+ socket.once("secureConnect", resolve30);
37318
36972
  socket.once("error", reject);
37319
36973
  });
37320
36974
  } else {
37321
36975
  socket = net3.createConnection({ host: this.host, port: this.port });
37322
- await new Promise((resolve31, reject) => {
37323
- socket.once("connect", resolve31);
36976
+ await new Promise((resolve30, reject) => {
36977
+ socket.once("connect", resolve30);
37324
36978
  socket.once("error", reject);
37325
36979
  });
37326
36980
  }
37327
36981
  const greeting = await readResponse(socket);
37328
36982
  if (greeting.code !== 220) {
37329
36983
  socket.destroy();
37330
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
36984
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}`, id: null };
37331
36985
  }
37332
36986
  const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
37333
36987
  if (ehlo.code !== 250) {
37334
36988
  socket.destroy();
37335
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
36989
+ return { success: false, message: `EHLO failed: ${ehlo.text}`, id: null };
37336
36990
  }
37337
36991
  if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
37338
36992
  const starttls = await sendCommand(socket, "STARTTLS");
37339
36993
  if (starttls.code !== 220) {
37340
36994
  socket.destroy();
37341
- return { success: false, message: `STARTTLS failed: ${starttls.text}` };
36995
+ return { success: false, message: `STARTTLS failed: ${starttls.text}`, id: null };
37342
36996
  }
37343
36997
  const plainSocket = socket;
37344
36998
  socket = tls2.connect(
37345
36999
  { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
37346
37000
  );
37347
- await new Promise((resolve31, reject) => {
37348
- socket.once("secureConnect", resolve31);
37001
+ await new Promise((resolve30, reject) => {
37002
+ socket.once("secureConnect", resolve30);
37349
37003
  socket.once("error", reject);
37350
37004
  });
37351
37005
  const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
37352
37006
  if (ehlo2.code !== 250) {
37353
37007
  socket.destroy();
37354
- return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
37008
+ return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}`, id: null };
37355
37009
  }
37356
37010
  }
37357
37011
  if (this.username && this.password) {
37358
37012
  const auth = await sendCommand(socket, "AUTH LOGIN");
37359
37013
  if (auth.code !== 334) {
37360
37014
  socket.destroy();
37361
- return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
37015
+ return { success: false, message: `AUTH LOGIN failed: ${auth.text}`, id: null };
37362
37016
  }
37363
37017
  const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
37364
37018
  if (userResp.code !== 334) {
37365
37019
  socket.destroy();
37366
- return { success: false, message: `AUTH username failed: ${userResp.text}` };
37020
+ return { success: false, message: `AUTH username failed: ${userResp.text}`, id: null };
37367
37021
  }
37368
37022
  const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
37369
37023
  if (passResp.code !== 235) {
37370
37024
  socket.destroy();
37371
- return { success: false, message: `AUTH password failed: ${passResp.text}` };
37025
+ return { success: false, message: `AUTH password failed: ${passResp.text}`, id: null };
37372
37026
  }
37373
37027
  }
37374
37028
  const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
37375
37029
  if (mailFrom.code !== 250) {
37376
37030
  socket.destroy();
37377
- return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
37031
+ return { success: false, message: `MAIL FROM failed: ${mailFrom.text}`, id: null };
37378
37032
  }
37379
37033
  for (const recipient of allRecipients) {
37380
37034
  const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
37381
37035
  if (rcpt.code !== 250 && rcpt.code !== 251) {
37382
37036
  socket.destroy();
37383
- return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
37037
+ return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}`, id: null };
37384
37038
  }
37385
37039
  }
37386
37040
  const dataCmd = await sendCommand(socket, "DATA");
37387
37041
  if (dataCmd.code !== 354) {
37388
37042
  socket.destroy();
37389
- return { success: false, message: `DATA failed: ${dataCmd.text}` };
37043
+ return { success: false, message: `DATA failed: ${dataCmd.text}`, id: null };
37390
37044
  }
37391
37045
  const mimeMessage = buildMimeMessage({
37392
37046
  from: this.fromAddress,
@@ -37405,16 +37059,31 @@ var init_messenger = __esm({
37405
37059
  const endData = await sendCommand(socket, mimeMessage + "\r\n.");
37406
37060
  if (endData.code !== 250) {
37407
37061
  socket.destroy();
37408
- return { success: false, message: `Message delivery failed: ${endData.text}` };
37062
+ return { success: false, message: `Message delivery failed: ${endData.text}`, id: null };
37409
37063
  }
37410
37064
  await sendCommand(socket, "QUIT");
37411
37065
  socket.destroy();
37412
37066
  return { success: true, message: "Email sent successfully", id: messageId };
37413
37067
  } catch (err) {
37414
37068
  const errMsg = err instanceof Error ? err.message : String(err);
37415
- return { success: false, message: `SMTP error: ${errMsg}` };
37069
+ return { success: false, message: `SMTP error: ${errMsg}`, id: null };
37416
37070
  }
37417
37071
  }
37072
+ /**
37073
+ * Render a Frond template STRING and send it as an HTML email (G7, parity with
37074
+ * Python's send_template). Extra send() options (cc, bcc, replyTo, attachments,
37075
+ * headers) pass through. If the Frond package cannot be loaded the raw template
37076
+ * is sent verbatim (matches Python's ImportError fallback) rather than failing.
37077
+ */
37078
+ async sendTemplate(to, subject, template, data = {}, cc, bcc, replyTo, attachments, headers) {
37079
+ let body = template;
37080
+ try {
37081
+ const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
37082
+ body = new Frond2().renderString(template, data);
37083
+ } catch {
37084
+ }
37085
+ return this.send(to, subject, body, true, void 0, cc, bcc, replyTo, attachments, headers);
37086
+ }
37418
37087
  /**
37419
37088
  * Test the SMTP connection without sending an email.
37420
37089
  */
@@ -37423,14 +37092,14 @@ var init_messenger = __esm({
37423
37092
  let socket;
37424
37093
  if (this.port === 465) {
37425
37094
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
37426
- await new Promise((resolve31, reject) => {
37427
- socket.once("secureConnect", resolve31);
37095
+ await new Promise((resolve30, reject) => {
37096
+ socket.once("secureConnect", resolve30);
37428
37097
  socket.once("error", reject);
37429
37098
  });
37430
37099
  } else {
37431
37100
  socket = net3.createConnection({ host: this.host, port: this.port });
37432
- await new Promise((resolve31, reject) => {
37433
- socket.once("connect", resolve31);
37101
+ await new Promise((resolve30, reject) => {
37102
+ socket.once("connect", resolve30);
37434
37103
  socket.once("error", reject);
37435
37104
  });
37436
37105
  }
@@ -37465,14 +37134,14 @@ var init_messenger = __esm({
37465
37134
  const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
37466
37135
  if (useTls) {
37467
37136
  socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
37468
- await new Promise((resolve31, reject) => {
37469
- socket.once("secureConnect", resolve31);
37137
+ await new Promise((resolve30, reject) => {
37138
+ socket.once("secureConnect", resolve30);
37470
37139
  socket.once("error", reject);
37471
37140
  });
37472
37141
  } else {
37473
37142
  socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
37474
- await new Promise((resolve31, reject) => {
37475
- socket.once("connect", resolve31);
37143
+ await new Promise((resolve30, reject) => {
37144
+ socket.once("connect", resolve30);
37476
37145
  socket.once("error", reject);
37477
37146
  });
37478
37147
  }
@@ -37509,7 +37178,7 @@ var init_messenger = __esm({
37509
37178
  }
37510
37179
  try {
37511
37180
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37512
- const searchResp = await imapCommand(socket, "SEARCH ALL");
37181
+ const searchResp = await imapCommand(socket, "UID SEARCH ALL");
37513
37182
  const uids = parseSearchResponse(searchResp);
37514
37183
  if (uids.length === 0) return [];
37515
37184
  uids.reverse();
@@ -37517,8 +37186,8 @@ var init_messenger = __esm({
37517
37186
  if (selected.length === 0) return [];
37518
37187
  const messages = [];
37519
37188
  for (const uid of selected) {
37520
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
37521
- messages.push(parseHeaderResponse(uid, fetchResp));
37189
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
37190
+ messages.push(parseSummary(uid, fetchResp));
37522
37191
  }
37523
37192
  return messages;
37524
37193
  } catch (err) {
@@ -37528,7 +37197,7 @@ var init_messenger = __esm({
37528
37197
  }
37529
37198
  }
37530
37199
  /**
37531
- * Read a single message by sequence number or UID.
37200
+ * Read a single message by its IMAP UID.
37532
37201
  */
37533
37202
  async read(uid, folder = "INBOX") {
37534
37203
  let socket;
@@ -37539,11 +37208,11 @@ var init_messenger = __esm({
37539
37208
  }
37540
37209
  try {
37541
37210
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37542
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
37211
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY[])`);
37543
37212
  if (!/\{\d+\}/.test(fetchResp)) {
37544
37213
  return null;
37545
37214
  }
37546
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
37215
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
37547
37216
  return parseFullMessage(uid, fetchResp);
37548
37217
  } catch (err) {
37549
37218
  throw imapFail("read", err);
@@ -37570,14 +37239,14 @@ var init_messenger = __esm({
37570
37239
  }
37571
37240
  try {
37572
37241
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37573
- const searchResp = await imapCommand(socket, `SEARCH ${query}`);
37242
+ const searchResp = await imapCommand(socket, `UID SEARCH ${query}`);
37574
37243
  const uids = parseSearchResponse(searchResp);
37575
37244
  if (uids.length === 0) return [];
37576
37245
  uids.reverse();
37577
37246
  const messages = [];
37578
37247
  for (const uid of uids.slice(0, limit)) {
37579
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
37580
- messages.push(parseHeaderResponse(uid, fetchResp));
37248
+ const fetchResp = await imapCommand(socket, `UID FETCH ${uid} (FLAGS BODY.PEEK[])`);
37249
+ messages.push(parseSummary(uid, fetchResp));
37581
37250
  }
37582
37251
  return messages;
37583
37252
  } catch (err) {
@@ -37587,26 +37256,46 @@ var init_messenger = __esm({
37587
37256
  }
37588
37257
  }
37589
37258
  /**
37590
- * Delete a message by UID.
37259
+ * Delete a message by UID (mark \Deleted, then EXPUNGE).
37260
+ *
37261
+ * `delete` is the one cross-framework name (python/php/ruby/node all spell it
37262
+ * `delete`). `deleteMessage` remains as a DEPRECATED alias for one release.
37591
37263
  */
37592
- async deleteMessage(uid, folder = "INBOX") {
37264
+ async delete(uid, folder = "INBOX") {
37593
37265
  const socket = await this.imapConnect();
37594
37266
  try {
37595
37267
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37596
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
37268
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Deleted)`);
37597
37269
  await imapCommand(socket, "EXPUNGE");
37598
37270
  } finally {
37599
37271
  await this.imapDisconnect(socket);
37600
37272
  }
37601
37273
  }
37274
+ /** @deprecated Use {@link delete} — kept as an alias for one release (G7). */
37275
+ async deleteMessage(uid, folder = "INBOX") {
37276
+ return this.delete(uid, folder);
37277
+ }
37602
37278
  /**
37603
- * Mark a message as read.
37279
+ * Mark a message as read (+FLAGS \Seen).
37604
37280
  */
37605
37281
  async markRead(uid, folder = "INBOX") {
37606
37282
  const socket = await this.imapConnect();
37607
37283
  try {
37608
37284
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37609
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
37285
+ await imapCommand(socket, `UID STORE ${uid} +FLAGS (\\Seen)`);
37286
+ } finally {
37287
+ await this.imapDisconnect(socket);
37288
+ }
37289
+ }
37290
+ /**
37291
+ * Mark a message as unread (-FLAGS \Seen) — the inverse of markRead (G7,
37292
+ * parity with Python's mark_unread).
37293
+ */
37294
+ async markUnread(uid, folder = "INBOX") {
37295
+ const socket = await this.imapConnect();
37296
+ try {
37297
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37298
+ await imapCommand(socket, `UID STORE ${uid} -FLAGS (\\Seen)`);
37610
37299
  } finally {
37611
37300
  await this.imapDisconnect(socket);
37612
37301
  }
@@ -37623,7 +37312,7 @@ var init_messenger = __esm({
37623
37312
  }
37624
37313
  try {
37625
37314
  await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
37626
- const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
37315
+ const searchResp = await imapCommand(socket, "UID SEARCH UNSEEN");
37627
37316
  return parseSearchResponse(searchResp).length;
37628
37317
  } catch (err) {
37629
37318
  throw imapFail("unread", err);
@@ -38379,17 +38068,17 @@ __export(ai_exports, {
38379
38068
  skillBlock: () => skillBlock,
38380
38069
  writeOrMerge: () => writeOrMerge
38381
38070
  });
38382
- import { existsSync as existsSync27, mkdirSync as mkdirSync20, writeFileSync as writeFileSync18, readFileSync as readFileSync26 } from "node:fs";
38071
+ import { existsSync as existsSync26, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17, readFileSync as readFileSync25 } from "node:fs";
38383
38072
  import { homedir } from "node:os";
38384
- import { join as join30, resolve as resolve21, relative as relative10, dirname as dirname14 } from "node:path";
38073
+ import { join as join29, resolve as resolve20, relative as relative10, dirname as dirname13 } from "node:path";
38385
38074
  import { fileURLToPath as fileURLToPath7 } from "node:url";
38386
38075
  import { execSync as execSync2, execFileSync as execFileSync3 } from "node:child_process";
38387
38076
  import { createInterface } from "node:readline";
38388
38077
  function readVersion() {
38389
38078
  try {
38390
- const thisDir = dirname14(fileURLToPath7(import.meta.url));
38391
- const rootPkg = resolve21(thisDir, "..", "..", "..", "package.json");
38392
- const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
38079
+ const thisDir = dirname13(fileURLToPath7(import.meta.url));
38080
+ const rootPkg = resolve20(thisDir, "..", "..", "..", "package.json");
38081
+ const pkg = JSON.parse(readFileSync25(rootPkg, "utf-8"));
38393
38082
  return pkg.version ?? "0.0.0";
38394
38083
  } catch {
38395
38084
  return "0.0.0";
@@ -38438,8 +38127,8 @@ function downloadSkillsSync(jobs) {
38438
38127
  function installSkills(root = ".", targets) {
38439
38128
  const ref = skillsRef();
38440
38129
  const dests = targets ?? [
38441
- join30(resolve21(root), ".claude", "skills"),
38442
- join30(homedir(), ".claude", "skills")
38130
+ join29(resolve20(root), ".claude", "skills"),
38131
+ join29(homedir(), ".claude", "skills")
38443
38132
  ];
38444
38133
  const jobs = [];
38445
38134
  const index = /* @__PURE__ */ new Map();
@@ -38457,9 +38146,9 @@ function installSkills(root = ".", targets) {
38457
38146
  const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
38458
38147
  skillMdUrl[skill] = `${base}/SKILL.md`;
38459
38148
  for (const dest of dests) {
38460
- add(`${base}/SKILL.md`, join30(dest, skill, "SKILL.md"));
38149
+ add(`${base}/SKILL.md`, join29(dest, skill, "SKILL.md"));
38461
38150
  for (const r of spec.references) {
38462
- add(`${base}/references/${r}`, join30(dest, skill, "references", r));
38151
+ add(`${base}/references/${r}`, join29(dest, skill, "references", r));
38463
38152
  }
38464
38153
  }
38465
38154
  }
@@ -38471,10 +38160,10 @@ function installSkills(root = ".", targets) {
38471
38160
  return installed;
38472
38161
  }
38473
38162
  function isInstalled(root, tool) {
38474
- return existsSync27(join30(resolve21(root), tool.contextFile));
38163
+ return existsSync26(join29(resolve20(root), tool.contextFile));
38475
38164
  }
38476
38165
  function showMenu(root = ".") {
38477
- const r = resolve21(root);
38166
+ const r = resolve20(root);
38478
38167
  console.log("\n Tina4 AI Context Installer\n");
38479
38168
  for (let i = 0; i < AI_TOOLS.length; i++) {
38480
38169
  const tool = AI_TOOLS[i];
@@ -38492,16 +38181,16 @@ function showMenu(root = ".") {
38492
38181
  const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
38493
38182
  console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
38494
38183
  console.log();
38495
- return new Promise((resolve31) => {
38184
+ return new Promise((resolve30) => {
38496
38185
  const rl = createInterface({ input: process.stdin, output: process.stdout });
38497
38186
  rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
38498
38187
  rl.close();
38499
- resolve31(answer.trim());
38188
+ resolve30(answer.trim());
38500
38189
  });
38501
38190
  });
38502
38191
  }
38503
38192
  function installSelected(root, selection) {
38504
- const rootPath = resolve21(root);
38193
+ const rootPath = resolve20(root);
38505
38194
  const created = [];
38506
38195
  let indices;
38507
38196
  let doInstallTina4Ai = false;
@@ -38594,33 +38283,33 @@ function looksLikeOldFrameworkInstall(existing) {
38594
38283
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
38595
38284
  const block = skillBlock(contextFile);
38596
38285
  const [start2, end] = markersFor(contextFile);
38597
- if (!existsSync27(contextPath)) {
38598
- writeFileSync18(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38286
+ if (!existsSync26(contextPath)) {
38287
+ writeFileSync17(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38599
38288
  return "Installed";
38600
38289
  }
38601
- const existing = readFileSync26(contextPath, "utf-8");
38290
+ const existing = readFileSync25(contextPath, "utf-8");
38602
38291
  if (hasMarkers(existing, start2, end)) {
38603
- writeFileSync18(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
38292
+ writeFileSync17(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
38604
38293
  return "Refreshed skill block in";
38605
38294
  }
38606
38295
  if (looksLikeOldFrameworkInstall(existing)) {
38607
38296
  const head = existing.replace(/^\s+/, "");
38608
38297
  const preamble = existing.slice(0, existing.length - head.length);
38609
38298
  const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
38610
- writeFileSync18(contextPath, newContent, "utf-8");
38299
+ writeFileSync17(contextPath, newContent, "utf-8");
38611
38300
  return "Migrated (replaced old framework dump in)";
38612
38301
  }
38613
- writeFileSync18(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38302
+ writeFileSync17(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
38614
38303
  return "Appended skill block to";
38615
38304
  }
38616
38305
  function installForTool(root, tool, context) {
38617
38306
  const created = [];
38618
- const contextPath = join30(root, tool.contextFile);
38307
+ const contextPath = join29(root, tool.contextFile);
38619
38308
  if (tool.configDir) {
38620
- mkdirSync20(join30(root, tool.configDir), { recursive: true });
38309
+ mkdirSync19(join29(root, tool.configDir), { recursive: true });
38621
38310
  }
38622
- const parentDir = dirname14(contextPath);
38623
- mkdirSync20(parentDir, { recursive: true });
38311
+ const parentDir = dirname13(contextPath);
38312
+ mkdirSync19(parentDir, { recursive: true });
38624
38313
  const action = writeOrMerge(contextPath, tool.contextFile, context);
38625
38314
  const rel = relative10(root, contextPath);
38626
38315
  created.push(rel);
@@ -38655,7 +38344,7 @@ function installTina4Ai() {
38655
38344
  function installClaudeSkills(root) {
38656
38345
  const created = [];
38657
38346
  for (const skill of installSkills(root)) {
38658
- created.push(join30(".claude", "skills", skill));
38347
+ created.push(join29(".claude", "skills", skill));
38659
38348
  console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
38660
38349
  }
38661
38350
  return created;
@@ -38996,11 +38685,11 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
38996
38685
  }
38997
38686
  function generateClaudeCodeContext() {
38998
38687
  try {
38999
- const thisDir = dirname14(fileURLToPath7(import.meta.url));
39000
- const repoRoot = resolve21(thisDir, "..", "..", "..");
39001
- const claudeMdPath = join30(repoRoot, "CLAUDE.md");
39002
- if (existsSync27(claudeMdPath)) {
39003
- return readFileSync26(claudeMdPath, "utf-8");
38688
+ const thisDir = dirname13(fileURLToPath7(import.meta.url));
38689
+ const repoRoot = resolve20(thisDir, "..", "..", "..");
38690
+ const claudeMdPath = join29(repoRoot, "CLAUDE.md");
38691
+ if (existsSync26(claudeMdPath)) {
38692
+ return readFileSync25(claudeMdPath, "utf-8");
39004
38693
  }
39005
38694
  } catch {
39006
38695
  }
@@ -40992,7 +40681,6 @@ __export(src_exports3, {
40992
40681
  RouteRef: () => RouteRef,
40993
40682
  Router: () => Router,
40994
40683
  SafeString: () => SafeString2,
40995
- ScssCompiler: () => ScssCompiler,
40996
40684
  SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
40997
40685
  ServiceRunner: () => ServiceRunner,
40998
40686
  Session: () => Session,
@@ -41189,7 +40877,6 @@ var init_src3 = __esm({
41189
40877
  init_session();
41190
40878
  init_i18n();
41191
40879
  init_fakeData();
41192
- init_scss();
41193
40880
  init_queue();
41194
40881
  init_job();
41195
40882
  init_mqtt();
@@ -41464,20 +41151,20 @@ ${cdStep} npm install
41464
41151
  }
41465
41152
 
41466
41153
  // src/commands/serve.ts
41467
- import { resolve as resolve22 } from "node:path";
41468
- import { existsSync as existsSync28 } from "node:fs";
41154
+ import { resolve as resolve21 } from "node:path";
41155
+ import { existsSync as existsSync27 } from "node:fs";
41469
41156
  async function serveProject(options) {
41470
41157
  if (options.noReload) {
41471
41158
  process.env.TINA4_NO_RELOAD = "true";
41472
41159
  }
41473
41160
  const port = options.port ?? 7148;
41474
41161
  const cwd = process.cwd();
41475
- const routesDir = resolve22(cwd, "src/routes");
41476
- const ormDir = resolve22(cwd, "src/orm");
41477
- const modelsDir = resolve22(cwd, "src/models");
41478
- const templatesDir = resolve22(cwd, "src/templates");
41479
- const staticDir = resolve22(cwd, "public");
41480
- if (!existsSync28(routesDir) && !existsSync28(modelsDir) && !existsSync28(ormDir)) {
41162
+ const routesDir = resolve21(cwd, "src/routes");
41163
+ const ormDir = resolve21(cwd, "src/orm");
41164
+ const modelsDir = resolve21(cwd, "src/models");
41165
+ const templatesDir = resolve21(cwd, "src/templates");
41166
+ const staticDir = resolve21(cwd, "public");
41167
+ if (!existsSync27(routesDir) && !existsSync27(modelsDir) && !existsSync27(ormDir)) {
41481
41168
  console.error(" Error: Not a Tina4 project. Run this from a project created with 'tina4 init'.");
41482
41169
  process.exit(1);
41483
41170
  }
@@ -41493,12 +41180,12 @@ async function serveProject(options) {
41493
41180
 
41494
41181
  // src/commands/migrate.ts
41495
41182
  init_dotenv();
41496
- import { existsSync as existsSync29, readdirSync as readdirSync20, readFileSync as readFileSync27 } from "node:fs";
41497
- import { join as join31, resolve as resolve23 } from "node:path";
41183
+ import { existsSync as existsSync28, readdirSync as readdirSync19, readFileSync as readFileSync26 } from "node:fs";
41184
+ import { join as join30, resolve as resolve22 } from "node:path";
41498
41185
  async function runMigrations(migrationDir) {
41499
41186
  loadEnv();
41500
- const dir = resolve23(migrationDir ?? "migrations");
41501
- if (!existsSync29(dir)) {
41187
+ const dir = resolve22(migrationDir ?? "migrations");
41188
+ if (!existsSync28(dir)) {
41502
41189
  console.log(" No migrations/ directory found. Nothing to run.");
41503
41190
  return;
41504
41191
  }
@@ -41529,7 +41216,7 @@ async function runMigrations(migrationDir) {
41529
41216
  process.exit(1);
41530
41217
  }
41531
41218
  await ensureMigrationTable2();
41532
- const files = readdirSync20(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql")).sort((a, b) => {
41219
+ const files = readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql")).sort((a, b) => {
41533
41220
  const aMatch = a.match(/^(\d+)/);
41534
41221
  const bMatch = b.match(/^(\d+)/);
41535
41222
  if (aMatch && bMatch) {
@@ -41551,7 +41238,7 @@ async function runMigrations(migrationDir) {
41551
41238
  if (await isMigrationApplied2(name)) {
41552
41239
  continue;
41553
41240
  }
41554
- const sql = readFileSync27(join31(dir, file), "utf-8").trim();
41241
+ const sql = readFileSync26(join30(dir, file), "utf-8").trim();
41555
41242
  if (!sql) continue;
41556
41243
  console.log(` Migrating: ${file}`);
41557
41244
  const adapter = getAdapter2();
@@ -41576,17 +41263,17 @@ async function runMigrations(migrationDir) {
41576
41263
  }
41577
41264
 
41578
41265
  // src/commands/migrateCreate.ts
41579
- import { existsSync as existsSync30, mkdirSync as mkdirSync21, writeFileSync as writeFileSync19 } from "node:fs";
41580
- import { join as join32, resolve as resolve24 } from "node:path";
41266
+ import { existsSync as existsSync29, mkdirSync as mkdirSync20, writeFileSync as writeFileSync18 } from "node:fs";
41267
+ import { join as join31, resolve as resolve23 } from "node:path";
41581
41268
  async function createMigration2(description) {
41582
41269
  if (!description) {
41583
41270
  console.error(" Usage: tina4 migrate:create <description>");
41584
41271
  console.error(' Example: tina4 migrate:create "create users table"');
41585
41272
  process.exit(1);
41586
41273
  }
41587
- const dir = resolve24("migrations");
41588
- if (!existsSync30(dir)) {
41589
- mkdirSync21(dir, { recursive: true });
41274
+ const dir = resolve23("migrations");
41275
+ if (!existsSync29(dir)) {
41276
+ mkdirSync20(dir, { recursive: true });
41590
41277
  }
41591
41278
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
41592
41279
  const now = /* @__PURE__ */ new Date();
@@ -41600,8 +41287,8 @@ async function createMigration2(description) {
41600
41287
  ].join("");
41601
41288
  const upFileName = `${timestamp2}_${safeName}.sql`;
41602
41289
  const downFileName = `${timestamp2}_${safeName}.down.sql`;
41603
- const upPath = join32(dir, upFileName);
41604
- const downPath = join32(dir, downFileName);
41290
+ const upPath = join31(dir, upFileName);
41291
+ const downPath = join31(dir, downFileName);
41605
41292
  const upTemplate = `-- Migration: ${description}
41606
41293
  -- Created: ${now.toISOString()}
41607
41294
 
@@ -41610,8 +41297,8 @@ async function createMigration2(description) {
41610
41297
  -- Created: ${now.toISOString()}
41611
41298
 
41612
41299
  `;
41613
- writeFileSync19(upPath, upTemplate, "utf-8");
41614
- writeFileSync19(downPath, downTemplate, "utf-8");
41300
+ writeFileSync18(upPath, upTemplate, "utf-8");
41301
+ writeFileSync18(downPath, downTemplate, "utf-8");
41615
41302
  console.log(` Created migration: ${upFileName}`);
41616
41303
  console.log(` Created rollback: ${downFileName}`);
41617
41304
  console.log(` Path: ${dir}`);
@@ -41619,10 +41306,10 @@ async function createMigration2(description) {
41619
41306
 
41620
41307
  // src/commands/migrateStatus.ts
41621
41308
  init_dotenv();
41622
- import { resolve as resolve25 } from "node:path";
41309
+ import { resolve as resolve24 } from "node:path";
41623
41310
  async function migrateStatus(migrationDir) {
41624
41311
  loadEnv();
41625
- const dir = resolve25(migrationDir ?? "migrations");
41312
+ const dir = resolve24(migrationDir ?? "migrations");
41626
41313
  let initDatabase2;
41627
41314
  let ensureMigrationTable2;
41628
41315
  let statusFn;
@@ -41667,10 +41354,10 @@ async function migrateStatus(migrationDir) {
41667
41354
 
41668
41355
  // src/commands/migrateRollback.ts
41669
41356
  init_dotenv();
41670
- import { resolve as resolve26 } from "node:path";
41357
+ import { resolve as resolve25 } from "node:path";
41671
41358
  async function migrateRollback(migrationDir) {
41672
41359
  loadEnv();
41673
- const dir = resolve26(migrationDir ?? "migrations");
41360
+ const dir = resolve25(migrationDir ?? "migrations");
41674
41361
  let initDatabase2;
41675
41362
  let ensureMigrationTable2;
41676
41363
  let rollbackFn;
@@ -41710,11 +41397,11 @@ async function migrateRollback(migrationDir) {
41710
41397
  }
41711
41398
 
41712
41399
  // src/commands/routes.ts
41713
- import { existsSync as existsSync31 } from "node:fs";
41714
- import { resolve as resolve27 } from "node:path";
41400
+ import { existsSync as existsSync30 } from "node:fs";
41401
+ import { resolve as resolve26 } from "node:path";
41715
41402
  async function listRoutes() {
41716
- const routesDir = resolve27("src/routes");
41717
- if (!existsSync31(routesDir)) {
41403
+ const routesDir = resolve26("src/routes");
41404
+ if (!existsSync30(routesDir)) {
41718
41405
  console.error(" No src/routes/ directory found. Are you in a Tina4 project?");
41719
41406
  process.exit(1);
41720
41407
  }
@@ -41754,14 +41441,14 @@ async function listRoutes() {
41754
41441
  }
41755
41442
 
41756
41443
  // src/commands/test.ts
41757
- import { existsSync as existsSync32, readdirSync as readdirSync22 } from "node:fs";
41758
- import { resolve as resolve28, join as join33 } from "node:path";
41444
+ import { existsSync as existsSync31, readdirSync as readdirSync21 } from "node:fs";
41445
+ import { resolve as resolve27, join as join32 } from "node:path";
41759
41446
  import { execSync as execSync3 } from "node:child_process";
41760
41447
  async function runTests(testPath) {
41761
41448
  const cwd = process.cwd();
41762
41449
  if (testPath) {
41763
- const file = resolve28(testPath);
41764
- if (!existsSync32(file)) {
41450
+ const file = resolve27(testPath);
41451
+ if (!existsSync31(file)) {
41765
41452
  console.error(` Error: Test file not found: ${testPath}`);
41766
41453
  process.exit(1);
41767
41454
  }
@@ -41781,14 +41468,14 @@ async function runTests(testPath) {
41781
41468
  ];
41782
41469
  let testFiles = [];
41783
41470
  for (const candidate of candidates) {
41784
- const fullPath = resolve28(cwd, candidate);
41785
- if (!existsSync32(fullPath)) continue;
41471
+ const fullPath = resolve27(cwd, candidate);
41472
+ if (!existsSync31(fullPath)) continue;
41786
41473
  if (candidate.endsWith(".ts")) {
41787
41474
  testFiles.push(fullPath);
41788
41475
  break;
41789
41476
  }
41790
41477
  try {
41791
- const files = readdirSync22(fullPath).filter((f) => f.endsWith(".ts")).map((f) => join33(fullPath, f));
41478
+ const files = readdirSync21(fullPath).filter((f) => f.endsWith(".ts")).map((f) => join32(fullPath, f));
41792
41479
  testFiles.push(...files);
41793
41480
  } catch {
41794
41481
  }
@@ -41817,8 +41504,8 @@ async function runTests(testPath) {
41817
41504
  }
41818
41505
 
41819
41506
  // src/commands/generate.ts
41820
- import { existsSync as existsSync33, mkdirSync as mkdirSync22, writeFileSync as writeFileSync20 } from "node:fs";
41821
- import { join as join34, resolve as resolve29 } from "node:path";
41507
+ import { existsSync as existsSync32, mkdirSync as mkdirSync21, writeFileSync as writeFileSync19 } from "node:fs";
41508
+ import { join as join33, resolve as resolve28 } from "node:path";
41822
41509
  var FIELD_TYPE_MAP = {
41823
41510
  string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
41824
41511
  str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
@@ -41835,16 +41522,16 @@ var FIELD_TYPE_MAP = {
41835
41522
  blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
41836
41523
  };
41837
41524
  function ensureDir(dir) {
41838
- if (!existsSync33(dir)) {
41839
- mkdirSync22(dir, { recursive: true });
41525
+ if (!existsSync32(dir)) {
41526
+ mkdirSync21(dir, { recursive: true });
41840
41527
  }
41841
41528
  }
41842
41529
  function writeFileSafe(path8, content) {
41843
- if (existsSync33(path8)) {
41530
+ if (existsSync32(path8)) {
41844
41531
  console.log(` File already exists: ${path8}`);
41845
41532
  return;
41846
41533
  }
41847
- writeFileSync20(path8, content, "utf-8");
41534
+ writeFileSync19(path8, content, "utf-8");
41848
41535
  console.log(` Created ${path8}`);
41849
41536
  }
41850
41537
  function toSnake(name) {
@@ -42001,9 +41688,9 @@ async function generate2(what, name, extraArgs = []) {
42001
41688
  function generateModel(name, flags, emitTest = true) {
42002
41689
  const fields = fieldsOrDefault(flags.fields || "");
42003
41690
  const table2 = toTableName(name);
42004
- const dir = resolve29("src/models");
41691
+ const dir = resolve28("src/models");
42005
41692
  ensureDir(dir);
42006
- const path8 = join34(dir, `${name}.ts`);
41693
+ const path8 = join33(dir, `${name}.ts`);
42007
41694
  const fieldLines = [
42008
41695
  ` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`
42009
41696
  ];
@@ -42037,8 +41724,8 @@ function generateRoute(name, flags, emitTest = true) {
42037
41724
  const singular = routePath.endsWith("s") ? routePath.slice(0, -1) : routePath;
42038
41725
  const model = flags.model;
42039
41726
  const isPublic = Boolean(flags.public);
42040
- const base = resolve29("src/routes/api", routePath);
42041
- const idDir = join34(base, "[id]");
41727
+ const base = resolve28("src/routes/api", routePath);
41728
+ const idDir = join33(base, "[id]");
42042
41729
  ensureDir(base);
42043
41730
  ensureDir(idDir);
42044
41731
  const table2 = model ? toTableName(model) : "";
@@ -42049,7 +41736,7 @@ function generateRoute(name, flags, emitTest = true) {
42049
41736
  const writeDoc = isPublic ? "Public (--public): no token required." : "Secure by default: requires a Bearer token (use --public to open).";
42050
41737
  if (model) {
42051
41738
  writeFileSafe(
42052
- join34(base, "get.ts"),
41739
+ join33(base, "get.ts"),
42053
41740
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42054
41741
  ${modelImportBase}
42055
41742
  export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
@@ -42065,7 +41752,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42065
41752
  );
42066
41753
  } else {
42067
41754
  writeFileSafe(
42068
- join34(base, "get.ts"),
41755
+ join33(base, "get.ts"),
42069
41756
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42070
41757
 
42071
41758
  export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
@@ -42084,7 +41771,7 @@ ${aiFill(`list_${routePath}`, {
42084
41771
  }
42085
41772
  if (model) {
42086
41773
  writeFileSafe(
42087
- join34(base, "post.ts"),
41774
+ join33(base, "post.ts"),
42088
41775
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42089
41776
  ${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
42090
41777
 
@@ -42106,7 +41793,7 @@ ${extend(
42106
41793
  );
42107
41794
  } else {
42108
41795
  writeFileSafe(
42109
- join34(base, "post.ts"),
41796
+ join33(base, "post.ts"),
42110
41797
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42111
41798
 
42112
41799
  ${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
@@ -42126,7 +41813,7 @@ ${aiFill(`create_${singular}`, {
42126
41813
  }
42127
41814
  if (model) {
42128
41815
  writeFileSafe(
42129
- join34(idDir, "get.ts"),
41816
+ join33(idDir, "get.ts"),
42130
41817
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42131
41818
  ${modelImportId}
42132
41819
  export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
@@ -42144,7 +41831,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42144
41831
  );
42145
41832
  } else {
42146
41833
  writeFileSafe(
42147
- join34(idDir, "get.ts"),
41834
+ join33(idDir, "get.ts"),
42148
41835
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42149
41836
 
42150
41837
  export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
@@ -42163,7 +41850,7 @@ ${aiFill(`get_${singular}`, {
42163
41850
  }
42164
41851
  if (model) {
42165
41852
  writeFileSafe(
42166
- join34(idDir, "put.ts"),
41853
+ join33(idDir, "put.ts"),
42167
41854
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42168
41855
  ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
42169
41856
 
@@ -42191,7 +41878,7 @@ ${extend(
42191
41878
  );
42192
41879
  } else {
42193
41880
  writeFileSafe(
42194
- join34(idDir, "put.ts"),
41881
+ join33(idDir, "put.ts"),
42195
41882
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42196
41883
 
42197
41884
  ${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
@@ -42211,7 +41898,7 @@ ${aiFill(`update_${singular}`, {
42211
41898
  }
42212
41899
  if (model) {
42213
41900
  writeFileSafe(
42214
- join34(idDir, "delete.ts"),
41901
+ join33(idDir, "delete.ts"),
42215
41902
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42216
41903
  ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
42217
41904
 
@@ -42230,7 +41917,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42230
41917
  );
42231
41918
  } else {
42232
41919
  writeFileSafe(
42233
- join34(idDir, "delete.ts"),
41920
+ join33(idDir, "delete.ts"),
42234
41921
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42235
41922
 
42236
41923
  ${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
@@ -42275,7 +41962,7 @@ function generateCrud(name, flags) {
42275
41962
  }
42276
41963
  function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
42277
41964
  const ts = timestamp();
42278
- const dir = resolve29("migrations");
41965
+ const dir = resolve28("migrations");
42279
41966
  ensureDir(dir);
42280
41967
  let table2;
42281
41968
  if (tableOverride) {
@@ -42287,7 +41974,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
42287
41974
  const fields = fieldsOverride || parseFields(flags.fields || "");
42288
41975
  const isCreate = name.startsWith("create_") || fieldsOverride !== void 0;
42289
41976
  const fileName = `${ts}_${name}.sql`;
42290
- const path8 = join34(dir, fileName);
41977
+ const path8 = join33(dir, fileName);
42291
41978
  let upSql;
42292
41979
  let downSql;
42293
41980
  if (isCreate) {
@@ -42319,7 +42006,7 @@ ${upSql}
42319
42006
  ${downSql}
42320
42007
  `;
42321
42008
  writeFileSafe(path8, content);
42322
- const downPath = join34(dir, `${ts}_${name}.down.sql`);
42009
+ const downPath = join33(dir, `${ts}_${name}.down.sql`);
42323
42010
  const downContent = `-- Rollback: ${name}
42324
42011
  -- Created: ${now}
42325
42012
 
@@ -42330,9 +42017,9 @@ ${downSql}
42330
42017
  }
42331
42018
  function generateMiddleware(name, _flags) {
42332
42019
  const snake = toSnake(name);
42333
- const dir = resolve29("src/middleware");
42020
+ const dir = resolve28("src/middleware");
42334
42021
  ensureDir(dir);
42335
- const path8 = join34(dir, `${snake}.ts`);
42022
+ const path8 = join33(dir, `${snake}.ts`);
42336
42023
  const content = `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42337
42024
 
42338
42025
  /**
@@ -42371,9 +42058,9 @@ function generateTest(name, flags) {
42371
42058
  const snake = toSnake(name);
42372
42059
  const singular = snake.endsWith("s") ? snake.slice(0, -1) : snake;
42373
42060
  const model = flags.model;
42374
- const dir = resolve29("tests");
42061
+ const dir = resolve28("tests");
42375
42062
  ensureDir(dir);
42376
- const path8 = join34(dir, `${snake}.test.ts`);
42063
+ const path8 = join33(dir, `${snake}.test.ts`);
42377
42064
  if (model && flags["secure-writes"]) {
42378
42065
  const isPublic = Boolean(flags.public);
42379
42066
  const posture = isPublic ? "open (--public)" : "gated";
@@ -42508,9 +42195,9 @@ function generateForm(name, flags) {
42508
42195
  datetime: "datetime-local",
42509
42196
  blob: "file"
42510
42197
  };
42511
- const dir = resolve29("src/templates/forms");
42198
+ const dir = resolve28("src/templates/forms");
42512
42199
  ensureDir(dir);
42513
- const path8 = join34(dir, `${table2}.twig`);
42200
+ const path8 = join33(dir, `${table2}.twig`);
42514
42201
  let fieldHtml = "";
42515
42202
  for (const [fname, ftype] of fields) {
42516
42203
  const itype = inputTypes[ftype] || "text";
@@ -42560,9 +42247,9 @@ function generateView(name, flags) {
42560
42247
  const table2 = toTableName(name);
42561
42248
  const routeName = toPlural(table2);
42562
42249
  const cols = fields.map(([f]) => f);
42563
- const dir = resolve29("src/templates/pages");
42250
+ const dir = resolve28("src/templates/pages");
42564
42251
  ensureDir(dir);
42565
- const listPath = join34(dir, `${routeName}.twig`);
42252
+ const listPath = join33(dir, `${routeName}.twig`);
42566
42253
  const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
42567
42254
  const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
42568
42255
  const listContent = `{% extends "base.twig" %}
@@ -42598,7 +42285,7 @@ ${td}
42598
42285
  {% endblock %}
42599
42286
  `;
42600
42287
  writeFileSafe(listPath, listContent);
42601
- const detailPath = join34(dir, `${table2}.twig`);
42288
+ const detailPath = join33(dir, `${table2}.twig`);
42602
42289
  const detailFields = cols.map((c) => ` <div class="mb-3"><strong>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}:</strong> {{ item.${c} }}</div>`).join("\n");
42603
42290
  const detailContent = `{% extends "base.twig" %}
42604
42291
  {% block title %}${name} Detail{% endblock %}
@@ -42620,14 +42307,14 @@ ${detailFields}
42620
42307
  function generateAuth(_flags) {
42621
42308
  console.log("\n Generating authentication scaffolding...\n");
42622
42309
  generateModel("User", { fields: "email:string,password:string,role:string" }, false);
42623
- const registerDir = resolve29("src/routes/api/auth/register");
42624
- const loginDir = resolve29("src/routes/api/auth/login");
42625
- const meDir = resolve29("src/routes/api/auth/me");
42310
+ const registerDir = resolve28("src/routes/api/auth/register");
42311
+ const loginDir = resolve28("src/routes/api/auth/login");
42312
+ const meDir = resolve28("src/routes/api/auth/me");
42626
42313
  ensureDir(registerDir);
42627
42314
  ensureDir(loginDir);
42628
42315
  ensureDir(meDir);
42629
42316
  writeFileSafe(
42630
- join34(registerDir, "post.ts"),
42317
+ join33(registerDir, "post.ts"),
42631
42318
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42632
42319
  import { hashPassword } from "tina4-nodejs";
42633
42320
  import User from "../../../../models/User.js";
@@ -42658,7 +42345,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42658
42345
  `
42659
42346
  );
42660
42347
  writeFileSafe(
42661
- join34(loginDir, "post.ts"),
42348
+ join33(loginDir, "post.ts"),
42662
42349
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42663
42350
  import { checkPassword, getToken } from "tina4-nodejs";
42664
42351
  import User from "../../../../models/User.js";
@@ -42689,7 +42376,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42689
42376
  `
42690
42377
  );
42691
42378
  writeFileSafe(
42692
- join34(meDir, "get.ts"),
42379
+ join33(meDir, "get.ts"),
42693
42380
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
42694
42381
  import { authenticateRequest } from "tina4-nodejs";
42695
42382
 
@@ -42705,10 +42392,10 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42705
42392
  }
42706
42393
  `
42707
42394
  );
42708
- const formsDir = resolve29("src/templates/forms");
42395
+ const formsDir = resolve28("src/templates/forms");
42709
42396
  ensureDir(formsDir);
42710
42397
  writeFileSafe(
42711
- join34(formsDir, "login.twig"),
42398
+ join33(formsDir, "login.twig"),
42712
42399
  `{% extends "base.twig" %}
42713
42400
  {% block title %}Login{% endblock %}
42714
42401
  {% block content %}
@@ -42732,7 +42419,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
42732
42419
  `
42733
42420
  );
42734
42421
  writeFileSafe(
42735
- join34(formsDir, "register.twig"),
42422
+ join33(formsDir, "register.twig"),
42736
42423
  `{% extends "base.twig" %}
42737
42424
  {% block title %}Register{% endblock %}
42738
42425
  {% block content %}
@@ -42766,9 +42453,9 @@ function generateService(name, flags) {
42766
42453
  const snake = toSnake(name);
42767
42454
  const camel = toCamel(toPascal(name)) || snake;
42768
42455
  const cron = flags.cron;
42769
- const dir = resolve29("src/services");
42456
+ const dir = resolve28("src/services");
42770
42457
  ensureDir(dir);
42771
- const path8 = join34(dir, `${snake}.ts`);
42458
+ const path8 = join33(dir, `${snake}.ts`);
42772
42459
  let scheduleField;
42773
42460
  let note;
42774
42461
  if (cron && cron !== true) {
@@ -42817,9 +42504,9 @@ function generateQueue(name, _flags) {
42817
42504
  const topic = name.replace(/^\//, "");
42818
42505
  const slug = toSnake(topic.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
42819
42506
  const pascal = toPascal(topic) || "Topic";
42820
- const dir = resolve29("src/services");
42507
+ const dir = resolve28("src/services");
42821
42508
  ensureDir(dir);
42822
- const path8 = join34(dir, `${slug}_consumer.ts`);
42509
+ const path8 = join33(dir, `${slug}_consumer.ts`);
42823
42510
  const body = aiFill(`handle${pascal}`, {
42824
42511
  intent: `process ONE ${topic} job payload`,
42825
42512
  given: "payload -> the produced job data (job.payload)",
@@ -42877,9 +42564,9 @@ export default {
42877
42564
  emitQueueTest(topic, slug, pascal);
42878
42565
  }
42879
42566
  function generateValidator(name, _flags) {
42880
- const dir = resolve29("src/validators");
42567
+ const dir = resolve28("src/validators");
42881
42568
  ensureDir(dir);
42882
- const path8 = join34(dir, `${toSnake(name)}.ts`);
42569
+ const path8 = join33(dir, `${toSnake(name)}.ts`);
42883
42570
  const rules = extend(
42884
42571
  "add / adjust the validation rules for this payload",
42885
42572
  `e.g. .email("email").minLength("name", 2).integer("age"); ground: tina4_context("validate request body with Validator", "nodejs")`
@@ -42904,9 +42591,9 @@ ${rules} validator.required("name"); // starter rule (matches the model's def
42904
42591
  }
42905
42592
  function generateSeeder(name, _flags) {
42906
42593
  const table2 = toTableName(name);
42907
- const dir = resolve29("src/seeds");
42594
+ const dir = resolve28("src/seeds");
42908
42595
  ensureDir(dir);
42909
- const path8 = join34(dir, `${table2}_seeder.ts`);
42596
+ const path8 = join33(dir, `${table2}_seeder.ts`);
42910
42597
  const overrides = extend(
42911
42598
  "override fields that need a specific shape (seedOrm auto-fills the rest)",
42912
42599
  `e.g. return { email: (f) => f.email(), status: "active" }; ground: tina4_context("seed ORM model with FakeData", "nodejs")`
@@ -42948,9 +42635,9 @@ function generateWebsocket(name, _flags) {
42948
42635
  let slug = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
42949
42636
  const base = slug.startsWith("ws_") ? slug.slice(3) : slug;
42950
42637
  const handlerName = `${toCamel(toPascal(base))}Ws`;
42951
- const dir = resolve29("src/routes");
42638
+ const dir = resolve28("src/routes");
42952
42639
  ensureDir(dir);
42953
- const path8 = join34(dir, `ws_${base}.ts`);
42640
+ const path8 = join33(dir, `ws_${base}.ts`);
42954
42641
  const body = aiFill(handlerName, {
42955
42642
  intent: `handle an inbound "message" frame on ${wsPath}`,
42956
42643
  given: "data -> the message payload (string); connection -> WebSocketConnection",
@@ -42997,9 +42684,9 @@ function generateListener(name, _flags) {
42997
42684
  const event = name.trim();
42998
42685
  const slug = toSnake(event.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
42999
42686
  const handlerName = `on${toPascal(slug)}`;
43000
- const dir = resolve29("src/listeners");
42687
+ const dir = resolve28("src/listeners");
43001
42688
  ensureDir(dir);
43002
- const path8 = join34(dir, `${slug}.ts`);
42689
+ const path8 = join33(dir, `${slug}.ts`);
43003
42690
  const body = aiFill(handlerName, {
43004
42691
  intent: `react to the '${event}' event`,
43005
42692
  given: `args -> whatever Events.emit("${event}", ...args) passed`,
@@ -43028,9 +42715,9 @@ Events.on("${event}", ${handlerName});
43028
42715
  emitListenerTest(event, slug);
43029
42716
  }
43030
42717
  function writeTest(testName, content) {
43031
- const dir = resolve29("tests");
42718
+ const dir = resolve28("tests");
43032
42719
  ensureDir(dir);
43033
- writeFileSafe(join34(dir, `${testName}.test.ts`), content);
42720
+ writeFileSafe(join33(dir, `${testName}.test.ts`), content);
43034
42721
  }
43035
42722
  function standaloneTest(doc, body) {
43036
42723
  return `${doc}
@@ -43414,15 +43101,15 @@ assert("DOWN drops the ${table2} table", db.tableExists("${table2}") === false);
43414
43101
  }
43415
43102
 
43416
43103
  // src/commands/seed.ts
43417
- import { existsSync as existsSync34, readdirSync as readdirSync23 } from "node:fs";
43418
- import { resolve as resolve30, join as join35 } from "node:path";
43104
+ import { existsSync as existsSync33, readdirSync as readdirSync22 } from "node:fs";
43105
+ import { resolve as resolve29, join as join34 } from "node:path";
43419
43106
  import { execSync as execSync4 } from "node:child_process";
43420
43107
  async function runSeeds(seedPath) {
43421
43108
  const cwd = process.cwd();
43422
- const seedDir = resolve30(cwd, "src/seeds");
43109
+ const seedDir = resolve29(cwd, "src/seeds");
43423
43110
  if (seedPath) {
43424
- const file = resolve30(seedPath);
43425
- if (!existsSync34(file)) {
43111
+ const file = resolve29(seedPath);
43112
+ if (!existsSync33(file)) {
43426
43113
  console.error(` Error: Seed file not found: ${seedPath}`);
43427
43114
  process.exit(1);
43428
43115
  }
@@ -43435,14 +43122,14 @@ async function runSeeds(seedPath) {
43435
43122
  }
43436
43123
  return;
43437
43124
  }
43438
- if (!existsSync34(seedDir)) {
43125
+ if (!existsSync33(seedDir)) {
43439
43126
  console.log(" No seeds directory found.");
43440
43127
  console.log(" Create seed files in src/seeds/ (e.g. src/seeds/001-users.ts)");
43441
43128
  return;
43442
43129
  }
43443
43130
  let seedFiles;
43444
43131
  try {
43445
- seedFiles = readdirSync23(seedDir).filter((f) => f.endsWith(".ts")).sort().map((f) => join35(seedDir, f));
43132
+ seedFiles = readdirSync22(seedDir).filter((f) => f.endsWith(".ts")).sort().map((f) => join34(seedDir, f));
43446
43133
  } catch {
43447
43134
  console.log(" Could not read src/seeds/ directory.");
43448
43135
  return;
@@ -43580,8 +43267,8 @@ function runMetrics(args = []) {
43580
43267
  // src/commands/queue.ts
43581
43268
  init_dotenv();
43582
43269
  init_queue();
43583
- import { readdirSync as readdirSync24, statSync as statSync19 } from "node:fs";
43584
- import { extname as extname9, join as join36 } from "node:path";
43270
+ import { readdirSync as readdirSync23, statSync as statSync19 } from "node:fs";
43271
+ import { extname as extname9, join as join35 } from "node:path";
43585
43272
  import { pathToFileURL as pathToFileURL2 } from "node:url";
43586
43273
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["once", "json"]);
43587
43274
  function parseFlags2(args) {
@@ -43613,7 +43300,7 @@ async function resolveQueueHandler(servicesDir, topic) {
43613
43300
  let entries;
43614
43301
  try {
43615
43302
  if (!statSync19(servicesDir).isDirectory()) return null;
43616
- entries = readdirSync24(servicesDir);
43303
+ entries = readdirSync23(servicesDir);
43617
43304
  } catch {
43618
43305
  return null;
43619
43306
  }
@@ -43621,7 +43308,7 @@ async function resolveQueueHandler(servicesDir, topic) {
43621
43308
  if (entry.startsWith("_")) continue;
43622
43309
  const ext = extname9(entry);
43623
43310
  if (ext !== ".ts" && ext !== ".js") continue;
43624
- const fullPath = join36(servicesDir, entry);
43311
+ const fullPath = join35(servicesDir, entry);
43625
43312
  try {
43626
43313
  if (!statSync19(fullPath).isFile()) continue;
43627
43314
  const mod = await import(pathToFileURL2(fullPath).href);
@@ -43773,8 +43460,8 @@ async function queueCommand(args = []) {
43773
43460
  }
43774
43461
 
43775
43462
  // src/commands/build.ts
43776
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync35, statSync as statSync20 } from "node:fs";
43777
- import { basename as basename8, delimiter as delimiter2, join as join37 } from "node:path";
43463
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync34, statSync as statSync20 } from "node:fs";
43464
+ import { basename as basename7, delimiter as delimiter2, join as join36 } from "node:path";
43778
43465
  import { spawnSync as spawnSync3 } from "node:child_process";
43779
43466
  function parseFlags3(args) {
43780
43467
  const flags = {};
@@ -43803,7 +43490,7 @@ function whichDocker() {
43803
43490
  for (const dir of pathValue.split(delimiter2)) {
43804
43491
  if (!dir) continue;
43805
43492
  for (const ext of exts) {
43806
- const candidate = join37(dir, `docker${ext}`);
43493
+ const candidate = join36(dir, `docker${ext}`);
43807
43494
  try {
43808
43495
  accessSync2(candidate, constants2.X_OK);
43809
43496
  if (statSync20(candidate).isFile()) return candidate;
@@ -43817,11 +43504,11 @@ function buildImage(args) {
43817
43504
  const flags = parseFlags3(args);
43818
43505
  let tag = typeof flags.tag === "string" ? flags.tag : "";
43819
43506
  if (!tag) {
43820
- const dirName = basename8(process.cwd()).toLowerCase();
43507
+ const dirName = basename7(process.cwd()).toLowerCase();
43821
43508
  tag = `${dirName || "tina4app"}:latest`;
43822
43509
  }
43823
43510
  const dockerfile = typeof flags.file === "string" && flags.file ? flags.file : "Dockerfile";
43824
- if (!existsSync35(dockerfile) || !statSync20(dockerfile).isFile()) {
43511
+ if (!existsSync34(dockerfile) || !statSync20(dockerfile).isFile()) {
43825
43512
  console.log(` \u2717 No ${dockerfile} found.`);
43826
43513
  console.log(" A Tina4 app deploys as a container. Scaffold a Dockerfile first:");
43827
43514
  console.log(" tina4 deploy docker (or: tina4nodejs init)");
@@ -43849,30 +43536,30 @@ function buildImage(args) {
43849
43536
 
43850
43537
  // src/bin.ts
43851
43538
  import { execSync as execSync5, spawnSync as spawnSync4 } from "node:child_process";
43852
- import { existsSync as existsSync36, readFileSync as readFileSync28, statSync as statSync21 } from "node:fs";
43853
- import { delimiter as delimiter3, dirname as dirname15, join as join38 } from "node:path";
43539
+ import { existsSync as existsSync35, readFileSync as readFileSync27, statSync as statSync21 } from "node:fs";
43540
+ import { delimiter as delimiter3, dirname as dirname14, join as join37 } from "node:path";
43854
43541
  import { fileURLToPath as fileURLToPath8, pathToFileURL as pathToFileURL3 } from "node:url";
43855
43542
  function readCliVersion() {
43856
- let dir = dirname15(fileURLToPath8(import.meta.url));
43543
+ let dir = dirname14(fileURLToPath8(import.meta.url));
43857
43544
  for (let i = 0; i < 6; i++) {
43858
- const pkgPath = join38(dir, "package.json");
43859
- if (existsSync36(pkgPath)) {
43545
+ const pkgPath = join37(dir, "package.json");
43546
+ if (existsSync35(pkgPath)) {
43860
43547
  try {
43861
- const pkg = JSON.parse(readFileSync28(pkgPath, "utf-8"));
43548
+ const pkg = JSON.parse(readFileSync27(pkgPath, "utf-8"));
43862
43549
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
43863
43550
  } catch {
43864
43551
  }
43865
43552
  }
43866
- const parent = dirname15(dir);
43553
+ const parent = dirname14(dir);
43867
43554
  if (parent === dir) break;
43868
43555
  dir = parent;
43869
43556
  }
43870
43557
  return "0.0.0";
43871
43558
  }
43872
43559
  function inContainer() {
43873
- if (existsSync36("/.dockerenv") || existsSync36("/run/.containerenv")) return true;
43560
+ if (existsSync35("/.dockerenv") || existsSync35("/run/.containerenv")) return true;
43874
43561
  try {
43875
- const blob = readFileSync28("/proc/1/cgroup", "utf-8");
43562
+ const blob = readFileSync27("/proc/1/cgroup", "utf-8");
43876
43563
  return blob.includes("docker") || blob.includes("containerd") || blob.includes("kubepods");
43877
43564
  } catch {
43878
43565
  return false;
@@ -44017,7 +43704,7 @@ async function openConsole() {
44017
43704
  r.context.Database = Database2;
44018
43705
  r.context.Log = Log2;
44019
43706
  r.context.db = db;
44020
- await new Promise((resolve31) => r.on("exit", resolve31));
43707
+ await new Promise((resolve30) => r.on("exit", resolve30));
44021
43708
  }
44022
43709
  async function installAiContext(args) {
44023
43710
  const { showMenu: showMenu2, installSelected: installSelected2, installAll: installAll2 } = await Promise.resolve().then(() => (init_ai(), ai_exports));
@@ -44175,7 +43862,7 @@ function findClient() {
44175
43862
  for (const dir of (process.env.PATH ?? "").split(delimiter3)) {
44176
43863
  if (!dir) continue;
44177
43864
  for (const name of names) {
44178
- const candidate = join38(dir, name);
43865
+ const candidate = join37(dir, name);
44179
43866
  try {
44180
43867
  if (statSync21(candidate).isFile()) return candidate;
44181
43868
  } catch {