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.
@@ -4063,33 +4063,17 @@ var init_engine = __esm({
4063
4063
  i++;
4064
4064
  }
4065
4065
  }
4066
- handleCache(tokens, start2, context) {
4067
- const [content] = stripTag(tokens[start2][1]);
4068
- const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
4069
- const cacheKey = m ? m[1] : "default";
4070
- const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
4071
- sweepExpiredCache(this.fragmentCache);
4072
- const cached = this.fragmentCache.get(cacheKey);
4073
- if (cached) {
4074
- const [htmlContent, expiresAt] = cached;
4075
- if (Date.now() < expiresAt) {
4076
- let i2 = start2 + 1;
4077
- let depth2 = 0;
4078
- while (i2 < tokens.length) {
4079
- if (tokens[i2][0] === "BLOCK") {
4080
- const [tagContent] = stripTag(tokens[i2][1]);
4081
- const tag = tagContent.split(/\s+/)[0] || "";
4082
- if (tag === "cache") depth2++;
4083
- else if (tag === "endcache") {
4084
- if (depth2 === 0) return [htmlContent, i2 + 1];
4085
- depth2--;
4086
- }
4087
- }
4088
- i2++;
4089
- }
4090
- return [htmlContent, i2];
4091
- }
4092
- }
4066
+ /**
4067
+ * Collect the body tokens of a {% <openTag> %}...{% end<openTag> %} block,
4068
+ * starting from the token after the opening tag (start + 1). Nested same-tag
4069
+ * blocks are kept in the body and balanced by depth; the matching closing tag is
4070
+ * consumed but NOT included. Returns [bodyTokens, indexAfterClosingTag].
4071
+ *
4072
+ * canNest guards the open-tag count: handleSetBlock passes it so the inline
4073
+ * {% set x = 1 %} form (which has no {% endset %}) never opens a nested block —
4074
+ * only the block form {% set x %} nests. Omitted, every openTag occurrence nests.
4075
+ */
4076
+ collectBlockBody(tokens, start2, openTag, closeTag, canNest) {
4093
4077
  const bodyTokens = [];
4094
4078
  let i = start2 + 1;
4095
4079
  let depth = 0;
@@ -4097,10 +4081,10 @@ var init_engine = __esm({
4097
4081
  if (tokens[i][0] === "BLOCK") {
4098
4082
  const [tagContent] = stripTag(tokens[i][1]);
4099
4083
  const tag = tagContent.split(/\s+/)[0] || "";
4100
- if (tag === "cache") {
4084
+ if (tag === openTag && (canNest ? canNest(tagContent) : true)) {
4101
4085
  depth++;
4102
4086
  bodyTokens.push(tokens[i]);
4103
- } else if (tag === "endcache") {
4087
+ } else if (tag === closeTag) {
4104
4088
  if (depth === 0) {
4105
4089
  i++;
4106
4090
  break;
@@ -4115,6 +4099,36 @@ var init_engine = __esm({
4115
4099
  }
4116
4100
  i++;
4117
4101
  }
4102
+ return [bodyTokens, i];
4103
+ }
4104
+ handleCache(tokens, start2, context) {
4105
+ const [content] = stripTag(tokens[start2][1]);
4106
+ const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
4107
+ const cacheKey = m ? m[1] : "default";
4108
+ const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
4109
+ sweepExpiredCache(this.fragmentCache);
4110
+ const cached = this.fragmentCache.get(cacheKey);
4111
+ if (cached) {
4112
+ const [htmlContent, expiresAt] = cached;
4113
+ if (Date.now() < expiresAt) {
4114
+ let i2 = start2 + 1;
4115
+ let depth = 0;
4116
+ while (i2 < tokens.length) {
4117
+ if (tokens[i2][0] === "BLOCK") {
4118
+ const [tagContent] = stripTag(tokens[i2][1]);
4119
+ const tag = tagContent.split(/\s+/)[0] || "";
4120
+ if (tag === "cache") depth++;
4121
+ else if (tag === "endcache") {
4122
+ if (depth === 0) return [htmlContent, i2 + 1];
4123
+ depth--;
4124
+ }
4125
+ }
4126
+ i2++;
4127
+ }
4128
+ return [htmlContent, i2];
4129
+ }
4130
+ }
4131
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "cache", "endcache");
4118
4132
  const rendered = this.renderTokens([...bodyTokens], context);
4119
4133
  capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
4120
4134
  this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
@@ -4279,62 +4293,20 @@ var init_engine = __esm({
4279
4293
  handleSetBlock(tokens, start2, context) {
4280
4294
  const [content] = stripTag(tokens[start2][1]);
4281
4295
  const name = (content.split(/\s+/)[1] || "").trim();
4282
- const bodyTokens = [];
4283
- let i = start2 + 1;
4284
- let depth = 0;
4285
- while (i < tokens.length) {
4286
- if (tokens[i][0] === "BLOCK") {
4287
- const [tagContent] = stripTag(tokens[i][1]);
4288
- const tag = tagContent.split(/\s+/)[0] || "";
4289
- if (tag === "set" && !tagContent.includes("=")) {
4290
- depth++;
4291
- bodyTokens.push(tokens[i]);
4292
- } else if (tag === "endset") {
4293
- if (depth === 0) {
4294
- i++;
4295
- break;
4296
- }
4297
- depth--;
4298
- bodyTokens.push(tokens[i]);
4299
- } else {
4300
- bodyTokens.push(tokens[i]);
4301
- }
4302
- } else {
4303
- bodyTokens.push(tokens[i]);
4304
- }
4305
- i++;
4306
- }
4296
+ const [bodyTokens, i] = this.collectBlockBody(
4297
+ tokens,
4298
+ start2,
4299
+ "set",
4300
+ "endset",
4301
+ (tagContent) => !tagContent.includes("=")
4302
+ );
4307
4303
  if (name) {
4308
4304
  context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
4309
4305
  }
4310
4306
  return i;
4311
4307
  }
4312
4308
  handleSpaceless(tokens, start2, context) {
4313
- const bodyTokens = [];
4314
- let i = start2 + 1;
4315
- let depth = 0;
4316
- while (i < tokens.length) {
4317
- if (tokens[i][0] === "BLOCK") {
4318
- const [tagContent] = stripTag(tokens[i][1]);
4319
- const tag = tagContent.split(/\s+/)[0] || "";
4320
- if (tag === "spaceless") {
4321
- depth++;
4322
- bodyTokens.push(tokens[i]);
4323
- } else if (tag === "endspaceless") {
4324
- if (depth === 0) {
4325
- i++;
4326
- break;
4327
- }
4328
- depth--;
4329
- bodyTokens.push(tokens[i]);
4330
- } else {
4331
- bodyTokens.push(tokens[i]);
4332
- }
4333
- } else {
4334
- bodyTokens.push(tokens[i]);
4335
- }
4336
- i++;
4337
- }
4309
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "spaceless", "endspaceless");
4338
4310
  let rendered = this.renderTokens([...bodyTokens], context);
4339
4311
  rendered = rendered.replace(/>\s+</g, "><");
4340
4312
  return [rendered, i];
@@ -4343,31 +4315,7 @@ var init_engine = __esm({
4343
4315
  const [content] = stripTag(tokens[start2][1]);
4344
4316
  const modeMatch = content.match(/^autoescape\s+(false|true)/);
4345
4317
  const autoEscapeOn = !(modeMatch && modeMatch[1] === "false");
4346
- const bodyTokens = [];
4347
- let i = start2 + 1;
4348
- let depth = 0;
4349
- while (i < tokens.length) {
4350
- if (tokens[i][0] === "BLOCK") {
4351
- const [tagContent] = stripTag(tokens[i][1]);
4352
- const tag = tagContent.split(/\s+/)[0] || "";
4353
- if (tag === "autoescape") {
4354
- depth++;
4355
- bodyTokens.push(tokens[i]);
4356
- } else if (tag === "endautoescape") {
4357
- if (depth === 0) {
4358
- i++;
4359
- break;
4360
- }
4361
- depth--;
4362
- bodyTokens.push(tokens[i]);
4363
- } else {
4364
- bodyTokens.push(tokens[i]);
4365
- }
4366
- } else {
4367
- bodyTokens.push(tokens[i]);
4368
- }
4369
- i++;
4370
- }
4318
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "autoescape", "endautoescape");
4371
4319
  if (!autoEscapeOn) {
4372
4320
  const oldAutoEscape = this._autoEscape;
4373
4321
  this._autoEscape = false;
@@ -5721,6 +5669,99 @@ var init_databaseResult = __esm({
5721
5669
  }
5722
5670
  });
5723
5671
 
5672
+ // ../orm/src/modelCollection.ts
5673
+ var ModelCollection;
5674
+ var init_modelCollection = __esm({
5675
+ "../orm/src/modelCollection.ts"() {
5676
+ "use strict";
5677
+ ModelCollection = class extends Array {
5678
+ /** Total rows matching the query's filter (ignores limit/offset). */
5679
+ _total = 0;
5680
+ /** The SQL limit that produced this page. */
5681
+ _limit = 0;
5682
+ /** The SQL offset that produced this page. */
5683
+ _offset = 0;
5684
+ /**
5685
+ * Derived array operations (`map`, `filter`, `slice`, spread, …) build a plain
5686
+ * `Array`, never another `ModelCollection`. This is what defuses the Array-
5687
+ * subclass constructor/species trap: the engine never calls this constructor
5688
+ * with a length during those operations.
5689
+ */
5690
+ static get [Symbol.species]() {
5691
+ return Array;
5692
+ }
5693
+ /**
5694
+ * @param items the page of hydrated model instances (or the length, when the
5695
+ * engine constructs a derived array — defended against here).
5696
+ * @param total total rows matching the query's filter (ignores limit/offset).
5697
+ * @param limit the SQL limit that produced this page.
5698
+ * @param offset the SQL offset that produced this page.
5699
+ */
5700
+ constructor(items, total = 0, limit = 0, offset = 0) {
5701
+ super();
5702
+ if (typeof items === "number") {
5703
+ this.length = items;
5704
+ return;
5705
+ }
5706
+ if (items) {
5707
+ for (let i = 0; i < items.length; i++) {
5708
+ this[i] = items[i];
5709
+ }
5710
+ }
5711
+ this._total = Math.trunc(total) || 0;
5712
+ this._limit = Math.trunc(limit) || 0;
5713
+ this._offset = Math.trunc(offset) || 0;
5714
+ }
5715
+ /**
5716
+ * Total rows matching the query's filter, ignoring limit/offset.
5717
+ *
5718
+ * This is the whole point of the collection: the page slice you are iterating
5719
+ * is capped by `limit`, but this number is the full count of matching rows —
5720
+ * what a pager needs to render "page 3 of 13".
5721
+ */
5722
+ getTotalRecords() {
5723
+ return this._total;
5724
+ }
5725
+ /**
5726
+ * The canonical pagination envelope — seven snake_case keys, identical to
5727
+ * `DatabaseResult.toPaginate()` (ADR-0043) and to the other three frameworks'
5728
+ * `toPaginate()` / `to_paginate()`.
5729
+ *
5730
+ * records the page's rows as plain objects (never re-sliced)
5731
+ * total getTotalRecords() — the true total for the filter
5732
+ * page floor(offset / per_page) + 1
5733
+ * per_page the query's limit
5734
+ * total_pages ceil(total / per_page)
5735
+ * limit the SQL limit actually applied
5736
+ * offset the SQL offset actually applied
5737
+ *
5738
+ * `records` are model dicts (via `toDict()`, the same serialisation the
5739
+ * framework applies to a model in a JSON response), so the JSON a client sees
5740
+ * matches `DatabaseResult` exactly — the result is uniform whether the route
5741
+ * returned a raw `db.fetch()` or an ORM query.
5742
+ */
5743
+ toPaginate() {
5744
+ const perPage = this._limit > 0 ? this._limit : this.length;
5745
+ const page = perPage > 0 ? Math.floor(this._offset / perPage) + 1 : 1;
5746
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this._total / perPage)) : 1;
5747
+ const records = this.map((model) => {
5748
+ const m = model;
5749
+ return m && typeof m.toDict === "function" ? m.toDict() : model;
5750
+ });
5751
+ return {
5752
+ records,
5753
+ total: this._total,
5754
+ page,
5755
+ per_page: perPage,
5756
+ total_pages: totalPages,
5757
+ limit: perPage,
5758
+ offset: this._offset
5759
+ };
5760
+ }
5761
+ };
5762
+ }
5763
+ });
5764
+
5724
5765
  // ../orm/src/databaseUrl.ts
5725
5766
  import { inspect } from "node:util";
5726
5767
  function stripOneSlash(path8) {
@@ -14069,6 +14110,7 @@ var init_baseModel = __esm({
14069
14110
  "../orm/src/baseModel.ts"() {
14070
14111
  "use strict";
14071
14112
  init_database();
14113
+ init_modelCollection();
14072
14114
  init_validation();
14073
14115
  init_queryBuilder();
14074
14116
  init_sqlite();
@@ -14304,6 +14346,30 @@ var init_baseModel = __esm({
14304
14346
  static getPkColumn() {
14305
14347
  return this.getDbColumn(this.getPkField());
14306
14348
  }
14349
+ /**
14350
+ * Shared read tail for the collection-returning finders (where / all / select
14351
+ * / find filter-form / withTrashed). Runs the SAME two calls `db.fetch()`
14352
+ * makes — the page fetch AND the COUNT probe over the SAME base SQL — hydrates
14353
+ * the rows into model instances, and returns a ModelCollection carrying the
14354
+ * total (ADR-0064).
14355
+ *
14356
+ * The total is FREE: `probeTotal` is the exact `COUNT(*)` probe `db.fetch()`
14357
+ * already runs; the ORM used to discard it. ZERO extra queries beyond that one
14358
+ * probe. `sql` MUST NOT carry its own LIMIT/OFFSET — `adapterFetch` applies
14359
+ * limit/offset to the page, and the probe wraps the un-limited SQL so it counts
14360
+ * the WHOLE filtered set, not the page.
14361
+ */
14362
+ static async _collect(sql, params, limit, offset, include) {
14363
+ const db = this.getDb();
14364
+ const rows = await adapterFetch(db, sql, params, limit, offset);
14365
+ const data = Array.isArray(rows) ? rows : rows?.data ?? [];
14366
+ const total = await probeTotal(db, sql, params, limit) ?? data.length;
14367
+ const instances = data.map((row) => new this(row));
14368
+ if (include) {
14369
+ await this._eagerLoad(instances, include);
14370
+ }
14371
+ return new ModelCollection(instances, total, limit, offset);
14372
+ }
14307
14373
  /**
14308
14374
  * Find a record by primary key.
14309
14375
  * @param id Primary key value.
@@ -14356,7 +14422,6 @@ var init_baseModel = __esm({
14356
14422
  );
14357
14423
  }
14358
14424
  const lim = typeof limit === "number" ? limit : 100;
14359
- const db = ModelClass.getDb();
14360
14425
  const conditions = [];
14361
14426
  const params = [];
14362
14427
  if (filter) {
@@ -14376,16 +14441,7 @@ var init_baseModel = __esm({
14376
14441
  if (orderBy) {
14377
14442
  sql += ` ORDER BY ${orderBy}`;
14378
14443
  }
14379
- const rows = await adapterFetch(db, sql, params, lim, offset);
14380
- const data = rows?.data ?? rows;
14381
- const instances = (Array.isArray(data) ? data : []).map((row) => {
14382
- const inst = new this(row);
14383
- return inst;
14384
- });
14385
- if (include) {
14386
- await ModelClass._eagerLoad(instances, include);
14387
- }
14388
- return instances;
14444
+ return ModelClass._collect(sql, params, lim, offset, include);
14389
14445
  }
14390
14446
  /**
14391
14447
  * Load a record into this instance via selectOne.
@@ -14453,7 +14509,6 @@ var init_baseModel = __esm({
14453
14509
  */
14454
14510
  static async all(limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
14455
14511
  const ModelClass = this;
14456
- const db = ModelClass.getDb();
14457
14512
  const conditions = [];
14458
14513
  if (ModelClass.softDelete) {
14459
14514
  conditions.push("is_deleted = 0");
@@ -14463,13 +14518,8 @@ var init_baseModel = __esm({
14463
14518
  }
14464
14519
  const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
14465
14520
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
14466
- const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
14467
- const rows = await adapterQuery(db, sql, []);
14468
- const instances = rows.map((row) => new ModelClass(row));
14469
- if (include) {
14470
- await ModelClass._eagerLoad(instances, include);
14471
- }
14472
- return instances;
14521
+ const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}`;
14522
+ return ModelClass._collect(sql, [], limit, offset, include);
14473
14523
  }
14474
14524
  /**
14475
14525
  * Query records with a WHERE clause.
@@ -14484,7 +14534,6 @@ var init_baseModel = __esm({
14484
14534
  */
14485
14535
  static async where(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
14486
14536
  const ModelClass = this;
14487
- const db = ModelClass.getDb();
14488
14537
  const parts = [];
14489
14538
  if (ModelClass.softDelete) {
14490
14539
  parts.push("is_deleted = 0");
@@ -14494,13 +14543,8 @@ var init_baseModel = __esm({
14494
14543
  }
14495
14544
  parts.push(`(${conditions})`);
14496
14545
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
14497
- const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
14498
- const rows = await adapterQuery(db, sql, params);
14499
- const instances = rows.map((row) => new ModelClass(row));
14500
- if (include) {
14501
- await ModelClass._eagerLoad(instances, include);
14502
- }
14503
- return instances;
14546
+ const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause}`;
14547
+ return ModelClass._collect(sql, params, limit, offset, include);
14504
14548
  }
14505
14549
  /**
14506
14550
  * Save this instance (insert or update). Returns this on success (fluent
@@ -14980,10 +15024,7 @@ var init_baseModel = __esm({
14980
15024
  */
14981
15025
  static async select(sql, params, limit = DEFAULT_ROW_CAP, offset = 0) {
14982
15026
  const ModelClass = this;
14983
- const db = ModelClass.getDb();
14984
- const paged = SQLTranslator.appendLimit(sql, limit, offset);
14985
- const rows = await adapterQuery(db, paged, params);
14986
- return rows.map((row) => new ModelClass(row));
15027
+ return ModelClass._collect(sql, params, limit, offset);
14987
15028
  }
14988
15029
  static async selectOne(sql, params, include) {
14989
15030
  const ModelClass = this;
@@ -15055,7 +15096,6 @@ var init_baseModel = __esm({
15055
15096
  */
15056
15097
  static async withTrashed(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0) {
15057
15098
  const ModelClass = this;
15058
- const db = ModelClass.getDb();
15059
15099
  const parts = [];
15060
15100
  if (ModelClass.tableFilter) {
15061
15101
  parts.push(ModelClass.tableFilter);
@@ -15067,14 +15107,7 @@ var init_baseModel = __esm({
15067
15107
  if (parts.length > 0) {
15068
15108
  sql += ` WHERE ${parts.join(" AND ")}`;
15069
15109
  }
15070
- if (limit !== void 0) {
15071
- sql += ` LIMIT ${limit}`;
15072
- }
15073
- if (offset !== void 0) {
15074
- sql += ` OFFSET ${offset}`;
15075
- }
15076
- const rows = await adapterQuery(db, sql, params);
15077
- return rows.map((row) => new ModelClass(row));
15110
+ return ModelClass._collect(sql, params, limit, offset);
15078
15111
  }
15079
15112
  /**
15080
15113
  * Count records matching conditions (respects soft delete and table filter).
@@ -18413,6 +18446,7 @@ __export(src_exports, {
18413
18446
  InvalidId: () => InvalidId,
18414
18447
  LocalStorage: () => LocalStorage,
18415
18448
  Migration: () => Migration,
18449
+ ModelCollection: () => ModelCollection,
18416
18450
  MongodbAdapter: () => MongodbAdapter,
18417
18451
  MssqlAdapter: () => MssqlAdapter,
18418
18452
  MysqlAdapter: () => MysqlAdapter,
@@ -18517,6 +18551,7 @@ var init_src = __esm({
18517
18551
  "use strict";
18518
18552
  init_types();
18519
18553
  init_databaseResult();
18554
+ init_modelCollection();
18520
18555
  init_database();
18521
18556
  init_database();
18522
18557
  init_databaseUrl();
@@ -35243,7 +35278,7 @@ function toolbarJs() {
35243
35278
  connect();
35244
35279
  })();`;
35245
35280
  }
35246
- 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;
35281
+ 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;
35247
35282
  var init_devAdmin = __esm({
35248
35283
  "src/devAdmin.ts"() {
35249
35284
  "use strict";
@@ -35581,9 +35616,6 @@ var init_devAdmin = __esm({
35581
35616
  { method: "POST", pattern: "/__dev/api/websockets/disconnect", handler: handleWebsocketsDisconnect },
35582
35617
  // Tools
35583
35618
  { method: "POST", pattern: "/__dev/api/tool", handler: handleTool },
35584
- // Chat — proxies to Rust agent /chat (SSE passthrough). Forwards
35585
- // active_file and any other body keys verbatim. See proxyToSupervisor.
35586
- { method: "POST", pattern: "/__dev/api/chat", handler: handleChat },
35587
35619
  // Threads — proxies to Rust agent /threads. Mirrors Python's
35588
35620
  // _api_threads + _api_threads_sub.
35589
35621
  { method: "GET", pattern: "/__dev/api/threads", handler: handleThreads },
@@ -35662,8 +35694,6 @@ var init_devAdmin = __esm({
35662
35694
  { method: "GET", pattern: "/__dev/api/supervise/diff", handler: handleSuperviseStub },
35663
35695
  { method: "POST", pattern: "/__dev/api/supervise/commit", handler: handleSuperviseStub },
35664
35696
  { method: "POST", pattern: "/__dev/api/supervise/cancel", handler: handleSuperviseStub },
35665
- // Execute — proxies to the framework_port+2000 Rust agent (SSE passthrough)
35666
- { method: "POST", pattern: "/__dev/api/execute", handler: handleExecute },
35667
35697
  // Framework-grounding MCP token config — self-contained (.env upsert)
35668
35698
  { method: "GET", pattern: "/__dev/api/grounding/status", handler: handleGroundingStatus },
35669
35699
  { method: "POST", pattern: "/__dev/api/grounding/token", handler: handleGroundingToken },
@@ -36233,9 +36263,6 @@ var init_devAdmin = __esm({
36233
36263
  }
36234
36264
  res.json({ tool, status: "executed", message: `Tool '${tool}' executed (stub)`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
36235
36265
  };
36236
- handleChat = async (req2, res) => {
36237
- await proxyToSupervisor(req2, res, "/chat");
36238
- };
36239
36266
  DEFAULT_MCP_URL = "https://mcp.tina4.com";
36240
36267
  handleGroundingStatus = async (_req, res) => {
36241
36268
  const token = resolveDevEnvVar("TINA4_MCP_TOKEN");
@@ -36523,35 +36550,6 @@ var init_devAdmin = __esm({
36523
36550
  501
36524
36551
  );
36525
36552
  };
36526
- handleExecute = async (req2, res) => {
36527
- const port = parseInt(process.env.TINA4_PORT ?? process.env.PORT ?? "7148", 10);
36528
- const agentUrl = `http://127.0.0.1:${port + 2e3}/execute`;
36529
- try {
36530
- const upstream = await fetch(agentUrl, {
36531
- method: "POST",
36532
- headers: { "Content-Type": "application/json" },
36533
- body: JSON.stringify(req2.body ?? {})
36534
- });
36535
- if (!upstream.body) {
36536
- res.json({ error: "agent returned no body" }, 502);
36537
- return;
36538
- }
36539
- res.raw.writeHead(upstream.status || 200, {
36540
- "Content-Type": upstream.headers.get("content-type") || "text/event-stream",
36541
- "Cache-Control": "no-cache",
36542
- Connection: "keep-alive"
36543
- });
36544
- const reader = upstream.body.getReader();
36545
- while (true) {
36546
- const { done, value } = await reader.read();
36547
- if (done) break;
36548
- if (value) res.raw.write(Buffer.from(value));
36549
- }
36550
- res.raw.end();
36551
- } catch (e) {
36552
- res.json({ error: `agent unreachable at ${agentUrl}: ${e.message}` }, 502);
36553
- }
36554
- };
36555
36553
  DEV_FILES_IGNORED = /* @__PURE__ */ new Set([
36556
36554
  "__pycache__",
36557
36555
  "node_modules",
@@ -37616,12 +37614,46 @@ function sanitizeSecurity(reqs, schemes) {
37616
37614
  });
37617
37615
  }
37618
37616
  function generate2(routes, models = []) {
37617
+ const schemes = resolveSecuritySchemes();
37618
+ const spec = {
37619
+ openapi: resolveOpenApiVersion(),
37620
+ info: buildInfo(),
37621
+ servers: resolveServers(),
37622
+ paths: {},
37623
+ components: {
37624
+ schemas: {},
37625
+ // Configurable security schemes (v3.13.42): bearerFormat via env, optional
37626
+ // apiKey scheme, plus any programmatically-registered schemes (which may
37627
+ // override bearerAuth — e.g. an oauth2 scheme with scopes).
37628
+ securitySchemes: schemes
37629
+ }
37630
+ };
37631
+ const tableToSchema = /* @__PURE__ */ new Map();
37632
+ buildComponentSchemas(models, spec, tableToSchema);
37633
+ const ctx = {
37634
+ models,
37635
+ tableToSchema,
37636
+ schemes,
37637
+ // Default scheme secured routes use when no explicit meta.security is set.
37638
+ defaultScheme: process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth",
37639
+ // Path filters (comma-separated raw-path prefixes).
37640
+ includePrefixes: csv(process.env.TINA4_SWAGGER_INCLUDE),
37641
+ excludePrefixes: csv(process.env.TINA4_SWAGGER_EXCLUDE),
37642
+ // Reusable custom schemas referenced by routes via meta.requestSchema/responseSchemas.
37643
+ refSchemas: /* @__PURE__ */ new Set(),
37644
+ usedTags: [],
37645
+ seenIds: /* @__PURE__ */ new Set()
37646
+ };
37647
+ for (const route of routes) {
37648
+ buildOperation(route, spec, ctx);
37649
+ }
37650
+ buildRefSchemas(spec, ctx.refSchemas);
37651
+ buildTags(spec, ctx.usedTags);
37652
+ return spec;
37653
+ }
37654
+ function buildInfo() {
37619
37655
  const info = {
37620
37656
  title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
37621
- // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
37622
- // 0.0.1). description defaults to the empty string, not a canned sentence.
37623
- // Both are the settled cross-framework defaults (parity with the Python
37624
- // master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
37625
37657
  version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
37626
37658
  description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
37627
37659
  };
@@ -37638,135 +37670,134 @@ function generate2(routes, models = []) {
37638
37670
  const [name, url] = licenseRaw.split("|").map((s) => s.trim());
37639
37671
  info.license = url ? { name, url } : { name };
37640
37672
  }
37641
- const schemes = resolveSecuritySchemes();
37642
- const spec = {
37643
- openapi: resolveOpenApiVersion(),
37644
- info,
37645
- servers: resolveServers(),
37646
- paths: {},
37647
- components: {
37648
- schemas: {},
37649
- // Configurable security schemes (v3.13.42): bearerFormat via env, optional
37650
- // apiKey scheme, plus any programmatically-registered schemes (which may
37651
- // override bearerAuth — e.g. an oauth2 scheme with scopes).
37652
- securitySchemes: schemes
37653
- }
37654
- };
37655
- const defaultScheme = process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth";
37656
- const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
37657
- const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
37658
- const refSchemas = /* @__PURE__ */ new Set();
37659
- const tableToSchema = /* @__PURE__ */ new Map();
37673
+ return info;
37674
+ }
37675
+ function buildComponentSchemas(models, spec, tableToSchema) {
37660
37676
  for (const model of models) {
37661
37677
  const schemaKey = schemaNameForModel(model);
37662
37678
  tableToSchema.set(model.tableName, schemaKey);
37663
37679
  spec.components.schemas[schemaKey] = modelToSchema(model);
37664
37680
  }
37665
- const usedTags = [];
37666
- const seenIds = /* @__PURE__ */ new Set();
37667
- for (const route of routes) {
37668
- if (!isIncludedPath(route.pattern, includePrefixes, excludePrefixes)) continue;
37669
- const openApiPath = patternToOpenAPI(route.pattern);
37670
- const method = route.method.toLowerCase();
37671
- if (!spec.paths[openApiPath]) {
37672
- spec.paths[openApiPath] = {};
37673
- }
37674
- const tags = route.meta?.tags ?? inferTags(route.pattern);
37675
- for (const t of tags) {
37676
- if (!usedTags.includes(t)) usedTags.push(t);
37677
- }
37678
- const operation = {
37679
- operationId: uniqueOperationId(method, openApiPath, seenIds),
37680
- summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
37681
- tags,
37682
- responses: route.meta?.responses ?? {
37683
- "200": { description: "Successful response" }
37684
- }
37685
- };
37686
- if (route.meta?.description) operation.description = route.meta.description;
37687
- if (route.meta?.deprecated) operation.deprecated = true;
37688
- const pathParams = extractPathParams(route.pattern);
37689
- if (pathParams.length > 0) {
37690
- operation.parameters = pathParams.map(({ name, schema }) => ({
37691
- name,
37692
- in: "path",
37693
- required: true,
37694
- schema
37695
- }));
37681
+ }
37682
+ function buildOperation(route, spec, ctx) {
37683
+ if (!isIncludedPath(route.pattern, ctx.includePrefixes, ctx.excludePrefixes)) return;
37684
+ const openApiPath = patternToOpenAPI(route.pattern);
37685
+ const method = route.method.toLowerCase();
37686
+ if (!spec.paths[openApiPath]) {
37687
+ spec.paths[openApiPath] = {};
37688
+ }
37689
+ const tags = route.meta?.tags ?? inferTags(route.pattern);
37690
+ for (const t of tags) {
37691
+ if (!ctx.usedTags.includes(t)) ctx.usedTags.push(t);
37692
+ }
37693
+ const operation = {
37694
+ operationId: uniqueOperationId(method, openApiPath, ctx.seenIds),
37695
+ summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
37696
+ tags,
37697
+ responses: route.meta?.responses ?? {
37698
+ "200": { description: "Successful response" }
37696
37699
  }
37697
- if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
37698
- const modelName = inferModelFromPath(route.pattern);
37699
- if (modelName && models.some((m) => m.tableName === modelName)) {
37700
- operation.parameters = [
37701
- ...operation.parameters ?? [],
37702
- { name: "page", in: "query", schema: { type: "integer", default: 1 } },
37703
- { name: "limit", in: "query", schema: { type: "integer", default: 20 } },
37704
- { name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" }
37705
- ];
37706
- }
37700
+ };
37701
+ if (route.meta?.description) operation.description = route.meta.description;
37702
+ if (route.meta?.deprecated) operation.deprecated = true;
37703
+ const parameters = operationParameters(route, method, ctx.models);
37704
+ if (parameters.length > 0) operation.parameters = parameters;
37705
+ operationRequestBody(route, method, operation, ctx);
37706
+ operationResponseSchemas(route, operation, ctx.refSchemas);
37707
+ operationSecurity(route, method, operation, ctx.schemes, ctx.defaultScheme);
37708
+ spec.paths[openApiPath][method] = operation;
37709
+ }
37710
+ function operationParameters(route, method, models) {
37711
+ let parameters = [];
37712
+ const pathParams = extractPathParams(route.pattern);
37713
+ if (pathParams.length > 0) {
37714
+ parameters = pathParams.map(({ name, schema }) => ({
37715
+ name,
37716
+ in: "path",
37717
+ required: true,
37718
+ schema
37719
+ }));
37720
+ }
37721
+ if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
37722
+ const modelName = inferModelFromPath(route.pattern);
37723
+ if (modelName && models.some((m) => m.tableName === modelName)) {
37724
+ parameters = [
37725
+ ...parameters,
37726
+ { name: "page", in: "query", schema: { type: "integer", default: 1 } },
37727
+ { name: "limit", in: "query", schema: { type: "integer", default: 20 } },
37728
+ { name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" }
37729
+ ];
37707
37730
  }
37708
- const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
37709
- if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
37710
- refSchemas.add(reqSchemaRef.name);
37711
- const media = {
37712
- schema: { $ref: `#/components/schemas/${reqSchemaRef.name}` }
37713
- };
37714
- if (route.meta?.example !== void 0) media.example = route.meta.example;
37731
+ }
37732
+ return parameters;
37733
+ }
37734
+ function mediaWithExample(schema, example) {
37735
+ const media = { schema };
37736
+ if (example !== void 0) media.example = example;
37737
+ return media;
37738
+ }
37739
+ function operationRequestBody(route, method, operation, ctx) {
37740
+ const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
37741
+ if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
37742
+ ctx.refSchemas.add(reqSchemaRef.name);
37743
+ const media = mediaWithExample({ $ref: `#/components/schemas/${reqSchemaRef.name}` }, route.meta?.example);
37744
+ operation.requestBody = {
37745
+ content: { [reqSchemaRef.contentType]: media }
37746
+ };
37747
+ } else if (method === "post" || method === "put") {
37748
+ const modelName = inferModelFromPath(route.pattern);
37749
+ const schemaKey = modelName ? ctx.tableToSchema.get(modelName) : void 0;
37750
+ if (schemaKey) {
37751
+ const sref = `#/components/schemas/${schemaKey}`;
37715
37752
  operation.requestBody = {
37716
- content: { [reqSchemaRef.contentType]: media }
37753
+ required: true,
37754
+ content: { "application/json": mediaWithExample({ $ref: sref }, route.meta?.example) }
37717
37755
  };
37718
- } else if (method === "post" || method === "put") {
37719
- const modelName = inferModelFromPath(route.pattern);
37720
- const schemaKey = modelName ? tableToSchema.get(modelName) : void 0;
37721
- if (schemaKey) {
37722
- const sref = `#/components/schemas/${schemaKey}`;
37723
- const media = { schema: { $ref: sref } };
37724
- if (route.meta?.example !== void 0) media.example = route.meta.example;
37725
- operation.requestBody = {
37726
- required: true,
37727
- content: { "application/json": media }
37728
- };
37729
- if (route.meta?.responses === void 0) {
37730
- operation.responses = {
37731
- "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
37732
- };
37733
- }
37734
- } else if (route.meta?.example !== void 0) {
37735
- operation.requestBody = {
37736
- content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
37756
+ if (route.meta?.responses === void 0) {
37757
+ operation.responses = {
37758
+ "200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
37737
37759
  };
37738
37760
  }
37761
+ } else if (route.meta?.example !== void 0) {
37762
+ operation.requestBody = {
37763
+ content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
37764
+ };
37739
37765
  }
37740
- const respSchemas = parseResponseSchemas(route.meta?.responseSchemas);
37741
- if (respSchemas.length > 0) {
37742
- const responses = operation.responses;
37743
- for (const { status: status2, name, isList } of respSchemas) {
37744
- refSchemas.add(name);
37745
- const sref = `#/components/schemas/${name}`;
37746
- const schema = isList ? { type: "array", items: { $ref: sref } } : { $ref: sref };
37747
- responses[status2] = {
37748
- description: status2.startsWith("2") ? "Successful response" : "Response",
37749
- content: { "application/json": { schema } }
37750
- };
37751
- }
37766
+ }
37767
+ }
37768
+ function operationResponseSchemas(route, operation, refSchemas) {
37769
+ const respSchemas = parseResponseSchemas(route.meta?.responseSchemas);
37770
+ if (respSchemas.length > 0) {
37771
+ const responses = operation.responses;
37772
+ for (const { status: status2, name, isList } of respSchemas) {
37773
+ refSchemas.add(name);
37774
+ const sref = `#/components/schemas/${name}`;
37775
+ const schema = isList ? { type: "array", items: { $ref: sref } } : { $ref: sref };
37776
+ responses[status2] = {
37777
+ description: status2.startsWith("2") ? "Successful response" : "Response",
37778
+ content: { "application/json": { schema } }
37779
+ };
37752
37780
  }
37753
- const hasExplicitSecurity = route.meta?.security !== void 0 || route.meta?.scopes !== void 0 && route.meta.scopes.length > 0;
37754
- if (hasExplicitSecurity) {
37755
- const normalized = normalizeSecurity(route.meta?.security, route.meta?.scopes);
37756
- operation.security = normalized.length > 0 ? sanitizeSecurity(normalized, schemes) : [];
37757
- if (normalized.length > 0) {
37758
- const responses = operation.responses;
37759
- if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
37760
- }
37761
- } else if (routeRequiresAuth(route, method)) {
37762
- const requirements = [{ [defaultScheme]: [] }];
37763
- if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
37764
- operation.security = sanitizeSecurity(requirements, schemes);
37781
+ }
37782
+ }
37783
+ function operationSecurity(route, method, operation, schemes, defaultScheme) {
37784
+ const hasExplicitSecurity = route.meta?.security !== void 0 || route.meta?.scopes !== void 0 && route.meta.scopes.length > 0;
37785
+ if (hasExplicitSecurity) {
37786
+ const normalized = normalizeSecurity(route.meta?.security, route.meta?.scopes);
37787
+ operation.security = normalized.length > 0 ? sanitizeSecurity(normalized, schemes) : [];
37788
+ if (normalized.length > 0) {
37765
37789
  const responses = operation.responses;
37766
37790
  if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
37767
37791
  }
37768
- spec.paths[openApiPath][method] = operation;
37792
+ } else if (routeRequiresAuth(route, method)) {
37793
+ const requirements = [{ [defaultScheme]: [] }];
37794
+ if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
37795
+ operation.security = sanitizeSecurity(requirements, schemes);
37796
+ const responses = operation.responses;
37797
+ if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
37769
37798
  }
37799
+ }
37800
+ function buildRefSchemas(spec, refSchemas) {
37770
37801
  if (refSchemas.size > 0) {
37771
37802
  const schemas = spec.components.schemas;
37772
37803
  for (const name of refSchemas) {
@@ -37775,10 +37806,11 @@ function generate2(routes, models = []) {
37775
37806
  }
37776
37807
  }
37777
37808
  }
37809
+ }
37810
+ function buildTags(spec, usedTags) {
37778
37811
  if (usedTags.length > 0) {
37779
37812
  spec.tags = usedTags.map((name) => ({ name }));
37780
37813
  }
37781
- return spec;
37782
37814
  }
37783
37815
  function routeRequiresAuth(route, method) {
37784
37816
  if (route.noAuth) return false;
@@ -39029,7 +39061,9 @@ function injectIntoHtml(ctx, devToolbar, html) {
39029
39061
  // Suppress the live reloader on the AI/stable port (data-reload="0"); the
39030
39062
  // toolbar JS early-returns when data-reload !== "1". Mirrors PHP's
39031
39063
  // suppressReload flag.
39032
- reload: !ctx.isAiPortRequest
39064
+ // Also suppress on the dev-admin dashboard (any /__dev page): its SPA reloads
39065
+ // itself gently, so the toolbar's full-page reloader must not fire there.
39066
+ reload: !ctx.isAiPortRequest && !ctx.pathname.startsWith("/__dev")
39033
39067
  };
39034
39068
  return injectFeedbackWidget(ctx.req, injectDevToolbar(html, toolbarCtx));
39035
39069
  }
@@ -39284,6 +39318,51 @@ async function runDispatch(ctx, rawReq, rawRes) {
39284
39318
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
39285
39319
  return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
39286
39320
  }
39321
+ function loopbackBindHosts(host) {
39322
+ const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
39323
+ switch (normalized) {
39324
+ case "localhost":
39325
+ return ["127.0.0.1", "::1"];
39326
+ case "127.0.0.1":
39327
+ case "0.0.0.0":
39328
+ return ["::1"];
39329
+ case "::1":
39330
+ case "::":
39331
+ return ["127.0.0.1"];
39332
+ default:
39333
+ return [];
39334
+ }
39335
+ }
39336
+ function startLoopbackSiblings(port, host, dispatch) {
39337
+ const siblingHosts = loopbackBindHosts(host);
39338
+ if (siblingHosts.length === 0) return Promise.resolve([]);
39339
+ return Promise.all(
39340
+ siblingHosts.map(
39341
+ (siblingHost) => new Promise((resolveSibling) => {
39342
+ const sibling = createServer2(dispatch);
39343
+ let settled = false;
39344
+ sibling.on("error", (err) => {
39345
+ const expected = err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT";
39346
+ if (!expected) {
39347
+ Log.debug(
39348
+ `Loopback sibling ${siblingHost}:${port} not bound (${err.code ?? err.message}) \u2014 skipped, primary already serves`
39349
+ );
39350
+ }
39351
+ if (!settled) {
39352
+ settled = true;
39353
+ resolveSibling(null);
39354
+ }
39355
+ });
39356
+ sibling.listen(port, siblingHost, () => {
39357
+ settled = true;
39358
+ resolveSibling(sibling);
39359
+ });
39360
+ })
39361
+ )
39362
+ ).then(
39363
+ (results) => results.filter((s) => s !== null)
39364
+ );
39365
+ }
39287
39366
  async function startServer(config) {
39288
39367
  loadEnv(".env.local");
39289
39368
  loadEnv();
@@ -39552,8 +39631,9 @@ ${reset2}
39552
39631
  }
39553
39632
  });
39554
39633
  return new Promise((resolvePromise) => {
39555
- server.listen(port, host, () => {
39634
+ server.listen(port, host, async () => {
39556
39635
  if (!cluster.isWorker) writePidfile(port);
39636
+ const siblingServers = await startLoopbackSiblings(port, host, dispatch);
39557
39637
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
39558
39638
  const isDebug = isTruthy(process.env.TINA4_DEBUG);
39559
39639
  const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
@@ -39616,14 +39696,22 @@ ${reset2}
39616
39696
  }
39617
39697
  let shuttingDown = false;
39618
39698
  const closeListeners = () => new Promise((done) => {
39619
- let pending = aiServer ? 2 : 1;
39699
+ let pending = 1 + (aiServer ? 1 : 0) + siblingServers.length;
39620
39700
  const one = () => {
39621
39701
  if (--pending === 0) done();
39622
39702
  };
39623
39703
  server.close(one);
39624
39704
  if (aiServer) aiServer.close(one);
39705
+ for (const sibling of siblingServers) {
39706
+ try {
39707
+ sibling.close(one);
39708
+ } catch {
39709
+ one();
39710
+ }
39711
+ }
39625
39712
  server.closeIdleConnections();
39626
39713
  aiServer?.closeIdleConnections();
39714
+ for (const sibling of siblingServers) sibling.closeIdleConnections();
39627
39715
  });
39628
39716
  const gracefulShutdown = async (signal) => {
39629
39717
  if (shuttingDown) return;
@@ -39651,6 +39739,7 @@ ${reset2}
39651
39739
  );
39652
39740
  server.closeAllConnections();
39653
39741
  aiServer?.closeAllConnections();
39742
+ for (const sibling of siblingServers) sibling.closeAllConnections();
39654
39743
  }
39655
39744
  try {
39656
39745
  const orm = await Promise.resolve().then(() => (init_src(), src_exports));
@@ -39678,6 +39767,12 @@ ${reset2}
39678
39767
  stopAllBackgroundTasks();
39679
39768
  if (aiServer) aiServer.close();
39680
39769
  server.close();
39770
+ for (const sibling of siblingServers) {
39771
+ try {
39772
+ sibling.close();
39773
+ } catch {
39774
+ }
39775
+ }
39681
39776
  Promise.resolve().then(() => (init_src(), src_exports)).then((orm) => orm.closeDatabase()).catch(() => {
39682
39777
  });
39683
39778
  },
@@ -46863,6 +46958,7 @@ __export(index_exports, {
46863
46958
  isValidSessionId: () => isValidSessionId,
46864
46959
  kafkaSecurityConfig: () => kafkaSecurityConfig,
46865
46960
  loadEnv: () => loadEnv,
46961
+ loopbackBindHosts: () => loopbackBindHosts,
46866
46962
  makeCaseInsensitiveHeaders: () => makeCaseInsensitiveHeaders,
46867
46963
  matchCronField: () => matchCronField,
46868
46964
  matchesCron: () => matchesCron,
@@ -47232,6 +47328,7 @@ export {
47232
47328
  isValidSessionId,
47233
47329
  kafkaSecurityConfig,
47234
47330
  loadEnv,
47331
+ loopbackBindHosts,
47235
47332
  makeCaseInsensitiveHeaders,
47236
47333
  matchCronField,
47237
47334
  matchesCron,