tina4-nodejs 3.13.131 → 3.13.133

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.
@@ -4064,33 +4064,17 @@ var init_engine = __esm({
4064
4064
  i++;
4065
4065
  }
4066
4066
  }
4067
- handleCache(tokens, start2, context) {
4068
- const [content] = stripTag(tokens[start2][1]);
4069
- const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
4070
- const cacheKey = m ? m[1] : "default";
4071
- const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
4072
- sweepExpiredCache(this.fragmentCache);
4073
- const cached = this.fragmentCache.get(cacheKey);
4074
- if (cached) {
4075
- const [htmlContent, expiresAt] = cached;
4076
- if (Date.now() < expiresAt) {
4077
- let i2 = start2 + 1;
4078
- let depth2 = 0;
4079
- while (i2 < tokens.length) {
4080
- if (tokens[i2][0] === "BLOCK") {
4081
- const [tagContent] = stripTag(tokens[i2][1]);
4082
- const tag = tagContent.split(/\s+/)[0] || "";
4083
- if (tag === "cache") depth2++;
4084
- else if (tag === "endcache") {
4085
- if (depth2 === 0) return [htmlContent, i2 + 1];
4086
- depth2--;
4087
- }
4088
- }
4089
- i2++;
4090
- }
4091
- return [htmlContent, i2];
4092
- }
4093
- }
4067
+ /**
4068
+ * Collect the body tokens of a {% <openTag> %}...{% end<openTag> %} block,
4069
+ * starting from the token after the opening tag (start + 1). Nested same-tag
4070
+ * blocks are kept in the body and balanced by depth; the matching closing tag is
4071
+ * consumed but NOT included. Returns [bodyTokens, indexAfterClosingTag].
4072
+ *
4073
+ * canNest guards the open-tag count: handleSetBlock passes it so the inline
4074
+ * {% set x = 1 %} form (which has no {% endset %}) never opens a nested block —
4075
+ * only the block form {% set x %} nests. Omitted, every openTag occurrence nests.
4076
+ */
4077
+ collectBlockBody(tokens, start2, openTag, closeTag, canNest) {
4094
4078
  const bodyTokens = [];
4095
4079
  let i = start2 + 1;
4096
4080
  let depth = 0;
@@ -4098,10 +4082,10 @@ var init_engine = __esm({
4098
4082
  if (tokens[i][0] === "BLOCK") {
4099
4083
  const [tagContent] = stripTag(tokens[i][1]);
4100
4084
  const tag = tagContent.split(/\s+/)[0] || "";
4101
- if (tag === "cache") {
4085
+ if (tag === openTag && (canNest ? canNest(tagContent) : true)) {
4102
4086
  depth++;
4103
4087
  bodyTokens.push(tokens[i]);
4104
- } else if (tag === "endcache") {
4088
+ } else if (tag === closeTag) {
4105
4089
  if (depth === 0) {
4106
4090
  i++;
4107
4091
  break;
@@ -4116,6 +4100,36 @@ var init_engine = __esm({
4116
4100
  }
4117
4101
  i++;
4118
4102
  }
4103
+ return [bodyTokens, i];
4104
+ }
4105
+ handleCache(tokens, start2, context) {
4106
+ const [content] = stripTag(tokens[start2][1]);
4107
+ const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
4108
+ const cacheKey = m ? m[1] : "default";
4109
+ const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
4110
+ sweepExpiredCache(this.fragmentCache);
4111
+ const cached = this.fragmentCache.get(cacheKey);
4112
+ if (cached) {
4113
+ const [htmlContent, expiresAt] = cached;
4114
+ if (Date.now() < expiresAt) {
4115
+ let i2 = start2 + 1;
4116
+ let depth = 0;
4117
+ while (i2 < tokens.length) {
4118
+ if (tokens[i2][0] === "BLOCK") {
4119
+ const [tagContent] = stripTag(tokens[i2][1]);
4120
+ const tag = tagContent.split(/\s+/)[0] || "";
4121
+ if (tag === "cache") depth++;
4122
+ else if (tag === "endcache") {
4123
+ if (depth === 0) return [htmlContent, i2 + 1];
4124
+ depth--;
4125
+ }
4126
+ }
4127
+ i2++;
4128
+ }
4129
+ return [htmlContent, i2];
4130
+ }
4131
+ }
4132
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "cache", "endcache");
4119
4133
  const rendered = this.renderTokens([...bodyTokens], context);
4120
4134
  capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
4121
4135
  this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
@@ -4280,62 +4294,20 @@ var init_engine = __esm({
4280
4294
  handleSetBlock(tokens, start2, context) {
4281
4295
  const [content] = stripTag(tokens[start2][1]);
4282
4296
  const name = (content.split(/\s+/)[1] || "").trim();
4283
- const bodyTokens = [];
4284
- let i = start2 + 1;
4285
- let depth = 0;
4286
- while (i < tokens.length) {
4287
- if (tokens[i][0] === "BLOCK") {
4288
- const [tagContent] = stripTag(tokens[i][1]);
4289
- const tag = tagContent.split(/\s+/)[0] || "";
4290
- if (tag === "set" && !tagContent.includes("=")) {
4291
- depth++;
4292
- bodyTokens.push(tokens[i]);
4293
- } else if (tag === "endset") {
4294
- if (depth === 0) {
4295
- i++;
4296
- break;
4297
- }
4298
- depth--;
4299
- bodyTokens.push(tokens[i]);
4300
- } else {
4301
- bodyTokens.push(tokens[i]);
4302
- }
4303
- } else {
4304
- bodyTokens.push(tokens[i]);
4305
- }
4306
- i++;
4307
- }
4297
+ const [bodyTokens, i] = this.collectBlockBody(
4298
+ tokens,
4299
+ start2,
4300
+ "set",
4301
+ "endset",
4302
+ (tagContent) => !tagContent.includes("=")
4303
+ );
4308
4304
  if (name) {
4309
4305
  context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
4310
4306
  }
4311
4307
  return i;
4312
4308
  }
4313
4309
  handleSpaceless(tokens, start2, context) {
4314
- const bodyTokens = [];
4315
- let i = start2 + 1;
4316
- let depth = 0;
4317
- while (i < tokens.length) {
4318
- if (tokens[i][0] === "BLOCK") {
4319
- const [tagContent] = stripTag(tokens[i][1]);
4320
- const tag = tagContent.split(/\s+/)[0] || "";
4321
- if (tag === "spaceless") {
4322
- depth++;
4323
- bodyTokens.push(tokens[i]);
4324
- } else if (tag === "endspaceless") {
4325
- if (depth === 0) {
4326
- i++;
4327
- break;
4328
- }
4329
- depth--;
4330
- bodyTokens.push(tokens[i]);
4331
- } else {
4332
- bodyTokens.push(tokens[i]);
4333
- }
4334
- } else {
4335
- bodyTokens.push(tokens[i]);
4336
- }
4337
- i++;
4338
- }
4310
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "spaceless", "endspaceless");
4339
4311
  let rendered = this.renderTokens([...bodyTokens], context);
4340
4312
  rendered = rendered.replace(/>\s+</g, "><");
4341
4313
  return [rendered, i];
@@ -4344,31 +4316,7 @@ var init_engine = __esm({
4344
4316
  const [content] = stripTag(tokens[start2][1]);
4345
4317
  const modeMatch = content.match(/^autoescape\s+(false|true)/);
4346
4318
  const autoEscapeOn = !(modeMatch && modeMatch[1] === "false");
4347
- const bodyTokens = [];
4348
- let i = start2 + 1;
4349
- let depth = 0;
4350
- while (i < tokens.length) {
4351
- if (tokens[i][0] === "BLOCK") {
4352
- const [tagContent] = stripTag(tokens[i][1]);
4353
- const tag = tagContent.split(/\s+/)[0] || "";
4354
- if (tag === "autoescape") {
4355
- depth++;
4356
- bodyTokens.push(tokens[i]);
4357
- } else if (tag === "endautoescape") {
4358
- if (depth === 0) {
4359
- i++;
4360
- break;
4361
- }
4362
- depth--;
4363
- bodyTokens.push(tokens[i]);
4364
- } else {
4365
- bodyTokens.push(tokens[i]);
4366
- }
4367
- } else {
4368
- bodyTokens.push(tokens[i]);
4369
- }
4370
- i++;
4371
- }
4319
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "autoescape", "endautoescape");
4372
4320
  if (!autoEscapeOn) {
4373
4321
  const oldAutoEscape = this._autoEscape;
4374
4322
  this._autoEscape = false;
@@ -5722,6 +5670,99 @@ var init_databaseResult = __esm({
5722
5670
  }
5723
5671
  });
5724
5672
 
5673
+ // ../orm/src/modelCollection.ts
5674
+ var ModelCollection;
5675
+ var init_modelCollection = __esm({
5676
+ "../orm/src/modelCollection.ts"() {
5677
+ "use strict";
5678
+ ModelCollection = class extends Array {
5679
+ /** Total rows matching the query's filter (ignores limit/offset). */
5680
+ _total = 0;
5681
+ /** The SQL limit that produced this page. */
5682
+ _limit = 0;
5683
+ /** The SQL offset that produced this page. */
5684
+ _offset = 0;
5685
+ /**
5686
+ * Derived array operations (`map`, `filter`, `slice`, spread, …) build a plain
5687
+ * `Array`, never another `ModelCollection`. This is what defuses the Array-
5688
+ * subclass constructor/species trap: the engine never calls this constructor
5689
+ * with a length during those operations.
5690
+ */
5691
+ static get [Symbol.species]() {
5692
+ return Array;
5693
+ }
5694
+ /**
5695
+ * @param items the page of hydrated model instances (or the length, when the
5696
+ * engine constructs a derived array — defended against here).
5697
+ * @param total total rows matching the query's filter (ignores limit/offset).
5698
+ * @param limit the SQL limit that produced this page.
5699
+ * @param offset the SQL offset that produced this page.
5700
+ */
5701
+ constructor(items, total = 0, limit = 0, offset = 0) {
5702
+ super();
5703
+ if (typeof items === "number") {
5704
+ this.length = items;
5705
+ return;
5706
+ }
5707
+ if (items) {
5708
+ for (let i = 0; i < items.length; i++) {
5709
+ this[i] = items[i];
5710
+ }
5711
+ }
5712
+ this._total = Math.trunc(total) || 0;
5713
+ this._limit = Math.trunc(limit) || 0;
5714
+ this._offset = Math.trunc(offset) || 0;
5715
+ }
5716
+ /**
5717
+ * Total rows matching the query's filter, ignoring limit/offset.
5718
+ *
5719
+ * This is the whole point of the collection: the page slice you are iterating
5720
+ * is capped by `limit`, but this number is the full count of matching rows —
5721
+ * what a pager needs to render "page 3 of 13".
5722
+ */
5723
+ getTotalRecords() {
5724
+ return this._total;
5725
+ }
5726
+ /**
5727
+ * The canonical pagination envelope — seven snake_case keys, identical to
5728
+ * `DatabaseResult.toPaginate()` (ADR-0043) and to the other three frameworks'
5729
+ * `toPaginate()` / `to_paginate()`.
5730
+ *
5731
+ * records the page's rows as plain objects (never re-sliced)
5732
+ * total getTotalRecords() — the true total for the filter
5733
+ * page floor(offset / per_page) + 1
5734
+ * per_page the query's limit
5735
+ * total_pages ceil(total / per_page)
5736
+ * limit the SQL limit actually applied
5737
+ * offset the SQL offset actually applied
5738
+ *
5739
+ * `records` are model dicts (via `toDict()`, the same serialisation the
5740
+ * framework applies to a model in a JSON response), so the JSON a client sees
5741
+ * matches `DatabaseResult` exactly — the result is uniform whether the route
5742
+ * returned a raw `db.fetch()` or an ORM query.
5743
+ */
5744
+ toPaginate() {
5745
+ const perPage = this._limit > 0 ? this._limit : this.length;
5746
+ const page = perPage > 0 ? Math.floor(this._offset / perPage) + 1 : 1;
5747
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this._total / perPage)) : 1;
5748
+ const records = this.map((model) => {
5749
+ const m = model;
5750
+ return m && typeof m.toDict === "function" ? m.toDict() : model;
5751
+ });
5752
+ return {
5753
+ records,
5754
+ total: this._total,
5755
+ page,
5756
+ per_page: perPage,
5757
+ total_pages: totalPages,
5758
+ limit: perPage,
5759
+ offset: this._offset
5760
+ };
5761
+ }
5762
+ };
5763
+ }
5764
+ });
5765
+
5725
5766
  // ../orm/src/databaseUrl.ts
5726
5767
  import { inspect } from "node:util";
5727
5768
  function stripOneSlash(path8) {
@@ -14070,6 +14111,7 @@ var init_baseModel = __esm({
14070
14111
  "../orm/src/baseModel.ts"() {
14071
14112
  "use strict";
14072
14113
  init_database();
14114
+ init_modelCollection();
14073
14115
  init_validation();
14074
14116
  init_queryBuilder();
14075
14117
  init_sqlite();
@@ -14305,6 +14347,30 @@ var init_baseModel = __esm({
14305
14347
  static getPkColumn() {
14306
14348
  return this.getDbColumn(this.getPkField());
14307
14349
  }
14350
+ /**
14351
+ * Shared read tail for the collection-returning finders (where / all / select
14352
+ * / find filter-form / withTrashed). Runs the SAME two calls `db.fetch()`
14353
+ * makes — the page fetch AND the COUNT probe over the SAME base SQL — hydrates
14354
+ * the rows into model instances, and returns a ModelCollection carrying the
14355
+ * total (ADR-0064).
14356
+ *
14357
+ * The total is FREE: `probeTotal` is the exact `COUNT(*)` probe `db.fetch()`
14358
+ * already runs; the ORM used to discard it. ZERO extra queries beyond that one
14359
+ * probe. `sql` MUST NOT carry its own LIMIT/OFFSET — `adapterFetch` applies
14360
+ * limit/offset to the page, and the probe wraps the un-limited SQL so it counts
14361
+ * the WHOLE filtered set, not the page.
14362
+ */
14363
+ static async _collect(sql, params, limit, offset, include) {
14364
+ const db = this.getDb();
14365
+ const rows = await adapterFetch(db, sql, params, limit, offset);
14366
+ const data = Array.isArray(rows) ? rows : rows?.data ?? [];
14367
+ const total = await probeTotal(db, sql, params, limit) ?? data.length;
14368
+ const instances = data.map((row) => new this(row));
14369
+ if (include) {
14370
+ await this._eagerLoad(instances, include);
14371
+ }
14372
+ return new ModelCollection(instances, total, limit, offset);
14373
+ }
14308
14374
  /**
14309
14375
  * Find a record by primary key.
14310
14376
  * @param id Primary key value.
@@ -14357,7 +14423,6 @@ var init_baseModel = __esm({
14357
14423
  );
14358
14424
  }
14359
14425
  const lim = typeof limit === "number" ? limit : 100;
14360
- const db = ModelClass.getDb();
14361
14426
  const conditions = [];
14362
14427
  const params = [];
14363
14428
  if (filter) {
@@ -14377,16 +14442,7 @@ var init_baseModel = __esm({
14377
14442
  if (orderBy) {
14378
14443
  sql += ` ORDER BY ${orderBy}`;
14379
14444
  }
14380
- const rows = await adapterFetch(db, sql, params, lim, offset);
14381
- const data = rows?.data ?? rows;
14382
- const instances = (Array.isArray(data) ? data : []).map((row) => {
14383
- const inst = new this(row);
14384
- return inst;
14385
- });
14386
- if (include) {
14387
- await ModelClass._eagerLoad(instances, include);
14388
- }
14389
- return instances;
14445
+ return ModelClass._collect(sql, params, lim, offset, include);
14390
14446
  }
14391
14447
  /**
14392
14448
  * Load a record into this instance via selectOne.
@@ -14454,7 +14510,6 @@ var init_baseModel = __esm({
14454
14510
  */
14455
14511
  static async all(limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
14456
14512
  const ModelClass = this;
14457
- const db = ModelClass.getDb();
14458
14513
  const conditions = [];
14459
14514
  if (ModelClass.softDelete) {
14460
14515
  conditions.push("is_deleted = 0");
@@ -14464,13 +14519,8 @@ var init_baseModel = __esm({
14464
14519
  }
14465
14520
  const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
14466
14521
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
14467
- const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
14468
- const rows = await adapterQuery(db, sql, []);
14469
- const instances = rows.map((row) => new ModelClass(row));
14470
- if (include) {
14471
- await ModelClass._eagerLoad(instances, include);
14472
- }
14473
- return instances;
14522
+ const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}`;
14523
+ return ModelClass._collect(sql, [], limit, offset, include);
14474
14524
  }
14475
14525
  /**
14476
14526
  * Query records with a WHERE clause.
@@ -14485,7 +14535,6 @@ var init_baseModel = __esm({
14485
14535
  */
14486
14536
  static async where(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
14487
14537
  const ModelClass = this;
14488
- const db = ModelClass.getDb();
14489
14538
  const parts = [];
14490
14539
  if (ModelClass.softDelete) {
14491
14540
  parts.push("is_deleted = 0");
@@ -14495,13 +14544,8 @@ var init_baseModel = __esm({
14495
14544
  }
14496
14545
  parts.push(`(${conditions})`);
14497
14546
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
14498
- const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
14499
- const rows = await adapterQuery(db, sql, params);
14500
- const instances = rows.map((row) => new ModelClass(row));
14501
- if (include) {
14502
- await ModelClass._eagerLoad(instances, include);
14503
- }
14504
- return instances;
14547
+ const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause}`;
14548
+ return ModelClass._collect(sql, params, limit, offset, include);
14505
14549
  }
14506
14550
  /**
14507
14551
  * Save this instance (insert or update). Returns this on success (fluent
@@ -14981,10 +15025,7 @@ var init_baseModel = __esm({
14981
15025
  */
14982
15026
  static async select(sql, params, limit = DEFAULT_ROW_CAP, offset = 0) {
14983
15027
  const ModelClass = this;
14984
- const db = ModelClass.getDb();
14985
- const paged = SQLTranslator.appendLimit(sql, limit, offset);
14986
- const rows = await adapterQuery(db, paged, params);
14987
- return rows.map((row) => new ModelClass(row));
15028
+ return ModelClass._collect(sql, params, limit, offset);
14988
15029
  }
14989
15030
  static async selectOne(sql, params, include) {
14990
15031
  const ModelClass = this;
@@ -15056,7 +15097,6 @@ var init_baseModel = __esm({
15056
15097
  */
15057
15098
  static async withTrashed(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0) {
15058
15099
  const ModelClass = this;
15059
- const db = ModelClass.getDb();
15060
15100
  const parts = [];
15061
15101
  if (ModelClass.tableFilter) {
15062
15102
  parts.push(ModelClass.tableFilter);
@@ -15068,14 +15108,7 @@ var init_baseModel = __esm({
15068
15108
  if (parts.length > 0) {
15069
15109
  sql += ` WHERE ${parts.join(" AND ")}`;
15070
15110
  }
15071
- if (limit !== void 0) {
15072
- sql += ` LIMIT ${limit}`;
15073
- }
15074
- if (offset !== void 0) {
15075
- sql += ` OFFSET ${offset}`;
15076
- }
15077
- const rows = await adapterQuery(db, sql, params);
15078
- return rows.map((row) => new ModelClass(row));
15111
+ return ModelClass._collect(sql, params, limit, offset);
15079
15112
  }
15080
15113
  /**
15081
15114
  * Count records matching conditions (respects soft delete and table filter).
@@ -18414,6 +18447,7 @@ __export(src_exports, {
18414
18447
  InvalidId: () => InvalidId,
18415
18448
  LocalStorage: () => LocalStorage,
18416
18449
  Migration: () => Migration,
18450
+ ModelCollection: () => ModelCollection,
18417
18451
  MongodbAdapter: () => MongodbAdapter,
18418
18452
  MssqlAdapter: () => MssqlAdapter,
18419
18453
  MysqlAdapter: () => MysqlAdapter,
@@ -18518,6 +18552,7 @@ var init_src = __esm({
18518
18552
  "use strict";
18519
18553
  init_types();
18520
18554
  init_databaseResult();
18555
+ init_modelCollection();
18521
18556
  init_database();
18522
18557
  init_database();
18523
18558
  init_databaseUrl();
@@ -35264,7 +35299,7 @@ function toolbarJs() {
35264
35299
  connect();
35265
35300
  })();`;
35266
35301
  }
35267
- var cpuCount, DEV_SAFE_METHODS, DEV_MCP_PREFIXES, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, handleChat, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, handleExecute, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
35302
+ var cpuCount, DEV_SAFE_METHODS, DEV_MCP_PREFIXES, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
35268
35303
  var init_devAdmin = __esm({
35269
35304
  "../core/src/devAdmin.ts"() {
35270
35305
  "use strict";
@@ -35602,9 +35637,6 @@ var init_devAdmin = __esm({
35602
35637
  { method: "POST", pattern: "/__dev/api/websockets/disconnect", handler: handleWebsocketsDisconnect },
35603
35638
  // Tools
35604
35639
  { method: "POST", pattern: "/__dev/api/tool", handler: handleTool },
35605
- // Chat — proxies to Rust agent /chat (SSE passthrough). Forwards
35606
- // active_file and any other body keys verbatim. See proxyToSupervisor.
35607
- { method: "POST", pattern: "/__dev/api/chat", handler: handleChat },
35608
35640
  // Threads — proxies to Rust agent /threads. Mirrors Python's
35609
35641
  // _api_threads + _api_threads_sub.
35610
35642
  { method: "GET", pattern: "/__dev/api/threads", handler: handleThreads },
@@ -35683,8 +35715,6 @@ var init_devAdmin = __esm({
35683
35715
  { method: "GET", pattern: "/__dev/api/supervise/diff", handler: handleSuperviseStub },
35684
35716
  { method: "POST", pattern: "/__dev/api/supervise/commit", handler: handleSuperviseStub },
35685
35717
  { method: "POST", pattern: "/__dev/api/supervise/cancel", handler: handleSuperviseStub },
35686
- // Execute — proxies to the framework_port+2000 Rust agent (SSE passthrough)
35687
- { method: "POST", pattern: "/__dev/api/execute", handler: handleExecute },
35688
35718
  // Framework-grounding MCP token config — self-contained (.env upsert)
35689
35719
  { method: "GET", pattern: "/__dev/api/grounding/status", handler: handleGroundingStatus },
35690
35720
  { method: "POST", pattern: "/__dev/api/grounding/token", handler: handleGroundingToken },
@@ -36254,9 +36284,6 @@ var init_devAdmin = __esm({
36254
36284
  }
36255
36285
  res.json({ tool, status: "executed", message: `Tool '${tool}' executed (stub)`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
36256
36286
  };
36257
- handleChat = async (req2, res) => {
36258
- await proxyToSupervisor(req2, res, "/chat");
36259
- };
36260
36287
  DEFAULT_MCP_URL = "https://mcp.tina4.com";
36261
36288
  handleGroundingStatus = async (_req, res) => {
36262
36289
  const token = resolveDevEnvVar("TINA4_MCP_TOKEN");
@@ -36544,35 +36571,6 @@ var init_devAdmin = __esm({
36544
36571
  501
36545
36572
  );
36546
36573
  };
36547
- handleExecute = async (req2, res) => {
36548
- const port = parseInt(process.env.TINA4_PORT ?? process.env.PORT ?? "7148", 10);
36549
- const agentUrl = `http://127.0.0.1:${port + 2e3}/execute`;
36550
- try {
36551
- const upstream = await fetch(agentUrl, {
36552
- method: "POST",
36553
- headers: { "Content-Type": "application/json" },
36554
- body: JSON.stringify(req2.body ?? {})
36555
- });
36556
- if (!upstream.body) {
36557
- res.json({ error: "agent returned no body" }, 502);
36558
- return;
36559
- }
36560
- res.raw.writeHead(upstream.status || 200, {
36561
- "Content-Type": upstream.headers.get("content-type") || "text/event-stream",
36562
- "Cache-Control": "no-cache",
36563
- Connection: "keep-alive"
36564
- });
36565
- const reader = upstream.body.getReader();
36566
- while (true) {
36567
- const { done, value } = await reader.read();
36568
- if (done) break;
36569
- if (value) res.raw.write(Buffer.from(value));
36570
- }
36571
- res.raw.end();
36572
- } catch (e) {
36573
- res.json({ error: `agent unreachable at ${agentUrl}: ${e.message}` }, 502);
36574
- }
36575
- };
36576
36574
  DEV_FILES_IGNORED = /* @__PURE__ */ new Set([
36577
36575
  "__pycache__",
36578
36576
  "node_modules",
@@ -37637,12 +37635,46 @@ function sanitizeSecurity(reqs, schemes) {
37637
37635
  });
37638
37636
  }
37639
37637
  function generate2(routes, models = []) {
37638
+ const schemes = resolveSecuritySchemes();
37639
+ const spec = {
37640
+ openapi: resolveOpenApiVersion(),
37641
+ info: buildInfo(),
37642
+ servers: resolveServers(),
37643
+ paths: {},
37644
+ components: {
37645
+ schemas: {},
37646
+ // Configurable security schemes (v3.13.42): bearerFormat via env, optional
37647
+ // apiKey scheme, plus any programmatically-registered schemes (which may
37648
+ // override bearerAuth — e.g. an oauth2 scheme with scopes).
37649
+ securitySchemes: schemes
37650
+ }
37651
+ };
37652
+ const tableToSchema = /* @__PURE__ */ new Map();
37653
+ buildComponentSchemas(models, spec, tableToSchema);
37654
+ const ctx = {
37655
+ models,
37656
+ tableToSchema,
37657
+ schemes,
37658
+ // Default scheme secured routes use when no explicit meta.security is set.
37659
+ defaultScheme: process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth",
37660
+ // Path filters (comma-separated raw-path prefixes).
37661
+ includePrefixes: csv(process.env.TINA4_SWAGGER_INCLUDE),
37662
+ excludePrefixes: csv(process.env.TINA4_SWAGGER_EXCLUDE),
37663
+ // Reusable custom schemas referenced by routes via meta.requestSchema/responseSchemas.
37664
+ refSchemas: /* @__PURE__ */ new Set(),
37665
+ usedTags: [],
37666
+ seenIds: /* @__PURE__ */ new Set()
37667
+ };
37668
+ for (const route of routes) {
37669
+ buildOperation(route, spec, ctx);
37670
+ }
37671
+ buildRefSchemas(spec, ctx.refSchemas);
37672
+ buildTags(spec, ctx.usedTags);
37673
+ return spec;
37674
+ }
37675
+ function buildInfo() {
37640
37676
  const info = {
37641
37677
  title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
37642
- // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
37643
- // 0.0.1). description defaults to the empty string, not a canned sentence.
37644
- // Both are the settled cross-framework defaults (parity with the Python
37645
- // master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
37646
37678
  version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
37647
37679
  description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
37648
37680
  };
@@ -37659,135 +37691,134 @@ function generate2(routes, models = []) {
37659
37691
  const [name, url] = licenseRaw.split("|").map((s) => s.trim());
37660
37692
  info.license = url ? { name, url } : { name };
37661
37693
  }
37662
- const schemes = resolveSecuritySchemes();
37663
- const spec = {
37664
- openapi: resolveOpenApiVersion(),
37665
- info,
37666
- servers: resolveServers(),
37667
- paths: {},
37668
- components: {
37669
- schemas: {},
37670
- // Configurable security schemes (v3.13.42): bearerFormat via env, optional
37671
- // apiKey scheme, plus any programmatically-registered schemes (which may
37672
- // override bearerAuth — e.g. an oauth2 scheme with scopes).
37673
- securitySchemes: schemes
37674
- }
37675
- };
37676
- const defaultScheme = process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth";
37677
- const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
37678
- const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
37679
- const refSchemas = /* @__PURE__ */ new Set();
37680
- const tableToSchema = /* @__PURE__ */ new Map();
37694
+ return info;
37695
+ }
37696
+ function buildComponentSchemas(models, spec, tableToSchema) {
37681
37697
  for (const model of models) {
37682
37698
  const schemaKey = schemaNameForModel(model);
37683
37699
  tableToSchema.set(model.tableName, schemaKey);
37684
37700
  spec.components.schemas[schemaKey] = modelToSchema(model);
37685
37701
  }
37686
- const usedTags = [];
37687
- const seenIds = /* @__PURE__ */ new Set();
37688
- for (const route of routes) {
37689
- if (!isIncludedPath(route.pattern, includePrefixes, excludePrefixes)) continue;
37690
- const openApiPath = patternToOpenAPI(route.pattern);
37691
- const method = route.method.toLowerCase();
37692
- if (!spec.paths[openApiPath]) {
37693
- spec.paths[openApiPath] = {};
37694
- }
37695
- const tags = route.meta?.tags ?? inferTags(route.pattern);
37696
- for (const t of tags) {
37697
- if (!usedTags.includes(t)) usedTags.push(t);
37698
- }
37699
- const operation = {
37700
- operationId: uniqueOperationId(method, openApiPath, seenIds),
37701
- summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
37702
- tags,
37703
- responses: route.meta?.responses ?? {
37704
- "200": { description: "Successful response" }
37705
- }
37706
- };
37707
- if (route.meta?.description) operation.description = route.meta.description;
37708
- if (route.meta?.deprecated) operation.deprecated = true;
37709
- const pathParams = extractPathParams(route.pattern);
37710
- if (pathParams.length > 0) {
37711
- operation.parameters = pathParams.map(({ name, schema }) => ({
37712
- name,
37713
- in: "path",
37714
- required: true,
37715
- schema
37716
- }));
37702
+ }
37703
+ function buildOperation(route, spec, ctx) {
37704
+ if (!isIncludedPath(route.pattern, ctx.includePrefixes, ctx.excludePrefixes)) return;
37705
+ const openApiPath = patternToOpenAPI(route.pattern);
37706
+ const method = route.method.toLowerCase();
37707
+ if (!spec.paths[openApiPath]) {
37708
+ spec.paths[openApiPath] = {};
37709
+ }
37710
+ const tags = route.meta?.tags ?? inferTags(route.pattern);
37711
+ for (const t of tags) {
37712
+ if (!ctx.usedTags.includes(t)) ctx.usedTags.push(t);
37713
+ }
37714
+ const operation = {
37715
+ operationId: uniqueOperationId(method, openApiPath, ctx.seenIds),
37716
+ summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
37717
+ tags,
37718
+ responses: route.meta?.responses ?? {
37719
+ "200": { description: "Successful response" }
37717
37720
  }
37718
- if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
37719
- const modelName = inferModelFromPath(route.pattern);
37720
- if (modelName && models.some((m) => m.tableName === modelName)) {
37721
- operation.parameters = [
37722
- ...operation.parameters ?? [],
37723
- { name: "page", in: "query", schema: { type: "integer", default: 1 } },
37724
- { name: "limit", in: "query", schema: { type: "integer", default: 20 } },
37725
- { name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" }
37726
- ];
37727
- }
37721
+ };
37722
+ if (route.meta?.description) operation.description = route.meta.description;
37723
+ if (route.meta?.deprecated) operation.deprecated = true;
37724
+ const parameters = operationParameters(route, method, ctx.models);
37725
+ if (parameters.length > 0) operation.parameters = parameters;
37726
+ operationRequestBody(route, method, operation, ctx);
37727
+ operationResponseSchemas(route, operation, ctx.refSchemas);
37728
+ operationSecurity(route, method, operation, ctx.schemes, ctx.defaultScheme);
37729
+ spec.paths[openApiPath][method] = operation;
37730
+ }
37731
+ function operationParameters(route, method, models) {
37732
+ let parameters = [];
37733
+ const pathParams = extractPathParams(route.pattern);
37734
+ if (pathParams.length > 0) {
37735
+ parameters = pathParams.map(({ name, schema }) => ({
37736
+ name,
37737
+ in: "path",
37738
+ required: true,
37739
+ schema
37740
+ }));
37741
+ }
37742
+ if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
37743
+ const modelName = inferModelFromPath(route.pattern);
37744
+ if (modelName && models.some((m) => m.tableName === modelName)) {
37745
+ parameters = [
37746
+ ...parameters,
37747
+ { name: "page", in: "query", schema: { type: "integer", default: 1 } },
37748
+ { name: "limit", in: "query", schema: { type: "integer", default: 20 } },
37749
+ { name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" }
37750
+ ];
37728
37751
  }
37729
- const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
37730
- if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
37731
- refSchemas.add(reqSchemaRef.name);
37732
- const media = {
37733
- schema: { $ref: `#/components/schemas/${reqSchemaRef.name}` }
37734
- };
37735
- if (route.meta?.example !== void 0) media.example = route.meta.example;
37752
+ }
37753
+ return parameters;
37754
+ }
37755
+ function mediaWithExample(schema, example) {
37756
+ const media = { schema };
37757
+ if (example !== void 0) media.example = example;
37758
+ return media;
37759
+ }
37760
+ function operationRequestBody(route, method, operation, ctx) {
37761
+ const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
37762
+ if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
37763
+ ctx.refSchemas.add(reqSchemaRef.name);
37764
+ const media = mediaWithExample({ $ref: `#/components/schemas/${reqSchemaRef.name}` }, route.meta?.example);
37765
+ operation.requestBody = {
37766
+ content: { [reqSchemaRef.contentType]: media }
37767
+ };
37768
+ } else if (method === "post" || method === "put") {
37769
+ const modelName = inferModelFromPath(route.pattern);
37770
+ const schemaKey = modelName ? ctx.tableToSchema.get(modelName) : void 0;
37771
+ if (schemaKey) {
37772
+ const sref = `#/components/schemas/${schemaKey}`;
37736
37773
  operation.requestBody = {
37737
- content: { [reqSchemaRef.contentType]: media }
37774
+ required: true,
37775
+ content: { "application/json": mediaWithExample({ $ref: sref }, route.meta?.example) }
37738
37776
  };
37739
- } else if (method === "post" || method === "put") {
37740
- const modelName = inferModelFromPath(route.pattern);
37741
- const schemaKey = modelName ? tableToSchema.get(modelName) : void 0;
37742
- if (schemaKey) {
37743
- const sref = `#/components/schemas/${schemaKey}`;
37744
- const media = { schema: { $ref: sref } };
37745
- if (route.meta?.example !== void 0) media.example = route.meta.example;
37746
- operation.requestBody = {
37747
- required: true,
37748
- content: { "application/json": media }
37749
- };
37750
- if (route.meta?.responses === void 0) {
37751
- operation.responses = {
37752
- "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
37753
- };
37754
- }
37755
- } else if (route.meta?.example !== void 0) {
37756
- operation.requestBody = {
37757
- content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
37777
+ if (route.meta?.responses === void 0) {
37778
+ operation.responses = {
37779
+ "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
37758
37780
  };
37759
37781
  }
37782
+ } else if (route.meta?.example !== void 0) {
37783
+ operation.requestBody = {
37784
+ content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
37785
+ };
37760
37786
  }
37761
- const respSchemas = parseResponseSchemas(route.meta?.responseSchemas);
37762
- if (respSchemas.length > 0) {
37763
- const responses = operation.responses;
37764
- for (const { status: status2, name, isList } of respSchemas) {
37765
- refSchemas.add(name);
37766
- const sref = `#/components/schemas/${name}`;
37767
- const schema = isList ? { type: "array", items: { $ref: sref } } : { $ref: sref };
37768
- responses[status2] = {
37769
- description: status2.startsWith("2") ? "Successful response" : "Response",
37770
- content: { "application/json": { schema } }
37771
- };
37772
- }
37787
+ }
37788
+ }
37789
+ function operationResponseSchemas(route, operation, refSchemas) {
37790
+ const respSchemas = parseResponseSchemas(route.meta?.responseSchemas);
37791
+ if (respSchemas.length > 0) {
37792
+ const responses = operation.responses;
37793
+ for (const { status: status2, name, isList } of respSchemas) {
37794
+ refSchemas.add(name);
37795
+ const sref = `#/components/schemas/${name}`;
37796
+ const schema = isList ? { type: "array", items: { $ref: sref } } : { $ref: sref };
37797
+ responses[status2] = {
37798
+ description: status2.startsWith("2") ? "Successful response" : "Response",
37799
+ content: { "application/json": { schema } }
37800
+ };
37773
37801
  }
37774
- const hasExplicitSecurity = route.meta?.security !== void 0 || route.meta?.scopes !== void 0 && route.meta.scopes.length > 0;
37775
- if (hasExplicitSecurity) {
37776
- const normalized = normalizeSecurity(route.meta?.security, route.meta?.scopes);
37777
- operation.security = normalized.length > 0 ? sanitizeSecurity(normalized, schemes) : [];
37778
- if (normalized.length > 0) {
37779
- const responses = operation.responses;
37780
- if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
37781
- }
37782
- } else if (routeRequiresAuth(route, method)) {
37783
- const requirements = [{ [defaultScheme]: [] }];
37784
- if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
37785
- operation.security = sanitizeSecurity(requirements, schemes);
37802
+ }
37803
+ }
37804
+ function operationSecurity(route, method, operation, schemes, defaultScheme) {
37805
+ const hasExplicitSecurity = route.meta?.security !== void 0 || route.meta?.scopes !== void 0 && route.meta.scopes.length > 0;
37806
+ if (hasExplicitSecurity) {
37807
+ const normalized = normalizeSecurity(route.meta?.security, route.meta?.scopes);
37808
+ operation.security = normalized.length > 0 ? sanitizeSecurity(normalized, schemes) : [];
37809
+ if (normalized.length > 0) {
37786
37810
  const responses = operation.responses;
37787
37811
  if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
37788
37812
  }
37789
- spec.paths[openApiPath][method] = operation;
37813
+ } else if (routeRequiresAuth(route, method)) {
37814
+ const requirements = [{ [defaultScheme]: [] }];
37815
+ if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
37816
+ operation.security = sanitizeSecurity(requirements, schemes);
37817
+ const responses = operation.responses;
37818
+ if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
37790
37819
  }
37820
+ }
37821
+ function buildRefSchemas(spec, refSchemas) {
37791
37822
  if (refSchemas.size > 0) {
37792
37823
  const schemas = spec.components.schemas;
37793
37824
  for (const name of refSchemas) {
@@ -37796,10 +37827,11 @@ function generate2(routes, models = []) {
37796
37827
  }
37797
37828
  }
37798
37829
  }
37830
+ }
37831
+ function buildTags(spec, usedTags) {
37799
37832
  if (usedTags.length > 0) {
37800
37833
  spec.tags = usedTags.map((name) => ({ name }));
37801
37834
  }
37802
- return spec;
37803
37835
  }
37804
37836
  function routeRequiresAuth(route, method) {
37805
37837
  if (route.noAuth) return false;
@@ -39050,7 +39082,9 @@ function injectIntoHtml(ctx, devToolbar, html) {
39050
39082
  // Suppress the live reloader on the AI/stable port (data-reload="0"); the
39051
39083
  // toolbar JS early-returns when data-reload !== "1". Mirrors PHP's
39052
39084
  // suppressReload flag.
39053
- reload: !ctx.isAiPortRequest
39085
+ // Also suppress on the dev-admin dashboard (any /__dev page): its SPA reloads
39086
+ // itself gently, so the toolbar's full-page reloader must not fire there.
39087
+ reload: !ctx.isAiPortRequest && !ctx.pathname.startsWith("/__dev")
39054
39088
  };
39055
39089
  return injectFeedbackWidget(ctx.req, injectDevToolbar(html, toolbarCtx));
39056
39090
  }
@@ -39305,6 +39339,51 @@ async function runDispatch(ctx, rawReq, rawRes) {
39305
39339
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
39306
39340
  return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
39307
39341
  }
39342
+ function loopbackBindHosts(host) {
39343
+ const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
39344
+ switch (normalized) {
39345
+ case "localhost":
39346
+ return ["127.0.0.1", "::1"];
39347
+ case "127.0.0.1":
39348
+ case "0.0.0.0":
39349
+ return ["::1"];
39350
+ case "::1":
39351
+ case "::":
39352
+ return ["127.0.0.1"];
39353
+ default:
39354
+ return [];
39355
+ }
39356
+ }
39357
+ function startLoopbackSiblings(port, host, dispatch) {
39358
+ const siblingHosts = loopbackBindHosts(host);
39359
+ if (siblingHosts.length === 0) return Promise.resolve([]);
39360
+ return Promise.all(
39361
+ siblingHosts.map(
39362
+ (siblingHost) => new Promise((resolveSibling) => {
39363
+ const sibling = createServer2(dispatch);
39364
+ let settled = false;
39365
+ sibling.on("error", (err) => {
39366
+ const expected = err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT";
39367
+ if (!expected) {
39368
+ Log.debug(
39369
+ `Loopback sibling ${siblingHost}:${port} not bound (${err.code ?? err.message}) \u2014 skipped, primary already serves`
39370
+ );
39371
+ }
39372
+ if (!settled) {
39373
+ settled = true;
39374
+ resolveSibling(null);
39375
+ }
39376
+ });
39377
+ sibling.listen(port, siblingHost, () => {
39378
+ settled = true;
39379
+ resolveSibling(sibling);
39380
+ });
39381
+ })
39382
+ )
39383
+ ).then(
39384
+ (results) => results.filter((s) => s !== null)
39385
+ );
39386
+ }
39308
39387
  async function startServer(config) {
39309
39388
  loadEnv(".env.local");
39310
39389
  loadEnv();
@@ -39573,8 +39652,9 @@ ${reset2}
39573
39652
  }
39574
39653
  });
39575
39654
  return new Promise((resolvePromise) => {
39576
- server.listen(port, host, () => {
39655
+ server.listen(port, host, async () => {
39577
39656
  if (!cluster.isWorker) writePidfile(port);
39657
+ const siblingServers = await startLoopbackSiblings(port, host, dispatch);
39578
39658
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
39579
39659
  const isDebug = isTruthy(process.env.TINA4_DEBUG);
39580
39660
  const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
@@ -39637,14 +39717,22 @@ ${reset2}
39637
39717
  }
39638
39718
  let shuttingDown = false;
39639
39719
  const closeListeners = () => new Promise((done) => {
39640
- let pending = aiServer ? 2 : 1;
39720
+ let pending = 1 + (aiServer ? 1 : 0) + siblingServers.length;
39641
39721
  const one = () => {
39642
39722
  if (--pending === 0) done();
39643
39723
  };
39644
39724
  server.close(one);
39645
39725
  if (aiServer) aiServer.close(one);
39726
+ for (const sibling of siblingServers) {
39727
+ try {
39728
+ sibling.close(one);
39729
+ } catch {
39730
+ one();
39731
+ }
39732
+ }
39646
39733
  server.closeIdleConnections();
39647
39734
  aiServer?.closeIdleConnections();
39735
+ for (const sibling of siblingServers) sibling.closeIdleConnections();
39648
39736
  });
39649
39737
  const gracefulShutdown = async (signal) => {
39650
39738
  if (shuttingDown) return;
@@ -39672,6 +39760,7 @@ ${reset2}
39672
39760
  );
39673
39761
  server.closeAllConnections();
39674
39762
  aiServer?.closeAllConnections();
39763
+ for (const sibling of siblingServers) sibling.closeAllConnections();
39675
39764
  }
39676
39765
  try {
39677
39766
  const orm = await Promise.resolve().then(() => (init_src(), src_exports));
@@ -39699,6 +39788,12 @@ ${reset2}
39699
39788
  stopAllBackgroundTasks();
39700
39789
  if (aiServer) aiServer.close();
39701
39790
  server.close();
39791
+ for (const sibling of siblingServers) {
39792
+ try {
39793
+ sibling.close();
39794
+ } catch {
39795
+ }
39796
+ }
39702
39797
  Promise.resolve().then(() => (init_src(), src_exports)).then((orm) => orm.closeDatabase()).catch(() => {
39703
39798
  });
39704
39799
  },
@@ -46902,6 +46997,7 @@ __export(src_exports3, {
46902
46997
  isValidSessionId: () => isValidSessionId,
46903
46998
  kafkaSecurityConfig: () => kafkaSecurityConfig,
46904
46999
  loadEnv: () => loadEnv,
47000
+ loopbackBindHosts: () => loopbackBindHosts,
46905
47001
  makeCaseInsensitiveHeaders: () => makeCaseInsensitiveHeaders,
46906
47002
  matchCronField: () => matchCronField,
46907
47003
  matchesCron: () => matchesCron,