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