tina4-nodejs 3.13.148 → 3.13.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CLAUDE.md +4 -3
  2. package/package.json +1 -1
  3. package/packages/cli/dist/bin.js +10898 -9844
  4. package/packages/cli/src/commands/generate.ts +184 -25
  5. package/packages/core/dist/index.js +10864 -9810
  6. package/packages/core/public/bundled-component-provenance.json +2 -2
  7. package/packages/core/public/js/tina4-dev-admin-split.html +202 -0
  8. package/packages/core/public/js/tina4-dev-admin.min.js +147 -138
  9. package/packages/core/src/devAdmin.ts +105 -1
  10. package/packages/core/src/index.ts +3 -0
  11. package/packages/core/src/messenger.ts +57 -1
  12. package/packages/core/src/metrics.ts +5 -1
  13. package/packages/core/src/plan.ts +1 -1
  14. package/packages/core/src/router.ts +11 -1
  15. package/packages/core/src/server.ts +5 -2
  16. package/packages/core/templates/crud/form.twig +26 -0
  17. package/packages/core/templates/crud/modals.twig +31 -0
  18. package/packages/core/templates/crud/page.twig +401 -0
  19. package/packages/core/templates/crud/table.twig +69 -0
  20. package/packages/frond/dist/index.js +9 -1
  21. package/packages/frond/src/engine.ts +9 -0
  22. package/packages/frond/src/index.ts +1 -1
  23. package/packages/orm/dist/index.js +1283 -219
  24. package/packages/orm/src/adapters/mongodb.ts +209 -34
  25. package/packages/orm/src/autoCrud.ts +9 -0
  26. package/packages/orm/src/crud.ts +273 -0
  27. package/packages/orm/src/crudHelpers.ts +194 -0
  28. package/packages/orm/src/crudRender.ts +202 -0
  29. package/packages/orm/src/crudTable.ts +171 -0
  30. package/packages/orm/src/index.ts +7 -1
  31. package/packages/orm/src/migration.ts +167 -4
  32. package/packages/orm/src/query.ts +99 -40
  33. package/packages/orm/src/sqlTranslator.ts +8 -1
  34. package/packages/orm/src/types.ts +19 -0
  35. package/types/core/src/devAdmin.d.ts +23 -0
  36. package/types/core/src/messenger.d.ts +4 -0
  37. package/types/core/src/router.d.ts +0 -5
  38. package/types/frond/src/engine.d.ts +6 -0
  39. package/types/frond/src/index.d.ts +1 -1
  40. package/types/orm/src/crud.d.ts +82 -0
  41. package/types/orm/src/crudHelpers.d.ts +63 -0
  42. package/types/orm/src/crudRender.d.ts +38 -0
  43. package/types/orm/src/crudTable.d.ts +45 -0
  44. package/types/orm/src/index.d.ts +3 -1
  45. package/types/orm/src/migration.d.ts +0 -18
  46. package/types/orm/src/query.d.ts +0 -12
  47. package/types/orm/src/types.d.ts +19 -0
@@ -1467,7 +1467,10 @@ var init_sqlTranslator = __esm({
1467
1467
  static appendLimit(sql, limit, offset) {
1468
1468
  if (limit === void 0 || limit === null || limit <= 0) return sql;
1469
1469
  if (_SQLTranslator.hasTrailingLimit(sql)) return sql;
1470
- const trimmed = sql.replace(/[\s;]+$/, "");
1470
+ const trailing = " \n\r\f\v;";
1471
+ let end = sql.length;
1472
+ while (end > 0 && trailing.includes(sql[end - 1])) end--;
1473
+ const trimmed = sql.slice(0, end);
1471
1474
  const suffix = offset !== void 0 && offset > 0 ? `LIMIT ${limit} OFFSET ${offset}` : `LIMIT ${limit}`;
1472
1475
  return `${trimmed}
1473
1476
  ${suffix}`;
@@ -2791,11 +2794,11 @@ function getToken(payload, secretOrExpiresIn, expiresIn = 60, algorithm) {
2791
2794
  if (resolvedExpiresIn !== 0) {
2792
2795
  claims.exp = now + resolvedExpiresIn * 60;
2793
2796
  }
2794
- const h = base64urlEncode(Buffer.from(JSON.stringify(header)));
2797
+ const h2 = base64urlEncode(Buffer.from(JSON.stringify(header)));
2795
2798
  const p = base64urlEncode(Buffer.from(JSON.stringify(claims)));
2796
- const signingInput = `${h}.${p}`;
2799
+ const signingInput = `${h2}.${p}`;
2797
2800
  const signature = sign(signingInput, resolvedSecret, resolvedAlgorithm);
2798
- return `${h}.${p}.${signature}`;
2801
+ return `${h2}.${p}.${signature}`;
2799
2802
  }
2800
2803
  function validToken(token, secret, algorithm) {
2801
2804
  const resolvedSecret = secret ?? process.env.TINA4_SECRET ?? "";
@@ -2806,8 +2809,8 @@ function validToken(token, secret, algorithm) {
2806
2809
  try {
2807
2810
  const parts = token.split(".");
2808
2811
  if (parts.length !== 3) return null;
2809
- const [h, p, sig] = parts;
2810
- const header = JSON.parse(base64urlDecode(h).toString());
2812
+ const [h2, p, sig] = parts;
2813
+ const header = JSON.parse(base64urlDecode(h2).toString());
2811
2814
  if (header.alg !== resolvedAlgorithm) return null;
2812
2815
  if (HMAC_DIGESTS.has(resolvedAlgorithm)) {
2813
2816
  const weak = insecureSecretMessage(resolvedSecret);
@@ -2816,7 +2819,7 @@ function validToken(token, secret, algorithm) {
2816
2819
  return null;
2817
2820
  }
2818
2821
  }
2819
- const signingInput = `${h}.${p}`;
2822
+ const signingInput = `${h2}.${p}`;
2820
2823
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
2821
2824
  return null;
2822
2825
  }
@@ -4593,7 +4596,7 @@ function any(path10, handler, middlewares, meta) {
4593
4596
  function websocket(path10, handler, options) {
4594
4597
  return defaultRouter.websocket(path10, handler, options);
4595
4598
  }
4596
- var WsRouteRef, RouteRef, Router, RouteGroup, defaultRouter;
4599
+ var WsRouteRef, RouteRef, Router, RouteGroup, TINA4_DEFAULT_ROUTER, globalRouterHost, defaultRouter;
4597
4600
  var init_router = __esm({
4598
4601
  "../core/src/router.ts"() {
4599
4602
  "use strict";
@@ -5230,7 +5233,9 @@ var init_router = __esm({
5230
5233
  callback(nestedGroup);
5231
5234
  }
5232
5235
  };
5233
- defaultRouter = new Router();
5236
+ TINA4_DEFAULT_ROUTER = /* @__PURE__ */ Symbol.for("tina4.defaultRouter");
5237
+ globalRouterHost = globalThis;
5238
+ defaultRouter = globalRouterHost[TINA4_DEFAULT_ROUTER] ?? (globalRouterHost[TINA4_DEFAULT_ROUTER] = new Router());
5234
5239
  }
5235
5240
  });
5236
5241
 
@@ -5480,6 +5485,7 @@ __export(engine_exports, {
5480
5485
  Frond: () => Frond,
5481
5486
  MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
5482
5487
  TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
5488
+ escapeHtml: () => escapeHtml,
5483
5489
  expressionFormCache: () => expressionFormCache,
5484
5490
  filterChainCache: () => filterChainCache,
5485
5491
  pathParseCache: () => pathParseCache,
@@ -6404,6 +6410,9 @@ function splitArgs(raw) {
6404
6410
  function htmlEscape(str) {
6405
6411
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
6406
6412
  }
6413
+ function escapeHtml(value) {
6414
+ return htmlEscape(String(value));
6415
+ }
6407
6416
  function jsEscape(value) {
6408
6417
  let out = "";
6409
6418
  for (const ch of String(value)) {
@@ -6580,11 +6589,11 @@ function _buildFormTokenJwt(descriptor = "") {
6580
6589
  if (_formTokenSessionId) {
6581
6590
  payload.session_id = _formTokenSessionId;
6582
6591
  }
6583
- const h = _b64url(Buffer.from(JSON.stringify(header)));
6592
+ const h2 = _b64url(Buffer.from(JSON.stringify(header)));
6584
6593
  const p = _b64url(Buffer.from(JSON.stringify(payload)));
6585
- const sigInput = `${h}.${p}`;
6594
+ const sigInput = `${h2}.${p}`;
6586
6595
  const sig = _b64url(createHmac2("sha256", secret).update(sigInput).digest());
6587
- return `${h}.${p}.${sig}`;
6596
+ return `${h2}.${p}.${sig}`;
6588
6597
  }
6589
6598
  function _generateFormToken(descriptor = "") {
6590
6599
  const token = _buildFormTokenJwt(descriptor);
@@ -13503,7 +13512,7 @@ function enginePath() {
13503
13512
  function runEngine(target) {
13504
13513
  const binary = enginePath();
13505
13514
  if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
13506
- const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
13515
+ const processResult = spawnSync(binary, ["metrics", "--path", target, "--json", "--no-history"], {
13507
13516
  encoding: "utf8",
13508
13517
  timeout: 6e4,
13509
13518
  maxBuffer: 64 * 1024 * 1024
@@ -13827,8 +13836,9 @@ __export(generate_exports, {
13827
13836
  toSnake: () => toSnake,
13828
13837
  toTableName: () => toTableName
13829
13838
  });
13830
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "node:fs";
13831
- import { join as join15, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
13839
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, readdirSync as readdirSync6, copyFileSync } from "node:fs";
13840
+ import { join as join15, relative as relative2, resolve as resolve6, sep as sep3, dirname as dirname7 } from "node:path";
13841
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
13832
13842
  function ensureDir(dir) {
13833
13843
  if (__resolution.dryRun) return;
13834
13844
  if (!existsSync13(dir)) {
@@ -13911,7 +13921,7 @@ function currentResolution() {
13911
13921
  transformations: [...__resolution.body.transformations]
13912
13922
  };
13913
13923
  if (__resolution.body.edit_hints) {
13914
- body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
13924
+ body.edit_hints = __resolution.body.edit_hints.map((h2) => ({ ...h2 }));
13915
13925
  }
13916
13926
  if (__resolution.body.next) {
13917
13927
  body.next = [...__resolution.body.next];
@@ -14437,23 +14447,144 @@ ${aiFill(`delete_${singular}`, {
14437
14447
  }
14438
14448
  function generateCrud(name, flags) {
14439
14449
  const table2 = resolveTable(name, flags, { announce: false });
14440
- const routeName = pluralizeReserved(toSnake(name));
14441
14450
  const isPublic = Boolean(flags.public);
14442
14451
  if (!__resolution.jsonMode) console.log(`
14443
14452
  Generating CRUD for ${name}...
14444
14453
  `);
14445
14454
  generateModel(name, flags, false);
14446
- generateRoute(routeName, { ...flags, model: name }, false);
14447
- generateForm(name, flags);
14448
- generateView(name, flags);
14449
- generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
14455
+ generateCrudAdminRoute(name, table2, isPublic);
14456
+ if (!flags["no-templates"]) copyCrudTemplates();
14457
+ generateCrudGateTest(name, table2, isPublic);
14450
14458
  if (!__resolution.jsonMode) {
14451
14459
  console.log(`
14452
14460
  CRUD generation complete for ${name}.`);
14453
14461
  console.log(" Run: tina4nodejs migrate");
14454
- console.log(" Visit: /swagger to see the API docs");
14462
+ console.log(` Visit: /admin/${table2} for the admin UI, or /swagger for the API docs`);
14455
14463
  }
14456
14464
  }
14465
+ function generateCrudAdminRoute(model, table2, isPublic) {
14466
+ const dir = resolve6("src/routes/admin", table2);
14467
+ ensureDir(dir);
14468
+ pushRoute(`/admin/${table2}`);
14469
+ if (__resolution.target === "crud") {
14470
+ setResolutionField("file_path", `src/routes/admin/${table2}/get.ts`);
14471
+ }
14472
+ const secureExport = isPublic ? "" : "export const secure = true;\n\n";
14473
+ const publicOpt = isPublic ? ", public: true" : "";
14474
+ const writeDoc = isPublic ? "--public: the admin page and the writes are OPEN (no token)." : "Secure by default: the admin page AND the writes require a valid Bearer token (pass --public to open them).";
14475
+ const pageDoc = isPublic ? "" : `
14476
+ // NOTE: the admin page is secured (export const secure = true). A browser needs a valid
14477
+ // token/session to open it; wire your login (Auth / Session) or run
14478
+ // \`generate crud ${model} --public\` for an open page. The AutoCrud READ API
14479
+ // (GET /api/${table2}, GET /api/${table2}/{id}) stays public by AutoCrud's default.`;
14480
+ const content = `import ${model} from "../../../models/${model}.js";
14481
+ import { Crud } from "tina4-nodejs/orm";
14482
+ import type { Tina4Request, Tina4Response } from "tina4-nodejs";
14483
+
14484
+ // ${model} admin \u2014 one server-rendered CRUD page (searchable, sortable,
14485
+ // paginated table + create/edit/delete modals). The REST backend (GET list,
14486
+ // GET/{id}, POST, PUT, DELETE) is AutoCrud; this file owns only the GET admin
14487
+ // page. ${writeDoc}${pageDoc}
14488
+ //
14489
+ // Restyle the UI by editing src/templates/crud/*.twig (copied into this app).
14490
+ // Register the AutoCrud backend for the model at load (idempotent).
14491
+ await Crud.registerBackend(${model}, { public: ${isPublic} });
14492
+
14493
+ ${secureExport}export const meta = { summary: "${model} admin", tags: ["admin"] };
14494
+
14495
+ export default async function (req: Tina4Request, res: Tina4Response) {
14496
+ return res.html(await Crud.toCrud(req, { model: ${model}, title: "${model} Admin"${publicOpt} }));
14497
+ }
14498
+ `;
14499
+ writeFileSafe(join15(dir, "get.ts"), content);
14500
+ }
14501
+ function frameworkCrudTemplatesDir() {
14502
+ const here = dirname7(fileURLToPath4(import.meta.url));
14503
+ const candidates = [
14504
+ resolve6(here, "../../../core/templates/crud"),
14505
+ // monorepo: packages/cli/src/commands -> packages/core/templates/crud
14506
+ resolve6(here, "../../core/templates/crud"),
14507
+ // built layout fallback
14508
+ resolve6(here, "../templates/crud")
14509
+ // flattened publish fallback
14510
+ ];
14511
+ return candidates.find((c) => existsSync13(c)) ?? null;
14512
+ }
14513
+ function copyCrudTemplates() {
14514
+ const sourceDir = frameworkCrudTemplatesDir();
14515
+ if (!sourceDir) {
14516
+ if (!__resolution.jsonMode) console.log(" Skipped crud templates (framework templates not found)");
14517
+ return;
14518
+ }
14519
+ const targetDir = resolve6("src/templates/crud");
14520
+ ensureDir(targetDir);
14521
+ for (const file of readdirSync6(sourceDir).filter((f) => f.endsWith(".twig")).sort()) {
14522
+ const dest = join15(targetDir, file);
14523
+ if (existsSync13(dest)) {
14524
+ if (!__resolution.jsonMode) console.log(` File already exists: ${dest}`);
14525
+ continue;
14526
+ }
14527
+ if (__resolution.dryRun) continue;
14528
+ copyFileSync(join15(sourceDir, file), dest);
14529
+ __resolution.actionsTaken.push(`wrote ${dest}`);
14530
+ if (!__resolution.jsonMode) console.log(` Created ${dest}`);
14531
+ }
14532
+ }
14533
+ function generateCrudGateTest(model, table2, isPublic) {
14534
+ const dir = resolve6("tests");
14535
+ ensureDir(dir);
14536
+ const posture = isPublic ? "open (--public)" : "gated";
14537
+ const writeCase = isPublic ? ` // --public opened the write: no 401 gate (the handler, not the gate, answers).
14538
+ assert("anonymous POST is NOT gated (--public)",
14539
+ (await client.post("/api/${table2}", { json: { name: "test" } })).status !== 401);` : ` // Secure by default: a tokenless POST is rejected by the gate with 401.
14540
+ assert("anonymous POST is gated -> 401",
14541
+ (await client.post("/api/${table2}", { json: { name: "test" } })).status === 401);
14542
+ // A valid Bearer token passes the gate (the gate no longer answers 401).
14543
+ const token = getToken({ userId: 1 });
14544
+ assert("authenticated POST passes the gate (not 401)",
14545
+ (await client.post("/api/${table2}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status !== 401);`;
14546
+ const pageCase = isPublic ? ` assert("admin page is NOT gated (--public)", (await client.get("/admin/${table2}")).status !== 401);` : ` assert("admin page is secured -> 401 without a token", (await client.get("/admin/${table2}")).status === 401);`;
14547
+ const content = `/**
14548
+ * ${model} CRUD admin (ADR-0094) \u2014 reads public, writes ${posture}, page ${isPublic ? "public" : "secured"}.
14549
+ *
14550
+ * Real end-to-end via TestClient: no mocks \u2014 real Router, real auth gate, real
14551
+ * JWT, real route discovery. The admin route registers the AutoCrud backend at
14552
+ * load; discoverRoutes imports it, so the /api/${table2} REST routes exist here.
14553
+ * This gate test proves the SECURE-BY-DEFAULT posture (what registers, what is
14554
+ * gated vs open) \u2014 adapter-independent, since the auth gate runs before the
14555
+ * handler. Run with: npx tsx tests/${table2}.test.ts
14556
+ */
14557
+ import { dirname, resolve } from "node:path";
14558
+ import { fileURLToPath } from "node:url";
14559
+ import { defaultRouter, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
14560
+
14561
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
14562
+ const here = dirname(fileURLToPath(import.meta.url));
14563
+
14564
+ let pass = 0;
14565
+ let fail = 0;
14566
+ function assert(label: string, ok: boolean): void {
14567
+ if (ok) { pass++; console.log(\` PASS \${label}\`); }
14568
+ else { fail++; console.log(\` FAIL \${label}\`); }
14569
+ }
14570
+
14571
+ // Discovering the routes imports src/routes/admin/${table2}/get.ts, whose
14572
+ // top-level Crud.registerBackend() adds the AutoCrud routes to defaultRouter.
14573
+ for (const def of await discoverRoutes(resolve(here, "../src/routes"))) defaultRouter.addRoute(def);
14574
+ const patterns = defaultRouter.getRoutes().map((r) => \`\${r.method} \${r.pattern}\`);
14575
+ const client = new TestClient();
14576
+
14577
+ assert("AutoCrud GET list route registered", patterns.includes("GET /api/${table2}"));
14578
+ assert("AutoCrud POST route registered", patterns.includes("POST /api/${table2}"));
14579
+ assert("admin page route registered", patterns.includes("GET /admin/${table2}"));
14580
+ ${pageCase}
14581
+ ${writeCase}
14582
+
14583
+ console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
14584
+ process.exit(fail > 0 ? 1 : 0);
14585
+ `;
14586
+ writeFileSafe(join15(dir, `${table2}.test.ts`), content);
14587
+ }
14457
14588
  function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
14458
14589
  const ts = timestamp();
14459
14590
  const dir = resolve6("migrations");
@@ -14595,7 +14726,7 @@ import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
14595
14726
  import { initDatabase } from "tina4-nodejs/orm";
14596
14727
  import ${model} from "../src/models/${model}.js";
14597
14728
 
14598
- process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
14729
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
14599
14730
  const here = dirname(fileURLToPath(import.meta.url));
14600
14731
 
14601
14732
  let pass = 0;
@@ -15558,7 +15689,7 @@ import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
15558
15689
  import { initDatabase } from "tina4-nodejs/orm";
15559
15690
  import User from "../src/models/User.js";
15560
15691
 
15561
- process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
15692
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "tina4-test-secret-0123456789abcdef0123456789";
15562
15693
  delete process.env.TINA4_API_KEY;
15563
15694
  const here = dirname(fileURLToPath(import.meta.url));`;
15564
15695
  const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
@@ -15745,7 +15876,7 @@ var init_generate = __esm({
15745
15876
  GENERATORS = {
15746
15877
  model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
15747
15878
  route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
15748
- crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
15879
+ crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public] [--no-templates]', summary: "Model + migration + AutoCrud-backed admin page (toCrud) + editable crud/ templates + gate test" },
15749
15880
  migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
15750
15881
  middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
15751
15882
  test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
@@ -15774,8 +15905,9 @@ var init_generate = __esm({
15774
15905
  ],
15775
15906
  crud: ({ name, table: table2 }) => [
15776
15907
  `Apply the migration: npx tina4nodejs migrate`,
15777
- `Serve and try: npx tina4nodejs serve -> visit /swagger`,
15778
- `Run the gate test: npx tsx tests/${pluralizeReserved(toSnake(name))}.test.ts`,
15908
+ `Serve and try: npx tina4nodejs serve -> visit /admin/${table2} (or /swagger)`,
15909
+ `Run the gate test: npx tsx tests/${table2}.test.ts`,
15910
+ `Restyle the UI: edit src/templates/crud/*.twig`,
15779
15911
  `Change fields: edit src/models/${name}.ts then re-run generate crud`
15780
15912
  ],
15781
15913
  migration: () => [
@@ -18724,8 +18856,8 @@ __export(context_exports, {
18724
18856
  fts5Supported: () => fts5Supported
18725
18857
  });
18726
18858
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
18727
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
18728
- import { basename as basename4, dirname as dirname8, extname as extname4, isAbsolute as isAbsolute4, join as join17, relative as relative4, resolve as resolve8 } from "node:path";
18859
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync8, realpathSync as realpathSync5 } from "node:fs";
18860
+ import { basename as basename4, dirname as dirname9, extname as extname4, isAbsolute as isAbsolute4, join as join17, relative as relative4, resolve as resolve8 } from "node:path";
18729
18861
  function fts5Supported() {
18730
18862
  try {
18731
18863
  const conn = new DatabaseSync2(":memory:");
@@ -18748,7 +18880,7 @@ function realResolve(abs) {
18748
18880
  } catch {
18749
18881
  }
18750
18882
  try {
18751
- return join17(realpathSync5(dirname8(abs)), basename4(abs));
18883
+ return join17(realpathSync5(dirname9(abs)), basename4(abs));
18752
18884
  } catch {
18753
18885
  return abs;
18754
18886
  }
@@ -18836,7 +18968,7 @@ var init_context = __esm({
18836
18968
  const check = fts5Check ?? fts5Supported;
18837
18969
  this.available = Boolean(check());
18838
18970
  if (!this.available) return;
18839
- const parent = dirname8(this.path);
18971
+ const parent = dirname9(this.path);
18840
18972
  if (parent !== "" && parent !== ".") {
18841
18973
  mkdirSync10(parent, { recursive: true });
18842
18974
  }
@@ -18914,7 +19046,7 @@ var init_context = __esm({
18914
19046
  this.root = rootAbs;
18915
19047
  let total = 0;
18916
19048
  const walk2 = (dir) => {
18917
- const entries = readdirSync7(dir, { withFileTypes: true });
19049
+ const entries = readdirSync8(dir, { withFileTypes: true });
18918
19050
  const files = [];
18919
19051
  const subdirs = [];
18920
19052
  for (const e of entries) {
@@ -20830,7 +20962,7 @@ var init_mongoBackend = __esm({
20830
20962
  return null;
20831
20963
  }
20832
20964
  }
20833
- push(queue, payload, delay, priority) {
20965
+ push(queue, payload, delay2, priority) {
20834
20966
  const id = randomUUID4();
20835
20967
  const now = (/* @__PURE__ */ new Date()).toISOString();
20836
20968
  const job = {
@@ -20839,7 +20971,7 @@ var init_mongoBackend = __esm({
20839
20971
  status: "pending",
20840
20972
  createdAt: now,
20841
20973
  attempts: 0,
20842
- delayUntil: delay ? new Date(Date.now() + delay * 1e3).toISOString() : null,
20974
+ delayUntil: delay2 ? new Date(Date.now() + delay2 * 1e3).toISOString() : null,
20843
20975
  // The pop sort has always been { priority: -1, createdAt: 1 }, but this
20844
20976
  // field was never written, so every job scored undefined and the queue
20845
20977
  // ran pure FIFO. Priority did not even reach here: the backend interface
@@ -21008,7 +21140,7 @@ var init_job = __esm({
21008
21140
  });
21009
21141
 
21010
21142
  // ../core/src/queueBackends/liteBackend.ts
21011
- import { mkdirSync as mkdirSync11, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
21143
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
21012
21144
  import { join as join18 } from "node:path";
21013
21145
  import { randomUUID as randomUUID5 } from "node:crypto";
21014
21146
  var LiteBackend;
@@ -21070,7 +21202,7 @@ var init_liteBackend = __esm({
21070
21202
  */
21071
21203
  close() {
21072
21204
  }
21073
- push(queue, payload, delay, priority) {
21205
+ push(queue, payload, delay2, priority) {
21074
21206
  const dir = this.ensureDir(queue);
21075
21207
  const id = randomUUID5();
21076
21208
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -21080,7 +21212,7 @@ var init_liteBackend = __esm({
21080
21212
  status: "pending",
21081
21213
  createdAt: now,
21082
21214
  attempts: 0,
21083
- delayUntil: delay ? new Date(Date.now() + delay * 1e3).toISOString() : null,
21215
+ delayUntil: delay2 ? new Date(Date.now() + delay2 * 1e3).toISOString() : null,
21084
21216
  priority: priority ?? 0,
21085
21217
  topic: queue,
21086
21218
  error: void 0
@@ -21099,7 +21231,7 @@ var init_liteBackend = __esm({
21099
21231
  const dir = this.ensureDir(queue);
21100
21232
  let filenames;
21101
21233
  try {
21102
- filenames = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21234
+ filenames = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21103
21235
  } catch {
21104
21236
  return [];
21105
21237
  }
@@ -21164,7 +21296,7 @@ var init_liteBackend = __esm({
21164
21296
  const reservedDir = this.ensureReservedDir(queue);
21165
21297
  let filenames;
21166
21298
  try {
21167
- filenames = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
21299
+ filenames = readdirSync9(reservedDir).filter((f) => f.endsWith(".queue-data"));
21168
21300
  } catch {
21169
21301
  return;
21170
21302
  }
@@ -21275,7 +21407,7 @@ var init_liteBackend = __esm({
21275
21407
  const scanDir = isDead ? this.ensureFailedDir(queue) : isReserved ? this.ensureReservedDir(queue) : this.ensureDir(queue);
21276
21408
  let files;
21277
21409
  try {
21278
- files = readdirSync8(scanDir).filter((f) => f.endsWith(".queue-data"));
21410
+ files = readdirSync9(scanDir).filter((f) => f.endsWith(".queue-data"));
21279
21411
  } catch {
21280
21412
  return 0;
21281
21413
  }
@@ -21296,7 +21428,7 @@ var init_liteBackend = __esm({
21296
21428
  const dir = this.ensureDir(queue);
21297
21429
  let count = 0;
21298
21430
  try {
21299
- const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21431
+ const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21300
21432
  for (const file of files) {
21301
21433
  unlinkSync6(join18(dir, file));
21302
21434
  count++;
@@ -21306,7 +21438,7 @@ var init_liteBackend = __esm({
21306
21438
  const failedDir = join18(dir, "failed");
21307
21439
  try {
21308
21440
  if (existsSync16(failedDir)) {
21309
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
21441
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21310
21442
  for (const file of files) {
21311
21443
  unlinkSync6(join18(failedDir, file));
21312
21444
  count++;
@@ -21317,7 +21449,7 @@ var init_liteBackend = __esm({
21317
21449
  const reservedDir = join18(dir, "reserved");
21318
21450
  try {
21319
21451
  if (existsSync16(reservedDir)) {
21320
- const files = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
21452
+ const files = readdirSync9(reservedDir).filter((f) => f.endsWith(".queue-data"));
21321
21453
  for (const file of files) {
21322
21454
  unlinkSync6(join18(reservedDir, file));
21323
21455
  count++;
@@ -21339,7 +21471,7 @@ var init_liteBackend = __esm({
21339
21471
  const dir = this.ensureDir(queue);
21340
21472
  const results = [];
21341
21473
  try {
21342
- const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
21474
+ const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data")).sort();
21343
21475
  for (const file of files) {
21344
21476
  try {
21345
21477
  const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
@@ -21363,7 +21495,7 @@ var init_liteBackend = __esm({
21363
21495
  */
21364
21496
  retry(queue, jobId, delaySeconds) {
21365
21497
  try {
21366
- const queues = readdirSync8(this.basePath);
21498
+ const queues = readdirSync9(this.basePath);
21367
21499
  for (const q of queues) {
21368
21500
  const failedDir = join18(this.basePath, q, "failed");
21369
21501
  const filePath = join18(failedDir, `${jobId}.queue-data`);
@@ -21389,7 +21521,7 @@ var init_liteBackend = __esm({
21389
21521
  const failedDir = this.ensureFailedDir(queue);
21390
21522
  const results = [];
21391
21523
  try {
21392
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
21524
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
21393
21525
  for (const file of files) {
21394
21526
  try {
21395
21527
  const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
@@ -21410,7 +21542,7 @@ var init_liteBackend = __esm({
21410
21542
  if (isDead) {
21411
21543
  const failedDir = this.ensureFailedDir(queue);
21412
21544
  try {
21413
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
21545
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21414
21546
  for (const file of files) {
21415
21547
  try {
21416
21548
  unlinkSync6(join18(failedDir, file));
@@ -21423,7 +21555,7 @@ var init_liteBackend = __esm({
21423
21555
  } else {
21424
21556
  const dir = this.ensureDir(queue);
21425
21557
  try {
21426
- const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21558
+ const files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21427
21559
  for (const file of files) {
21428
21560
  try {
21429
21561
  const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
@@ -21449,7 +21581,7 @@ var init_liteBackend = __esm({
21449
21581
  const queueDir = this.ensureDir(queue);
21450
21582
  let count = 0;
21451
21583
  try {
21452
- const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
21584
+ const files = readdirSync9(failedDir).filter((f) => f.endsWith(".queue-data"));
21453
21585
  for (const file of files) {
21454
21586
  try {
21455
21587
  const filePath = join18(failedDir, file);
@@ -21476,7 +21608,7 @@ var init_liteBackend = __esm({
21476
21608
  const dir = this.ensureDir(queue);
21477
21609
  let files;
21478
21610
  try {
21479
- files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
21611
+ files = readdirSync9(dir).filter((f) => f.endsWith(".queue-data"));
21480
21612
  } catch {
21481
21613
  return null;
21482
21614
  }
@@ -21717,11 +21849,11 @@ var init_queue = __esm({
21717
21849
  *
21718
21850
  * @param priority — Higher value = higher priority. Default 0.
21719
21851
  */
21720
- push(payload, delay, priority = 0) {
21852
+ push(payload, delay2, priority = 0) {
21721
21853
  if (this.externalBackend) {
21722
- return this.externalBackend.push(this.topic, payload, delay, priority);
21854
+ return this.externalBackend.push(this.topic, payload, delay2, priority);
21723
21855
  }
21724
- return this.liteBackend.push(this.topic, payload, delay, priority);
21856
+ return this.liteBackend.push(this.topic, payload, delay2, priority);
21725
21857
  }
21726
21858
  /**
21727
21859
  * Atomically claim the next available job from this queue's topic. Returns null if empty.
@@ -21952,11 +22084,11 @@ var init_queue = __esm({
21952
22084
  * shares the same retarget path consume()/process() use — keeping produce and
21953
22085
  * consume symmetric on the same topic argument.
21954
22086
  */
21955
- produce(topic, payload, priority = 0, delay = 0) {
22087
+ produce(topic, payload, priority = 0, delay2 = 0) {
21956
22088
  const previous = this.topic;
21957
22089
  this.retarget(topic);
21958
22090
  try {
21959
- return this.push(payload, delay, priority);
22091
+ return this.push(payload, delay2, priority);
21960
22092
  } finally {
21961
22093
  this.retarget(previous);
21962
22094
  }
@@ -22558,7 +22690,7 @@ var init_plan = __esm({
22558
22690
  const existing = (currentPlan.steps || []).map((s) => s.text || "");
22559
22691
  const title = currentPlan.title || target;
22560
22692
  const goal = currentPlan.goal || "";
22561
- const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11437/api/chat";
22693
+ const aiUrl = process.env.TINA4_AI_URL || "http://localhost:11435/api/chat";
22562
22694
  const aiModel = process.env.TINA4_AI_MODEL || "qwen2.5-coder:14b";
22563
22695
  const prompts = fleshPrompt(title, goal, existing, prompt);
22564
22696
  const fetched = await fetchFleshReply(aiUrl, aiModel, prompts);
@@ -22957,7 +23089,7 @@ var init_projectIndex = __esm({
22957
23089
  }
22958
23090
  }
22959
23091
  hits.sort((a, b) => b[0] - a[0]);
22960
- return hits.slice(0, Math.max(1, limit)).map((h) => h[1]);
23092
+ return hits.slice(0, Math.max(1, limit)).map((h2) => h2[1]);
22961
23093
  },
22962
23094
  fileEntry(relPath) {
22963
23095
  ProjectIndex.refresh();
@@ -23486,7 +23618,7 @@ __export(docs_exports, {
23486
23618
  });
23487
23619
  import * as fs8 from "node:fs";
23488
23620
  import * as path8 from "node:path";
23489
- import { fileURLToPath as fileURLToPath4 } from "node:url";
23621
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
23490
23622
  function detectFrameworkRoots() {
23491
23623
  const corePackageRoot = path8.resolve(__DIRNAME, "..");
23492
23624
  const packagesRoot = path8.resolve(corePackageRoot, "..");
@@ -23893,7 +24025,7 @@ var init_docs = __esm({
23893
24025
  "hash"
23894
24026
  ]);
23895
24027
  _DOCS_CACHE = /* @__PURE__ */ new Map();
23896
- __FILENAME = fileURLToPath4(import.meta.url);
24028
+ __FILENAME = fileURLToPath5(import.meta.url);
23897
24029
  __DIRNAME = path8.dirname(__FILENAME);
23898
24030
  USER_DIRS = ["src/orm", "src/routes", "src/app", "src/services", "src/models"];
23899
24031
  CAMEL_SPLIT = /([a-z0-9])([A-Z])|([A-Z]+)([A-Z][a-z])/g;
@@ -24270,11 +24402,12 @@ ${end}
24270
24402
 
24271
24403
  // ../core/src/devAdmin.ts
24272
24404
  import { cpus as osCpus } from "node:os";
24273
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync12, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync15, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
24274
- import { join as join22, dirname as dirname10, resolve as resolve12, relative as relative9, sep as sep6, basename as basename5 } from "node:path";
24275
- import { fileURLToPath as fileURLToPath5 } from "node:url";
24405
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync13, mkdirSync as mkdirSync14, copyFileSync as copyFileSync2, statSync as statSync15, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
24406
+ import { join as join22, dirname as dirname11, resolve as resolve12, relative as relative9, sep as sep6, basename as basename5 } from "node:path";
24407
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
24276
24408
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
24277
- function escapeHtml(value) {
24409
+ import net2 from "node:net";
24410
+ function escapeHtml2(value) {
24278
24411
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
24279
24412
  }
24280
24413
  function devSameOriginOk(req2) {
@@ -24312,6 +24445,42 @@ function devHostAllowed(hostHeader) {
24312
24445
  if (configured) allowed.add(configured);
24313
24446
  return allowed.has(name);
24314
24447
  }
24448
+ function ipInCidr(ip, entry) {
24449
+ try {
24450
+ ip = (ip ?? "").trim();
24451
+ if (ip.toLowerCase().startsWith("::ffff:")) ip = ip.slice(7);
24452
+ const ipFam = net2.isIP(ip);
24453
+ if (!ipFam) return false;
24454
+ entry = (entry ?? "").trim();
24455
+ const bl = new net2.BlockList();
24456
+ if (entry.includes("/")) {
24457
+ const [addr, bitsStr] = entry.split("/");
24458
+ const bits = Number(bitsStr);
24459
+ const fam = net2.isIP(addr);
24460
+ if (!fam || !Number.isInteger(bits)) return false;
24461
+ bl.addSubnet(addr, bits, fam === 4 ? "ipv4" : "ipv6");
24462
+ } else {
24463
+ const fam = net2.isIP(entry);
24464
+ if (!fam) return false;
24465
+ bl.addAddress(entry, fam === 4 ? "ipv4" : "ipv6");
24466
+ }
24467
+ return bl.check(ip, ipFam === 4 ? "ipv4" : "ipv6");
24468
+ } catch {
24469
+ return false;
24470
+ }
24471
+ }
24472
+ function devPeerAllowed(remoteIp) {
24473
+ const ip = (remoteIp ?? "").trim();
24474
+ if (isLoopback(ip)) return true;
24475
+ const configured = (process.env.TINA4_DEV_ALLOWED_PEERS ?? "").trim();
24476
+ if (!configured) return false;
24477
+ return configured.split(",").some((entry) => entry.trim() !== "" && ipInCidr(ip, entry));
24478
+ }
24479
+ function devToolbarAllowed(req2) {
24480
+ if (!devHostAllowed(req2.header("host"))) return false;
24481
+ const peer = req2.socket?.remoteAddress ?? "";
24482
+ return devPeerAllowed(peer);
24483
+ }
24315
24484
  function devRequestDenial(req2) {
24316
24485
  const path10 = (req2.path ?? "").replace(/\/+/g, "/");
24317
24486
  const isMcp = DEV_MCP_PREFIXES.some((prefix) => path10 === prefix || path10.startsWith(prefix + "/"));
@@ -24321,9 +24490,12 @@ function devRequestDenial(req2) {
24321
24490
  if (!devSameOriginOk(req2)) {
24322
24491
  return { status: 403, error: "dev-admin: refused (cross-origin request)" };
24323
24492
  }
24493
+ if (DEV_PUBLIC_ASSETS.has(path10)) {
24494
+ return null;
24495
+ }
24324
24496
  if (!isMcp) {
24325
24497
  const peer = req2.socket?.remoteAddress ?? "";
24326
- if (!(isLoopback(peer) || mcpTokenOk(req2, true))) {
24498
+ if (!(devPeerAllowed(peer) || mcpTokenOk(req2, true))) {
24327
24499
  return { status: 403, error: "dev-admin: refused (non-loopback peer)" };
24328
24500
  }
24329
24501
  }
@@ -24412,7 +24584,7 @@ function mapQueueJob(job, topic, status2) {
24412
24584
  function readQueueDir(dir, topic, status2) {
24413
24585
  if (!existsSync20(dir)) return [];
24414
24586
  const jobs = [];
24415
- for (const filename of readdirSync12(dir).sort()) {
24587
+ for (const filename of readdirSync13(dir).sort()) {
24416
24588
  if (!filename.endsWith(".queue-data")) continue;
24417
24589
  try {
24418
24590
  jobs.push(mapQueueJob(JSON.parse(readFileSync18(join22(dir, filename), "utf-8")), topic, status2));
@@ -24554,12 +24726,12 @@ function upsertDevEnvVar(key, value) {
24554
24726
  }
24555
24727
  function formatUptime(seconds) {
24556
24728
  const d = Math.floor(seconds / 86400);
24557
- const h = Math.floor(seconds % 86400 / 3600);
24729
+ const h2 = Math.floor(seconds % 86400 / 3600);
24558
24730
  const m = Math.floor(seconds % 3600 / 60);
24559
24731
  const s = Math.floor(seconds % 60);
24560
24732
  const parts = [];
24561
24733
  if (d > 0) parts.push(`${d}d`);
24562
- if (h > 0) parts.push(`${h}h`);
24734
+ if (h2 > 0) parts.push(`${h2}h`);
24563
24735
  if (m > 0) parts.push(`${m}m`);
24564
24736
  parts.push(`${s}s`);
24565
24737
  return parts.join(" ");
@@ -24580,7 +24752,7 @@ function parseEnvFile() {
24580
24752
  function walkDirRecursive(dir) {
24581
24753
  const results = [];
24582
24754
  if (!existsSync20(dir)) return results;
24583
- for (const entry of readdirSync12(dir)) {
24755
+ for (const entry of readdirSync13(dir)) {
24584
24756
  const full = join22(dir, entry);
24585
24757
  if (statSync15(full).isDirectory()) {
24586
24758
  results.push(...walkDirRecursive(full));
@@ -24610,8 +24782,8 @@ function handleGalleryDeploy(router) {
24610
24782
  for (const srcFile of allFiles) {
24611
24783
  const rel = relative9(gallerySrc, srcFile);
24612
24784
  const dest = join22(projectSrc, rel);
24613
- mkdirSync14(dirname10(dest), { recursive: true });
24614
- copyFileSync(srcFile, dest);
24785
+ mkdirSync14(dirname11(dest), { recursive: true });
24786
+ copyFileSync2(srcFile, dest);
24615
24787
  copied.push(rel);
24616
24788
  }
24617
24789
  try {
@@ -24641,9 +24813,9 @@ function realPathOf(target) {
24641
24813
  present = true;
24642
24814
  } catch {
24643
24815
  }
24644
- if (present || dirname10(existing) === existing) break;
24816
+ if (present || dirname11(existing) === existing) break;
24645
24817
  tail.unshift(basename5(existing));
24646
- existing = dirname10(existing);
24818
+ existing = dirname11(existing);
24647
24819
  }
24648
24820
  return join22(realpathSync6(existing), ...tail);
24649
24821
  }
@@ -24767,9 +24939,9 @@ function applyMcpOutcome(res, outcome) {
24767
24939
  }
24768
24940
  function renderToolbarHtml(ctx) {
24769
24941
  const nodeVersion = process.version;
24770
- const method = escapeHtml(ctx.method);
24771
- const path10 = escapeHtml(ctx.path);
24772
- const matchedPattern = escapeHtml(ctx.matchedPattern);
24942
+ const method = escapeHtml2(ctx.method);
24943
+ const path10 = escapeHtml2(ctx.path);
24944
+ const matchedPattern = escapeHtml2(ctx.matchedPattern);
24773
24945
  const reload = ctx.reload === false ? "0" : "1";
24774
24946
  return `<link rel="stylesheet" href="/__dev/toolbar.css">
24775
24947
  <div id="tina4-dev-toolbar" data-reload="${reload}">
@@ -24955,7 +25127,7 @@ function toolbarJs() {
24955
25127
  connect();
24956
25128
  })();`;
24957
25129
  }
24958
- var cpuCount, 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;
25130
+ var cpuCount, DEV_MCP_PREFIXES, DEV_PUBLIC_ASSETS, 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, handleDevAdminSplit, handleToolbarCss, handleToolbarJs;
24959
25131
  var init_devAdmin = __esm({
24960
25132
  "../core/src/devAdmin.ts"() {
24961
25133
  "use strict";
@@ -24968,6 +25140,7 @@ var init_devAdmin = __esm({
24968
25140
  init_version();
24969
25141
  cpuCount = osCpus().length;
24970
25142
  DEV_MCP_PREFIXES = ["/__dev/api/mcp", "/__dev/mcp"];
25143
+ DEV_PUBLIC_ASSETS = /* @__PURE__ */ new Set(["/__dev/toolbar.css", "/__dev/toolbar.js"]);
24971
25144
  DEV_SECRET_BASENAMES = /* @__PURE__ */ new Set([
24972
25145
  ".env",
24973
25146
  ".envrc",
@@ -25424,6 +25597,7 @@ var init_devAdmin = __esm({
25424
25597
  { method: "GET", pattern: "/__dev/api/docs/.well-known.json", handler: handleDocsWellKnown },
25425
25598
  // JS asset
25426
25599
  { method: "GET", pattern: "/__dev/js/tina4-dev-admin.min.js", handler: handleDevAdminJs },
25600
+ { method: "GET", pattern: "/__dev/split", handler: handleDevAdminSplit },
25427
25601
  // Dev-toolbar assets — served as external CSS/JS so the injected toolbar is
25428
25602
  // CSP-clean (no inline style=, onclick=, or <script>) under a strict
25429
25603
  // `default-src 'self'`. Parity with PHP's /__dev/toolbar.css + toolbar.js.
@@ -25669,7 +25843,7 @@ var init_devAdmin = __esm({
25669
25843
  const queueDir = queueBasePath2();
25670
25844
  let topics = [];
25671
25845
  if (existsSync20(queueDir)) {
25672
- topics = readdirSync12(queueDir).filter((d) => {
25846
+ topics = readdirSync13(queueDir).filter((d) => {
25673
25847
  try {
25674
25848
  return statSync15(join22(queueDir, d)).isDirectory();
25675
25849
  } catch {
@@ -26173,13 +26347,13 @@ var init_devAdmin = __esm({
26173
26347
  res.json({ success: false, error: msg });
26174
26348
  }
26175
26349
  };
26176
- __devAdminFilename = fileURLToPath5(import.meta.url);
26177
- __devAdminDirname = dirname10(__devAdminFilename);
26350
+ __devAdminFilename = fileURLToPath6(import.meta.url);
26351
+ __devAdminDirname = dirname11(__devAdminFilename);
26178
26352
  handleGalleryList = (_req, res) => {
26179
26353
  const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
26180
26354
  const items = [];
26181
26355
  if (existsSync20(galleryDir)) {
26182
- const entries = readdirSync12(galleryDir).sort();
26356
+ const entries = readdirSync13(galleryDir).sort();
26183
26357
  for (const entry of entries) {
26184
26358
  const entryPath = join22(galleryDir, entry);
26185
26359
  const metaFile = join22(entryPath, "meta.json");
@@ -26283,7 +26457,7 @@ var init_devAdmin = __esm({
26283
26457
  if (cwdInGit !== "") cwdInGit += "/";
26284
26458
  }
26285
26459
  const entries = [];
26286
- for (const name of readdirSync12(target).sort()) {
26460
+ for (const name of readdirSync13(target).sort()) {
26287
26461
  if (devFilesHidden(name)) continue;
26288
26462
  const full = join22(target, name);
26289
26463
  const entryRel = relative9(root, full).replace(/\\/g, "/");
@@ -26314,7 +26488,7 @@ var init_devAdmin = __esm({
26314
26488
  if (isDir) {
26315
26489
  hasChildren = false;
26316
26490
  try {
26317
- for (const c of readdirSync12(full)) {
26491
+ for (const c of readdirSync13(full)) {
26318
26492
  if (devFilesHidden(c)) continue;
26319
26493
  hasChildren = true;
26320
26494
  break;
@@ -26406,7 +26580,7 @@ var init_devAdmin = __esm({
26406
26580
  return;
26407
26581
  }
26408
26582
  try {
26409
- mkdirSync14(dirname10(target), { recursive: true });
26583
+ mkdirSync14(dirname11(target), { recursive: true });
26410
26584
  const existed = existsSync20(target);
26411
26585
  writeFileSync13(target, content, "utf-8");
26412
26586
  try {
@@ -26478,7 +26652,7 @@ var init_devAdmin = __esm({
26478
26652
  }
26479
26653
  try {
26480
26654
  const { renameSync: renameSync3 } = await import("node:fs");
26481
- mkdirSync14(dirname10(dst), { recursive: true });
26655
+ mkdirSync14(dirname11(dst), { recursive: true });
26482
26656
  renameSync3(src, dst);
26483
26657
  res.json({ ok: true, from: relative9(root, src), to: relative9(root, dst) });
26484
26658
  } catch (e) {
@@ -26499,8 +26673,8 @@ var init_devAdmin = __esm({
26499
26673
  return;
26500
26674
  }
26501
26675
  try {
26502
- const { rmSync } = await import("node:fs");
26503
- rmSync(target, { recursive: true, force: true });
26676
+ const { rmSync: rmSync2 } = await import("node:fs");
26677
+ rmSync2(target, { recursive: true, force: true });
26504
26678
  res.json({ ok: true, deleted: relative9(root, target) });
26505
26679
  } catch (e) {
26506
26680
  res.json({ error: e.message }, 500);
@@ -26835,10 +27009,10 @@ var init_devAdmin = __esm({
26835
27009
  });
26836
27010
  };
26837
27011
  handleDevAdminJs = async (_req, res) => {
26838
- const { readFileSync: readFileSync27, existsSync: existsSync28 } = await import("node:fs");
26839
- const { dirname: dirname15, join: join33, resolve: resolve21 } = await import("node:path");
26840
- const { fileURLToPath: fileURLToPath8 } = await import("node:url");
26841
- const dir = dirname15(fileURLToPath8(import.meta.url));
27012
+ const { readFileSync: readFileSync27, existsSync: existsSync29 } = await import("node:fs");
27013
+ const { dirname: dirname17, join: join33, resolve: resolve21 } = await import("node:path");
27014
+ const { fileURLToPath: fileURLToPath10 } = await import("node:url");
27015
+ const dir = dirname17(fileURLToPath10(import.meta.url));
26842
27016
  const candidates = [
26843
27017
  join33(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
26844
27018
  // src/../public/js/
@@ -26849,7 +27023,7 @@ var init_devAdmin = __esm({
26849
27023
  // project public/
26850
27024
  ];
26851
27025
  for (const jsPath of candidates) {
26852
- if (existsSync28(jsPath)) {
27026
+ if (existsSync29(jsPath)) {
26853
27027
  try {
26854
27028
  const content = readFileSync27(jsPath, "utf-8");
26855
27029
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
@@ -26862,6 +27036,31 @@ var init_devAdmin = __esm({
26862
27036
  res.raw.writeHead(404, { "Content-Type": "text/plain" });
26863
27037
  res.raw.end("tina4-dev-admin.min.js not found");
26864
27038
  };
27039
+ handleDevAdminSplit = async (_req, res) => {
27040
+ const { readFileSync: readFileSync27, existsSync: existsSync29 } = await import("node:fs");
27041
+ const { dirname: dirname17, join: join33, resolve: resolve21 } = await import("node:path");
27042
+ const { fileURLToPath: fileURLToPath10 } = await import("node:url");
27043
+ const dir = dirname17(fileURLToPath10(import.meta.url));
27044
+ const candidates = [
27045
+ join33(dir, "..", "public", "js", "tina4-dev-admin-split.html"),
27046
+ join33(dir, "..", "..", "public", "js", "tina4-dev-admin-split.html"),
27047
+ resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin-split.html"),
27048
+ resolve21(process.cwd(), "public", "js", "tina4-dev-admin-split.html")
27049
+ ];
27050
+ for (const path10 of candidates) {
27051
+ if (existsSync29(path10)) {
27052
+ try {
27053
+ const content = readFileSync27(path10, "utf-8");
27054
+ res.raw.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
27055
+ res.raw.end(content);
27056
+ return;
27057
+ } catch {
27058
+ }
27059
+ }
27060
+ }
27061
+ res.raw.writeHead(404, { "Content-Type": "text/plain" });
27062
+ res.raw.end("tina4-dev-admin-split.html not found");
27063
+ };
26865
27064
  handleToolbarCss = (_req, res) => {
26866
27065
  res.raw.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" });
26867
27066
  res.raw.end(toolbarCss());
@@ -26874,7 +27073,7 @@ var init_devAdmin = __esm({
26874
27073
  });
26875
27074
 
26876
27075
  // ../core/src/i18n.ts
26877
- import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync21 } from "node:fs";
27076
+ import { readFileSync as readFileSync19, readdirSync as readdirSync14, existsSync as existsSync21 } from "node:fs";
26878
27077
  import { join as join23, resolve as resolve13 } from "node:path";
26879
27078
  var I18n;
26880
27079
  var init_i18n = __esm({
@@ -26956,7 +27155,7 @@ var init_i18n = __esm({
26956
27155
  return [this._defaultLocale];
26957
27156
  }
26958
27157
  try {
26959
- const files = readdirSync13(this._localeDir);
27158
+ const files = readdirSync14(this._localeDir);
26960
27159
  const locales = files.filter((f) => f.endsWith(".json") || f.endsWith(".yml") || f.endsWith(".yaml")).map((f) => f.replace(/\.(json|ya?ml)$/, "")).sort();
26961
27160
  return locales.length > 0 ? locales : [this._defaultLocale];
26962
27161
  } catch {
@@ -28212,10 +28411,10 @@ var init_docsAutoDiscovery = __esm({
28212
28411
 
28213
28412
  // ../core/src/server.ts
28214
28413
  import { randomBytes as randomBytes7 } from "node:crypto";
28215
- import { resolve as resolve15, dirname as dirname11, join as join25, relative as relative10 } from "node:path";
28216
- import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync16 } from "node:fs";
28414
+ import { resolve as resolve15, dirname as dirname12, join as join25, relative as relative10 } from "node:path";
28415
+ import { existsSync as existsSync23, readdirSync as readdirSync15, readFileSync as readFileSync21, statSync as statSync16 } from "node:fs";
28217
28416
  import { isatty } from "node:tty";
28218
- import { fileURLToPath as fileURLToPath6 } from "node:url";
28417
+ import { fileURLToPath as fileURLToPath7 } from "node:url";
28219
28418
  import { execFileSync as execFileSync3, exec } from "node:child_process";
28220
28419
  import cluster from "node:cluster";
28221
28420
  import os2 from "node:os";
@@ -28255,7 +28454,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
28255
28454
  if (!existsSync23(dir)) return;
28256
28455
  let hasSql = false;
28257
28456
  try {
28258
- hasSql = readdirSync14(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
28457
+ hasSql = readdirSync15(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
28259
28458
  } catch {
28260
28459
  return;
28261
28460
  }
@@ -28425,7 +28624,7 @@ function injectDevToolbar(html, ctx) {
28425
28624
  function walkGalleryFiles(dir) {
28426
28625
  const results = [];
28427
28626
  if (!existsSync23(dir)) return results;
28428
- for (const f of readdirSync14(dir)) {
28627
+ for (const f of readdirSync15(dir)) {
28429
28628
  const full = join25(dir, f);
28430
28629
  if (statSync16(full).isDirectory()) results.push(...walkGalleryFiles(full));
28431
28630
  else results.push(full);
@@ -28437,7 +28636,7 @@ function getGalleryDeployedState() {
28437
28636
  const state = {};
28438
28637
  if (!existsSync23(galleryDir)) return state;
28439
28638
  try {
28440
- const entries = readdirSync14(galleryDir).sort();
28639
+ const entries = readdirSync15(galleryDir).sort();
28441
28640
  for (const entry of entries) {
28442
28641
  const entryPath = join25(galleryDir, entry);
28443
28642
  const metaFile = join25(entryPath, "meta.json");
@@ -28488,7 +28687,7 @@ function resolveTemplate(pathname, templatesDir) {
28488
28687
  const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
28489
28688
  if (existsSync23(pagesDir)) {
28490
28689
  const scan = (dir, prefix) => {
28491
- for (const entry of readdirSync14(dir, { withFileTypes: true })) {
28690
+ for (const entry of readdirSync15(dir, { withFileTypes: true })) {
28492
28691
  if (entry.name.startsWith("_")) continue;
28493
28692
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
28494
28693
  if (entry.isDirectory()) {
@@ -28807,7 +29006,7 @@ function isInjectableHtml(res) {
28807
29006
  return isHtmlResponse(res) && !res.raw.getHeader("content-encoding");
28808
29007
  }
28809
29008
  function injectIntoHtml(ctx, devToolbar, html) {
28810
- if (!devToolbar) return injectFeedbackWidget(ctx.req, html);
29009
+ if (!devToolbar || !devToolbarAllowed(ctx.req)) return injectFeedbackWidget(ctx.req, html);
28811
29010
  const toolbarCtx = {
28812
29011
  version: TINA4_VERSION,
28813
29012
  method: ctx.req.method ?? "GET",
@@ -29175,7 +29374,7 @@ function wireLocaleGlobal(frondEngine, base) {
29175
29374
  const localeDir = resolve15(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
29176
29375
  if (!existsSync23(localeDir)) return;
29177
29376
  try {
29178
- const localeFiles = readdirSync14(localeDir).filter((file) => file.endsWith(".json"));
29377
+ const localeFiles = readdirSync15(localeDir).filter((file) => file.endsWith(".json"));
29179
29378
  if (localeFiles.length === 0 || frondEngine.globals?.t) return;
29180
29379
  const i18nInstance = new I18n(process.env.TINA4_LOCALE ?? "en", localeDir);
29181
29380
  frondEngine.addGlobal("t", (key, params) => i18nInstance.t(key, params));
@@ -29590,8 +29789,8 @@ var init_server = __esm({
29590
29789
  init_i18n();
29591
29790
  init_background();
29592
29791
  init_version();
29593
- __filename = fileURLToPath6(import.meta.url);
29594
- __dirname = dirname11(__filename);
29792
+ __filename = fileURLToPath7(import.meta.url);
29793
+ __dirname = dirname12(__filename);
29595
29794
  BUILTIN_ERROR_TEMPLATES_DIR = resolve15(__dirname, "..", "templates");
29596
29795
  BUILTIN_PUBLIC_DIR = resolve15(__dirname, "..", "public");
29597
29796
  swaggerAssetsEnabled = false;
@@ -29753,7 +29952,7 @@ var init_env = __esm({
29753
29952
 
29754
29953
  // ../core/src/fakeData.ts
29755
29954
  import { randomInt, randomUUID as randomUUID6 } from "node:crypto";
29756
- import { existsSync as existsSync24, readdirSync as readdirSync15 } from "node:fs";
29955
+ import { existsSync as existsSync24, readdirSync as readdirSync16 } from "node:fs";
29757
29956
  import { resolve as resolve16, join as join26 } from "node:path";
29758
29957
  function mulberry32(seed) {
29759
29958
  let s = seed | 0;
@@ -30253,7 +30452,7 @@ var init_fakeData = __esm({
30253
30452
  async seedDir(seedDir) {
30254
30453
  const dir = resolve16(seedDir ?? "src/seeds");
30255
30454
  if (!existsSync24(dir)) return [];
30256
- const files = readdirSync15(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
30455
+ const files = readdirSync16(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
30257
30456
  const executed = [];
30258
30457
  for (const file of files) {
30259
30458
  const fullPath = join26(dir, file);
@@ -30359,7 +30558,7 @@ var init_mqttMessage = __esm({
30359
30558
  });
30360
30559
 
30361
30560
  // ../core/src/mqtt.ts
30362
- import net2 from "node:net";
30561
+ import net3 from "node:net";
30363
30562
  import tls from "node:tls";
30364
30563
  import { randomBytes as randomBytes8 } from "node:crypto";
30365
30564
  import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
@@ -30830,7 +31029,7 @@ var init_mqtt = __esm({
30830
31029
  if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
30831
31030
  sock = tls.connect(opts, () => settle(() => resolve21(sock)));
30832
31031
  } else {
30833
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
31032
+ sock = net3.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
30834
31033
  }
30835
31034
  sock.once("error", (err) => {
30836
31035
  settle(() => {
@@ -31047,7 +31246,7 @@ var init_mqtt = __esm({
31047
31246
  });
31048
31247
 
31049
31248
  // ../core/src/service.ts
31050
- import { readdirSync as readdirSync16, statSync as statSync17, watchFile, unwatchFile } from "node:fs";
31249
+ import { readdirSync as readdirSync17, statSync as statSync17, watchFile, unwatchFile } from "node:fs";
31051
31250
  import { join as join27, extname as extname7 } from "node:path";
31052
31251
  import { pathToFileURL } from "node:url";
31053
31252
  function matchCronField(field, value) {
@@ -31215,7 +31414,7 @@ var init_service = __esm({
31215
31414
  const discovered = [];
31216
31415
  let entries;
31217
31416
  try {
31218
- entries = readdirSync16(dir);
31417
+ entries = readdirSync17(dir);
31219
31418
  } catch {
31220
31419
  return discovered;
31221
31420
  }
@@ -31342,7 +31541,7 @@ var init_service = __esm({
31342
31541
  const dir = serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
31343
31542
  let entries;
31344
31543
  try {
31345
- entries = readdirSync16(dir);
31544
+ entries = readdirSync17(dir);
31346
31545
  } catch {
31347
31546
  return;
31348
31547
  }
@@ -31449,9 +31648,9 @@ function ipToBytes(ip) {
31449
31648
  }
31450
31649
  function parseCidr(cidr) {
31451
31650
  const [netStr, bitsStr] = cidr.split("/");
31452
- const net4 = ipToBytes(netStr);
31453
- if (!net4) return null;
31454
- return { net: net4, bits: Number(bitsStr) };
31651
+ const net5 = ipToBytes(netStr);
31652
+ if (!net5) return null;
31653
+ return { net: net5, bits: Number(bitsStr) };
31455
31654
  }
31456
31655
  function inCidr(packed, cidr) {
31457
31656
  if (packed.length !== cidr.net.length) return false;
@@ -32030,11 +32229,11 @@ var init_api = __esm({
32030
32229
  path: null
32031
32230
  };
32032
32231
  }
32033
- const net4 = await this.performRequest("GET", url, headers, data, MAX_REDIRECTS);
32034
- if (net4.kind === "error") {
32035
- return { http_code: null, headers: {}, error: net4.error, path: null };
32232
+ const net5 = await this.performRequest("GET", url, headers, data, MAX_REDIRECTS);
32233
+ if (net5.kind === "error") {
32234
+ return { http_code: null, headers: {}, error: net5.error, path: null };
32036
32235
  }
32037
- const res = net4.res;
32236
+ const res = net5.res;
32038
32237
  const code = res.statusCode ?? null;
32039
32238
  const respHeaders = flattenHeaders(res.headers);
32040
32239
  if (code === null || code >= 400) {
@@ -32292,11 +32491,11 @@ var init_api = __esm({
32292
32491
  if (this.transportFn) {
32293
32492
  return this.callTransport(method, url, headers, data);
32294
32493
  }
32295
- const net4 = await this.performRequest(method, url, headers, data, MAX_REDIRECTS);
32296
- if (net4.kind === "error") {
32297
- return { http_code: null, body: null, headers: {}, error: net4.error };
32494
+ const net5 = await this.performRequest(method, url, headers, data, MAX_REDIRECTS);
32495
+ if (net5.kind === "error") {
32496
+ return { http_code: null, body: null, headers: {}, error: net5.error };
32298
32497
  }
32299
- return this.readResponse(net4.res);
32498
+ return this.readResponse(net5.res);
32300
32499
  }
32301
32500
  /**
32302
32501
  * Invoke a user-injected transport and normalize its result. The transport
@@ -32473,7 +32672,7 @@ var init_api = __esm({
32473
32672
  });
32474
32673
 
32475
32674
  // ../core/src/messenger.ts
32476
- import net3 from "node:net";
32675
+ import net4 from "node:net";
32477
32676
  import tls2 from "node:tls";
32478
32677
  import { readFileSync as readFileSync23 } from "node:fs";
32479
32678
  import { basename as basename7 } from "node:path";
@@ -32485,6 +32684,32 @@ function normaliseEncryption(value, kind) {
32485
32684
  }
32486
32685
  return normalised;
32487
32686
  }
32687
+ function resolveSendTimeoutMs(optionTimeout) {
32688
+ if (optionTimeout === void 0 || optionTimeout === null) {
32689
+ return resolveEnvTimeoutSeconds(process.env.TINA4_MAIL_TIMEOUT) * 1e3;
32690
+ }
32691
+ if (!Number.isInteger(optionTimeout) || optionTimeout < 1) {
32692
+ throw new Error(
32693
+ `Messenger timeout must be a whole number of seconds and at least 1, got ${optionTimeout}.`
32694
+ );
32695
+ }
32696
+ return optionTimeout * 1e3;
32697
+ }
32698
+ function resolveEnvTimeoutSeconds(raw) {
32699
+ if (!raw || raw.trim() === "") return DEFAULT_TIMEOUT_SECONDS;
32700
+ const trimmed = raw.trim();
32701
+ const seconds = /^\d+$/.test(trimmed) ? Number(trimmed) : NaN;
32702
+ if (Number.isInteger(seconds) && seconds >= 1) return seconds;
32703
+ warnBadMailTimeout(raw);
32704
+ return DEFAULT_TIMEOUT_SECONDS;
32705
+ }
32706
+ function warnBadMailTimeout(raw) {
32707
+ if (_timeoutWarned.has(raw)) return;
32708
+ _timeoutWarned.add(raw);
32709
+ Log.warning(
32710
+ `TINA4_MAIL_TIMEOUT must be a whole number of seconds and at least 1, got ${JSON.stringify(raw)} - using the default of ${DEFAULT_TIMEOUT_SECONDS} seconds`
32711
+ );
32712
+ }
32488
32713
  function failOnIdle(socket, timeoutMs, what) {
32489
32714
  socket.setTimeout(timeoutMs, () => {
32490
32715
  socket.destroy(new Error(`${what} timed out after ${timeoutMs / 1e3}s`));
@@ -32496,7 +32721,7 @@ function connectVerifiedTls(host, port, socket) {
32496
32721
  host,
32497
32722
  port,
32498
32723
  socket,
32499
- servername: net3.isIP(host) ? void 0 : host,
32724
+ servername: net4.isIP(host) ? void 0 : host,
32500
32725
  rejectUnauthorized: true
32501
32726
  });
32502
32727
  return new Promise((resolve21, reject) => {
@@ -32508,7 +32733,7 @@ function connectVerifiedTls(host, port, socket) {
32508
32733
  });
32509
32734
  }
32510
32735
  function connectPlain(host, port) {
32511
- const socket = net3.createConnection({ host, port });
32736
+ const socket = net4.createConnection({ host, port });
32512
32737
  return new Promise((resolve21, reject) => {
32513
32738
  socket.once("connect", () => {
32514
32739
  socket.removeListener("error", reject);
@@ -32746,7 +32971,7 @@ function decodeTransfer(body, encoding) {
32746
32971
  }
32747
32972
  }
32748
32973
  if (enc === "quoted-printable") {
32749
- return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
32974
+ return body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_m, h2) => String.fromCharCode(parseInt(h2, 16)));
32750
32975
  }
32751
32976
  return body;
32752
32977
  }
@@ -32863,7 +33088,7 @@ function parseFullMessage(uid, response) {
32863
33088
  function createMessenger() {
32864
33089
  return new Messenger();
32865
33090
  }
32866
- var MAIL_ENCRYPTION_VALUES, SMTP_SEND_TIMEOUT_MS, SMTP_CHECK_TIMEOUT_MS, MessengerConnectionError, SmtpCommandError, Messenger, imapTagCounter;
33091
+ var MAIL_ENCRYPTION_VALUES, SMTP_SEND_TIMEOUT_MS, SMTP_CHECK_TIMEOUT_MS, _timeoutWarned, DEFAULT_TIMEOUT_SECONDS, MessengerConnectionError, SmtpCommandError, Messenger, imapTagCounter;
32867
33092
  var init_messenger = __esm({
32868
33093
  "../core/src/messenger.ts"() {
32869
33094
  "use strict";
@@ -32873,6 +33098,8 @@ var init_messenger = __esm({
32873
33098
  MAIL_ENCRYPTION_VALUES = ["ssl", "tls", "starttls", "none"];
32874
33099
  SMTP_SEND_TIMEOUT_MS = 3e4;
32875
33100
  SMTP_CHECK_TIMEOUT_MS = 1e4;
33101
+ _timeoutWarned = /* @__PURE__ */ new Set();
33102
+ DEFAULT_TIMEOUT_SECONDS = 30;
32876
33103
  MessengerConnectionError = class extends Error {
32877
33104
  constructor(message) {
32878
33105
  super(message);
@@ -32902,6 +33129,8 @@ var init_messenger = __esm({
32902
33129
  imapUser;
32903
33130
  imapPass;
32904
33131
  imapEncryption;
33132
+ /** Resolved SMTP send timeout, in milliseconds (see the constructor). */
33133
+ timeoutMs;
32905
33134
  constructor(options) {
32906
33135
  this.smtpConfigured = Boolean(options?.host ?? process.env.TINA4_MAIL_HOST);
32907
33136
  this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
@@ -32923,6 +33152,7 @@ var init_messenger = __esm({
32923
33152
  this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
32924
33153
  this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
32925
33154
  this.imapEncryption = normaliseEncryption(options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls", "IMAP");
33155
+ this.timeoutMs = resolveSendTimeoutMs(options?.timeout);
32926
33156
  }
32927
33157
  /**
32928
33158
  * How the SMTP connection is secured (ADR-0071 section 1). Port 465 is the
@@ -33058,7 +33288,7 @@ var init_messenger = __esm({
33058
33288
  async sendSmtp(options, recipients, messageId) {
33059
33289
  let socket = null;
33060
33290
  try {
33061
- socket = await this.openSmtpSession();
33291
+ socket = await this.openSmtpSession(this.timeoutMs);
33062
33292
  await this.authenticateSmtp(socket);
33063
33293
  await this.sendSmtpEnvelope(socket, recipients.allRecipients);
33064
33294
  await this.sendSmtpMessage(socket, options, recipients, messageId);
@@ -34107,13 +34337,13 @@ var init_htmlElement = __esm({
34107
34337
  // ../core/src/ai.ts
34108
34338
  import { existsSync as existsSync26, mkdirSync as mkdirSync17, writeFileSync as writeFileSync16, readFileSync as readFileSync24 } from "node:fs";
34109
34339
  import { homedir } from "node:os";
34110
- import { join as join28, resolve as resolve17, relative as relative11, dirname as dirname12 } from "node:path";
34111
- import { fileURLToPath as fileURLToPath7 } from "node:url";
34340
+ import { join as join28, resolve as resolve17, relative as relative11, dirname as dirname13 } from "node:path";
34341
+ import { fileURLToPath as fileURLToPath8 } from "node:url";
34112
34342
  import { execSync, execFileSync as execFileSync4 } from "node:child_process";
34113
34343
  import { createInterface } from "node:readline";
34114
34344
  function readVersion() {
34115
34345
  try {
34116
- const thisDir = dirname12(fileURLToPath7(import.meta.url));
34346
+ const thisDir = dirname13(fileURLToPath8(import.meta.url));
34117
34347
  const rootPkg = resolve17(thisDir, "..", "..", "..", "package.json");
34118
34348
  const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
34119
34349
  return pkg.version ?? "0.0.0";
@@ -34329,7 +34559,7 @@ function replaceMarkerBlock(existing, block, start2, end) {
34329
34559
  }
34330
34560
  function looksLikeOldFrameworkInstall(existing) {
34331
34561
  const head = existing.replace(/^\s+/, "").slice(0, 400);
34332
- return OLD_FRAMEWORK_HEADERS.some((h) => head.startsWith(h));
34562
+ return OLD_FRAMEWORK_HEADERS.some((h2) => head.startsWith(h2));
34333
34563
  }
34334
34564
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
34335
34565
  const block = skillBlock(contextFile);
@@ -34359,7 +34589,7 @@ function installForTool(root, tool, context) {
34359
34589
  if (tool.configDir) {
34360
34590
  mkdirSync17(join28(root, tool.configDir), { recursive: true });
34361
34591
  }
34362
- const parentDir = dirname12(contextPath);
34592
+ const parentDir = dirname13(contextPath);
34363
34593
  mkdirSync17(parentDir, { recursive: true });
34364
34594
  const action = writeOrMerge(contextPath, tool.contextFile, context);
34365
34595
  const rel = relative11(root, contextPath);
@@ -34736,7 +34966,7 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
34736
34966
  }
34737
34967
  function generateClaudeCodeContext() {
34738
34968
  try {
34739
- const thisDir = dirname12(fileURLToPath7(import.meta.url));
34969
+ const thisDir = dirname13(fileURLToPath8(import.meta.url));
34740
34970
  const repoRoot = resolve17(thisDir, "..", "..", "..");
34741
34971
  const claudeMdPath = join28(repoRoot, "CLAUDE.md");
34742
34972
  if (existsSync26(claudeMdPath)) {
@@ -35584,8 +35814,8 @@ var init_aiClient = __esm({
35584
35814
  static retryDelay(headers, deadline) {
35585
35815
  const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
35586
35816
  const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
35587
- const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
35588
- return new Promise((resolve21) => setTimeout(resolve21, delay));
35817
+ const delay2 = Math.min(requested, Math.max(0, deadline - performance.now()));
35818
+ return new Promise((resolve21) => setTimeout(resolve21, delay2));
35589
35819
  }
35590
35820
  static async requestJson(config, headers, body) {
35591
35821
  const deadline = performance.now() + config.totalTimeout * 1e3;
@@ -38640,7 +38870,7 @@ __export(sqlite_exports, {
38640
38870
  });
38641
38871
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
38642
38872
  import { mkdirSync as mkdirSync18 } from "node:fs";
38643
- import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29, resolve as resolve18, sep as sep7 } from "node:path";
38873
+ import { dirname as dirname14, isAbsolute as isAbsolute5, join as join29, resolve as resolve18, sep as sep7 } from "node:path";
38644
38874
  function isIdentifier(str) {
38645
38875
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
38646
38876
  }
@@ -38677,7 +38907,7 @@ function resolveSqlitePath(dbPath) {
38677
38907
  }
38678
38908
  const cwd = resolve18(process.cwd());
38679
38909
  const path10 = join29(cwd, dbPath);
38680
- const parent = dirname13(path10);
38910
+ const parent = dirname14(path10);
38681
38911
  const absParent = resolve18(parent);
38682
38912
  if (absParent !== cwd && !absParent.startsWith(cwd + sep7)) {
38683
38913
  throw new Error(
@@ -41106,9 +41336,75 @@ __export(mongodb_exports, {
41106
41336
  function isMatchAllWhere(where) {
41107
41337
  return where != null && MATCH_ALL_WHERE.test(where);
41108
41338
  }
41339
+ function isSqlWs(ch) {
41340
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
41341
+ }
41342
+ function isWordChar(ch) {
41343
+ return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_";
41344
+ }
41345
+ function onlyWs(sql, start2, end) {
41346
+ for (let i = start2; i < end; i++) if (!isSqlWs(sql[i])) return false;
41347
+ return true;
41348
+ }
41349
+ function keywordAt(upper, keyword, from) {
41350
+ let at = upper.indexOf(keyword, from);
41351
+ while (at >= 0) {
41352
+ const end = at + keyword.length;
41353
+ const okBefore = at === 0 || isSqlWs(upper[at - 1]);
41354
+ const okAfter = end >= upper.length || isSqlWs(upper[end]);
41355
+ if (okBefore && okAfter) return at;
41356
+ at = upper.indexOf(keyword, at + 1);
41357
+ }
41358
+ return -1;
41359
+ }
41360
+ function earliest(positions, fallback) {
41361
+ let min = fallback;
41362
+ for (const p of positions) if (p >= 0 && p < min) min = p;
41363
+ return min;
41364
+ }
41365
+ function readIdentifier(sql, from) {
41366
+ let i = from;
41367
+ while (i < sql.length && isSqlWs(sql[i])) i++;
41368
+ if (i < sql.length && QUOTE_CHARS.includes(sql[i])) i++;
41369
+ const start2 = i;
41370
+ while (i < sql.length && isWordChar(sql[i])) i++;
41371
+ if (i === start2) return null;
41372
+ const name = sql.slice(start2, i);
41373
+ if (i < sql.length && QUOTE_CHARS.includes(sql[i])) i++;
41374
+ return [name, i];
41375
+ }
41376
+ function keywordGapEnd(upper, sql, keyword, from) {
41377
+ const at = keywordAt(upper, keyword, from);
41378
+ if (at < 0 || !onlyWs(sql, from, at)) return -1;
41379
+ return at + keyword.length;
41380
+ }
41381
+ function stripQuotes(value) {
41382
+ let result = value;
41383
+ if (result.length > 0 && (result[0] === '"' || result[0] === "'")) result = result.slice(1);
41384
+ const last = result.length - 1;
41385
+ if (last >= 0 && (result[last] === '"' || result[last] === "'")) result = result.slice(0, last);
41386
+ return result;
41387
+ }
41388
+ function splitOnAnd(where) {
41389
+ const parts = [];
41390
+ const lower = where.toLowerCase();
41391
+ let segStart = 0;
41392
+ let i = 0;
41393
+ while (i < where.length) {
41394
+ if (lower.startsWith("and", i) && i > 0 && isSqlWs(where[i - 1]) && i + 3 < where.length && isSqlWs(where[i + 3])) {
41395
+ parts.push(where.slice(segStart, i));
41396
+ i += 3;
41397
+ segStart = i;
41398
+ continue;
41399
+ }
41400
+ i++;
41401
+ }
41402
+ parts.push(where.slice(segStart));
41403
+ return parts;
41404
+ }
41109
41405
  function parseWhereClause(where, params, paramOffset = 0) {
41110
41406
  const filter = {};
41111
- const parts = where.split(/\s+AND\s+/i);
41407
+ const parts = splitOnAnd(where);
41112
41408
  let paramIndex = paramOffset;
41113
41409
  for (const part of parts) {
41114
41410
  const trimmed = part.trim();
@@ -41181,27 +41477,57 @@ function requireWriteFilter(filter, operation, table2) {
41181
41477
  );
41182
41478
  }
41183
41479
  }
41184
- function parseSelect(sql, params) {
41185
- const match = sql.match(/^SELECT\s+(.*?)\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.*?))?(?:\s+ORDER\s+BY\s+(.*?))?(?:\s+LIMIT\s+(\d+))?(?:\s+OFFSET\s+(\d+))?$/is);
41186
- if (!match) return null;
41187
- const [, cols, collection, whereClause, orderBy, limitStr, skipStr] = match;
41480
+ function buildProjection(cols) {
41188
41481
  const projection = {};
41189
- if (cols && cols.trim() !== "*") {
41190
- for (const col of cols.split(",")) {
41191
- const name = col.trim().replace(/^["']|["']$/g, "");
41192
- if (name && name !== "*") projection[name] = 1;
41193
- }
41194
- }
41195
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
41482
+ if (!cols || cols.trim() === "*") return projection;
41483
+ for (const col of cols.split(",")) {
41484
+ const name = stripQuotes(col.trim());
41485
+ if (name && name !== "*") projection[name] = 1;
41486
+ }
41487
+ return projection;
41488
+ }
41489
+ function stripTrailingDirection(part) {
41490
+ const lower = part.toLowerCase();
41491
+ let suffix = 0;
41492
+ if (lower.endsWith("asc")) suffix = 3;
41493
+ else if (lower.endsWith("desc")) suffix = 4;
41494
+ else return part;
41495
+ const beforeIdx = part.length - suffix - 1;
41496
+ if (beforeIdx < 0 || !isSqlWs(part[beforeIdx])) return part;
41497
+ let i = beforeIdx;
41498
+ while (i >= 0 && isSqlWs(part[i])) i--;
41499
+ return part.slice(0, i + 1);
41500
+ }
41501
+ function parseOrderBy(orderBy) {
41196
41502
  const sort = {};
41197
- if (orderBy) {
41198
- for (const part of orderBy.split(",")) {
41199
- const trimmed = part.trim();
41200
- const desc = /DESC$/i.test(trimmed);
41201
- const col = trimmed.replace(/\s+(ASC|DESC)$/i, "").replace(/^["']|["']$/g, "").trim();
41202
- sort[col] = desc ? -1 : 1;
41203
- }
41503
+ for (const rawPart of orderBy.split(",")) {
41504
+ const trimmed = rawPart.trim();
41505
+ const desc = trimmed.toLowerCase().endsWith("desc");
41506
+ const col = stripQuotes(stripTrailingDirection(trimmed)).trim();
41507
+ sort[col] = desc ? -1 : 1;
41204
41508
  }
41509
+ return sort;
41510
+ }
41511
+ function parseSelect(sql, params) {
41512
+ const upper = sql.toUpperCase();
41513
+ if (!(upper.startsWith("SELECT") && (sql.length === 6 || isSqlWs(sql[6])))) return null;
41514
+ const fromAt = keywordAt(upper, "FROM", 6);
41515
+ if (fromAt < 0) return null;
41516
+ const cols = sql.slice(6, fromAt).trim();
41517
+ const ident = readIdentifier(sql, fromAt + 4);
41518
+ if (!ident) return null;
41519
+ const [collection, afterColl] = ident;
41520
+ const whereAt = keywordAt(upper, "WHERE", afterColl);
41521
+ const orderAt = keywordAt(upper, "ORDER BY", afterColl);
41522
+ const limitAt = keywordAt(upper, "LIMIT", afterColl);
41523
+ const offsetAt = keywordAt(upper, "OFFSET", afterColl);
41524
+ const end = sql.length;
41525
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5, earliest([orderAt, limitAt, offsetAt], end)).trim();
41526
+ const orderBy = orderAt < 0 ? void 0 : sql.slice(orderAt + 8, earliest([limitAt, offsetAt], end)).trim();
41527
+ const limitStr = limitAt < 0 ? void 0 : sql.slice(limitAt + 5, earliest([offsetAt], end)).trim();
41528
+ const skipStr = offsetAt < 0 ? void 0 : sql.slice(offsetAt + 6).trim();
41529
+ const projection = buildProjection(cols);
41530
+ const filter = whereClause ? parseWhereClause(whereClause, params).filter : {};
41205
41531
  return {
41206
41532
  type: "find",
41207
41533
  collection,
@@ -41209,7 +41535,7 @@ function parseSelect(sql, params) {
41209
41535
  projection: Object.keys(projection).length > 0 ? projection : void 0,
41210
41536
  limit: limitStr ? parseInt(limitStr, 10) : void 0,
41211
41537
  skip: skipStr ? parseInt(skipStr, 10) : void 0,
41212
- sort: orderBy ? sort : void 0
41538
+ sort: orderBy ? parseOrderBy(orderBy) : void 0
41213
41539
  };
41214
41540
  }
41215
41541
  function parseInsert(sql, params) {
@@ -41245,20 +41571,33 @@ function parseSetClause(setClause, params) {
41245
41571
  return { document, consumed };
41246
41572
  }
41247
41573
  function parseUpdate(sql, params) {
41248
- const match = sql.match(/^UPDATE\s+["']?(\w+)["']?\s+SET\s+(.*?)(?:\s+WHERE\s+(.+))?$/is);
41249
- if (!match) return null;
41250
- const [, collection, setClause, whereClause] = match;
41574
+ const upper = sql.toUpperCase();
41575
+ if (!(upper.startsWith("UPDATE") && sql.length > 6 && isSqlWs(sql[6]))) return null;
41576
+ const ident = readIdentifier(sql, 6);
41577
+ if (!ident) return null;
41578
+ const [collection, afterColl] = ident;
41579
+ const setAt = keywordAt(upper, "SET", afterColl);
41580
+ if (setAt < 0 || !onlyWs(sql, afterColl, setAt)) return null;
41581
+ const whereAt = keywordAt(upper, "WHERE", setAt + 3);
41582
+ const setClause = sql.slice(setAt + 3, whereAt < 0 ? sql.length : whereAt).trim();
41583
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5).trim();
41251
41584
  const set = parseSetClause(setClause, params);
41252
- const matchAll = isMatchAllWhere(whereClause?.trim());
41253
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params, set.consumed).filter : {};
41585
+ const matchAll = isMatchAllWhere(whereClause);
41586
+ const filter = whereClause ? parseWhereClause(whereClause, params, set.consumed).filter : {};
41254
41587
  return { type: "updateMany", collection, filter, update: { $set: set.document }, matchAll };
41255
41588
  }
41256
41589
  function parseDelete(sql, params) {
41257
- const match = sql.match(/^DELETE\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is);
41258
- if (!match) return null;
41259
- const [, collection, whereClause] = match;
41260
- const matchAll = isMatchAllWhere(whereClause?.trim());
41261
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
41590
+ const upper = sql.toUpperCase();
41591
+ if (!upper.startsWith("DELETE")) return null;
41592
+ const fromAt = keywordAt(upper, "FROM", 6);
41593
+ if (fromAt < 0 || !onlyWs(sql, 6, fromAt)) return null;
41594
+ const ident = readIdentifier(sql, fromAt + 4);
41595
+ if (!ident) return null;
41596
+ const [collection, afterColl] = ident;
41597
+ const whereAt = keywordAt(upper, "WHERE", afterColl);
41598
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5).trim();
41599
+ const matchAll = isMatchAllWhere(whereClause);
41600
+ const filter = whereClause ? parseWhereClause(whereClause, params).filter : {};
41262
41601
  return { type: "deleteMany", collection, filter, matchAll };
41263
41602
  }
41264
41603
  function parseCreate(sql) {
@@ -41266,10 +41605,23 @@ function parseCreate(sql) {
41266
41605
  return match ? { type: "raw", collection: match[1] } : null;
41267
41606
  }
41268
41607
  function parseCount(sql, params) {
41269
- const match = sql.match(/^SELECT\s+COUNT\(\*\)\s+AS\s+(\w+)\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is);
41270
- if (!match) return null;
41271
- const [, alias, collection, whereClause] = match;
41272
- const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
41608
+ const upper = sql.toUpperCase();
41609
+ if (!upper.startsWith("SELECT")) return null;
41610
+ const afterCount = keywordGapEnd(upper, sql, "COUNT(*)", 6);
41611
+ if (afterCount < 0) return null;
41612
+ const afterAs = keywordGapEnd(upper, sql, "AS", afterCount);
41613
+ if (afterAs < 0) return null;
41614
+ const aliasIdent = readIdentifier(sql, afterAs);
41615
+ if (!aliasIdent) return null;
41616
+ const [alias, afterAlias] = aliasIdent;
41617
+ const afterFrom = keywordGapEnd(upper, sql, "FROM", afterAlias);
41618
+ if (afterFrom < 0) return null;
41619
+ const coll = readIdentifier(sql, afterFrom);
41620
+ if (!coll) return null;
41621
+ const [collection, afterColl] = coll;
41622
+ const whereAt = keywordAt(upper, "WHERE", afterColl);
41623
+ const whereClause = whereAt < 0 ? void 0 : sql.slice(whereAt + 5).trim();
41624
+ const filter = whereClause ? parseWhereClause(whereClause, params).filter : {};
41273
41625
  return { type: "aggregate", collection, pipeline: [{ $match: filter }, { $count: alias }] };
41274
41626
  }
41275
41627
  function parseSql(sql, params = []) {
@@ -41295,12 +41647,13 @@ function fieldTypeToJsonSchema(def) {
41295
41647
  return { bsonType: "string" };
41296
41648
  }
41297
41649
  }
41298
- var MATCH_ALL_WHERE, MongodbAdapter;
41650
+ var MATCH_ALL_WHERE, QUOTE_CHARS, MongodbAdapter;
41299
41651
  var init_mongodb = __esm({
41300
41652
  "src/adapters/mongodb.ts"() {
41301
41653
  "use strict";
41302
41654
  init_connectTimeout();
41303
41655
  MATCH_ALL_WHERE = /^\s*1\s*=\s*1\s*$/;
41656
+ QUOTE_CHARS = `"'`;
41304
41657
  MongodbAdapter = class {
41305
41658
  constructor(config) {
41306
41659
  this.config = config;
@@ -43624,13 +43977,13 @@ var init_database = __esm({
43624
43977
  });
43625
43978
 
43626
43979
  // src/model.ts
43627
- import { readdirSync as readdirSync17, statSync as statSync18 } from "node:fs";
43980
+ import { readdirSync as readdirSync18, statSync as statSync18 } from "node:fs";
43628
43981
  import { join as join30, extname as extname8 } from "node:path";
43629
43982
  async function discoverModels(modelsDir) {
43630
43983
  const models = [];
43631
43984
  let files;
43632
43985
  try {
43633
- files = readdirSync17(modelsDir);
43986
+ files = readdirSync18(modelsDir);
43634
43987
  } catch {
43635
43988
  return models;
43636
43989
  }
@@ -43677,8 +44030,10 @@ var init_model = __esm({
43677
44030
  });
43678
44031
 
43679
44032
  // src/migration.ts
43680
- import { existsSync as existsSync27, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17 } from "node:fs";
44033
+ import { existsSync as existsSync27, readdirSync as readdirSync19, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17, rmSync } from "node:fs";
43681
44034
  import { join as join31, resolve as resolve19 } from "node:path";
44035
+ import { tmpdir } from "node:os";
44036
+ import { createHash as createHash10 } from "node:crypto";
43682
44037
  function unwrapAdapter(db) {
43683
44038
  let cur = db;
43684
44039
  while (cur && cur.constructor?.name === "CachedDatabaseAdapter" && cur.adapter) {
@@ -44157,17 +44512,113 @@ function warnUnprefixedMigrations(files) {
44157
44512
  );
44158
44513
  }
44159
44514
  }
44515
+ async function acquireMigrationLock(db, dir) {
44516
+ let engine = "";
44517
+ try {
44518
+ engine = (db.getDatabaseType() || "").toLowerCase();
44519
+ } catch {
44520
+ engine = "";
44521
+ }
44522
+ try {
44523
+ if (engine.startsWith("postgres")) {
44524
+ await adapterQuery(db, `SELECT pg_advisory_lock(${PG_ADVISORY_KEY}) AS locked`);
44525
+ return { kind: "postgres" };
44526
+ }
44527
+ if (engine.startsWith("mysql")) {
44528
+ await adapterQuery(db, `SELECT GET_LOCK('${MIGRATION_LOCK_NAME}', -1) AS locked`);
44529
+ return { kind: "mysql" };
44530
+ }
44531
+ if (engine === "mssql" || engine === "sqlserver") {
44532
+ await adapterExecute(
44533
+ db,
44534
+ `DECLARE @res INT; EXEC @res = sp_getapplock @Resource = '${MIGRATION_LOCK_NAME}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = -1`
44535
+ );
44536
+ return { kind: "mssql" };
44537
+ }
44538
+ } catch (err) {
44539
+ Log.debug(`DB migration lock unavailable (${engine}): ${err instanceof Error ? err.message : err}; using a file lock`);
44540
+ }
44541
+ return acquireFileLock(dir);
44542
+ }
44543
+ async function acquireFileLock(dir) {
44544
+ const lockFile = join31(tmpdir(), `tina4-migration-${createHash10("sha256").update(resolve19(dir)).digest("hex")}.lock`);
44545
+ const deadline = Date.now() + 6e4;
44546
+ for (; ; ) {
44547
+ try {
44548
+ writeFileSync17(lockFile, String(process.pid), { flag: "wx" });
44549
+ return { kind: "file", resource: lockFile };
44550
+ } catch (err) {
44551
+ if (err.code !== "EEXIST") {
44552
+ Log.debug(`file migration lock unavailable: ${err instanceof Error ? err.message : err}; running unlocked`);
44553
+ return { kind: "none" };
44554
+ }
44555
+ if (isStaleLock(lockFile)) {
44556
+ try {
44557
+ rmSync(lockFile, { force: true });
44558
+ } catch {
44559
+ }
44560
+ continue;
44561
+ }
44562
+ if (Date.now() > deadline) {
44563
+ Log.debug("file migration lock wait timed out; running unlocked");
44564
+ return { kind: "none" };
44565
+ }
44566
+ await delay(50);
44567
+ }
44568
+ }
44569
+ }
44570
+ function isStaleLock(lockFile) {
44571
+ let raw;
44572
+ try {
44573
+ raw = readFileSync25(lockFile, "utf-8").trim();
44574
+ } catch {
44575
+ return false;
44576
+ }
44577
+ const pid = parseInt(raw, 10);
44578
+ if (!raw || !Number.isFinite(pid) || pid <= 0) {
44579
+ return false;
44580
+ }
44581
+ try {
44582
+ process.kill(pid, 0);
44583
+ return false;
44584
+ } catch (err) {
44585
+ return err.code === "ESRCH";
44586
+ }
44587
+ }
44588
+ async function releaseMigrationLock(db, lock) {
44589
+ try {
44590
+ if (lock.kind === "postgres") {
44591
+ await adapterQuery(db, `SELECT pg_advisory_unlock(${PG_ADVISORY_KEY}) AS released`);
44592
+ } else if (lock.kind === "mysql") {
44593
+ await adapterQuery(db, `SELECT RELEASE_LOCK('${MIGRATION_LOCK_NAME}') AS released`);
44594
+ } else if (lock.kind === "mssql") {
44595
+ await adapterExecute(db, `EXEC sp_releaseapplock @Resource = '${MIGRATION_LOCK_NAME}', @LockOwner = 'Session'`);
44596
+ } else if (lock.kind === "file" && lock.resource) {
44597
+ rmSync(lock.resource, { recursive: true, force: true });
44598
+ }
44599
+ } catch (err) {
44600
+ Log.debug(`migration lock release failed (${lock.kind}): ${err instanceof Error ? err.message : err}`);
44601
+ }
44602
+ }
44160
44603
  async function migrate(adapter, options) {
44161
44604
  const db = adapter ?? getAdapter();
44162
44605
  const dir = resolve19(options?.migrationsDir ?? "migrations");
44163
44606
  const delimiter2 = options?.delimiter ?? ";";
44164
- const result = { applied: [], skipped: [], failed: [] };
44165
44607
  if (!existsSync27(dir)) {
44166
- return result;
44608
+ return { applied: [], skipped: [], failed: [] };
44609
+ }
44610
+ const lock = await acquireMigrationLock(db, dir);
44611
+ try {
44612
+ return await runPendingMigrations(db, dir, delimiter2);
44613
+ } finally {
44614
+ await releaseMigrationLock(db, lock);
44167
44615
  }
44616
+ }
44617
+ async function runPendingMigrations(db, dir, delimiter2) {
44618
+ const result = { applied: [], skipped: [], failed: [] };
44168
44619
  await ensureMigrationTableOn(db);
44169
44620
  const files = sortMigrationFiles(
44170
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44621
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44171
44622
  );
44172
44623
  if (files.length === 0) return result;
44173
44624
  warnUnprefixedMigrations(files);
@@ -44240,14 +44691,14 @@ async function status(adapter, options) {
44240
44691
  }
44241
44692
  if (!await adapterTableExists(db, MIGRATION_TABLE)) {
44242
44693
  const files2 = sortMigrationFiles(
44243
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44694
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44244
44695
  );
44245
44696
  result.pending = files2;
44246
44697
  return result;
44247
44698
  }
44248
44699
  await ensureMigrationTableOn(db);
44249
44700
  const files = sortMigrationFiles(
44250
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44701
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44251
44702
  );
44252
44703
  const appliedNames = /* @__PURE__ */ new Set();
44253
44704
  try {
@@ -44347,7 +44798,7 @@ export class ${className} {
44347
44798
  writeFileSync17(filePath, content, "utf-8");
44348
44799
  return filePath;
44349
44800
  }
44350
- var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, Migration;
44801
+ var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, MIGRATION_LOCK_NAME, PG_ADVISORY_KEY, delay, Migration;
44351
44802
  var init_migration = __esm({
44352
44803
  "src/migration.ts"() {
44353
44804
  "use strict";
@@ -44372,6 +44823,12 @@ var init_migration = __esm({
44372
44823
  };
44373
44824
  SMART_QUOTE_RE = new RegExp(`[${Object.keys(SMART_QUOTES).join("")}]`, "g");
44374
44825
  SET_TERM_RE = /^SET\s+TERM\s+(\S+)$/i;
44826
+ MIGRATION_LOCK_NAME = "tina4_migration_lock";
44827
+ PG_ADVISORY_KEY = BigInt.asIntN(
44828
+ 64,
44829
+ BigInt("0x" + createHash10("sha256").update(MIGRATION_LOCK_NAME).digest("hex").slice(0, 16))
44830
+ ).toString();
44831
+ delay = (ms) => new Promise((r) => setTimeout(r, ms));
44375
44832
  Migration = class {
44376
44833
  db;
44377
44834
  dir;
@@ -44428,7 +44885,7 @@ var init_migration = __esm({
44428
44885
  const dir = resolve19(this.dir);
44429
44886
  if (!existsSync27(dir)) return [];
44430
44887
  return sortMigrationFiles(
44431
- readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44888
+ readdirSync19(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
44432
44889
  );
44433
44890
  }
44434
44891
  };
@@ -44436,17 +44893,40 @@ var init_migration = __esm({
44436
44893
  });
44437
44894
 
44438
44895
  // src/query.ts
44439
- function resolveFieldColumn(fields, columnOf, key) {
44440
- const field = resolveField(fields, columnOf, key);
44441
- return field === null ? null : columnOf(field);
44896
+ function resolveFieldColumn(fields, columnOf2, key) {
44897
+ const field = resolveField(fields, columnOf2, key);
44898
+ return field === null ? null : columnOf2(field);
44442
44899
  }
44443
- function resolveField(fields, columnOf, key) {
44900
+ function resolveField(fields, columnOf2, key) {
44444
44901
  if (Object.hasOwn(fields, key)) return key;
44445
44902
  for (const field of Object.keys(fields)) {
44446
- if (columnOf(field) === key) return field;
44903
+ if (columnOf2(field) === key) return field;
44447
44904
  }
44448
44905
  return null;
44449
44906
  }
44907
+ function applySearch(options, conditions, params, quote) {
44908
+ const searchTerm = (options.search ?? "").trim();
44909
+ if (searchTerm === "" || !options.searchColumns || options.searchColumns.length === 0) return;
44910
+ const likeParts = options.searchColumns.map((col) => `${quote(col)} LIKE ?`);
44911
+ conditions.push(`(${likeParts.join(" OR ")})`);
44912
+ for (const _ of options.searchColumns) params.push(`%${searchTerm}%`);
44913
+ }
44914
+ function orderPart(rawPart, bareDir, column2) {
44915
+ const trimmed = rawPart.trim();
44916
+ if (trimmed === "") return null;
44917
+ const descending = trimmed.startsWith("-");
44918
+ const col = column2("sort", descending ? trimmed.slice(1) : trimmed);
44919
+ return `${col} ${bareDir ?? (descending ? "DESC" : "ASC")}`;
44920
+ }
44921
+ function buildOrderClause(options, column2) {
44922
+ if (!options.sort) return "";
44923
+ const rawParts = options.sort.split(",");
44924
+ const first = rawParts[0].trim();
44925
+ const singleBare = rawParts.length === 1 && first !== "" && !first.startsWith("-");
44926
+ const bareDir = singleBare && String(options.sortDir).toLowerCase() === "desc" ? "DESC" : null;
44927
+ const parts = rawParts.map((part) => orderPart(part, bareDir, column2)).filter((term) => term !== null);
44928
+ return parts.length > 0 ? `ORDER BY ${parts.join(", ")}` : "";
44929
+ }
44450
44930
  function buildQuery(tableName, options, extraConditions, quote = quoteIdentifierAnsi, resolveColumn = () => null) {
44451
44931
  const conditions = [];
44452
44932
  const params = [];
@@ -44474,19 +44954,9 @@ function buildQuery(tableName, options, extraConditions, quote = quoteIdentifier
44474
44954
  }
44475
44955
  }
44476
44956
  }
44957
+ applySearch(options, conditions, params, quote);
44477
44958
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
44478
- let orderClause = "";
44479
- if (options.sort) {
44480
- const parts = [];
44481
- for (const part of options.sort.split(",")) {
44482
- const trimmed = part.trim();
44483
- if (trimmed === "") continue;
44484
- const descending = trimmed.startsWith("-");
44485
- const col = column2("sort", descending ? trimmed.slice(1) : trimmed);
44486
- parts.push(`${col} ${descending ? "DESC" : "ASC"}`);
44487
- }
44488
- if (parts.length > 0) orderClause = `ORDER BY ${parts.join(", ")}`;
44489
- }
44959
+ const orderClause = buildOrderClause(options, column2);
44490
44960
  const limit = Math.min(options.limit ?? DEFAULT_ROW_CAP, DEFAULT_ROW_CAP);
44491
44961
  const page = Math.max(options.page ?? 1, 1);
44492
44962
  const offset = (page - 1) * limit;
@@ -44504,32 +44974,41 @@ function buildQuery(tableName, options, extraConditions, quote = quoteIdentifier
44504
44974
  filterParams: params
44505
44975
  };
44506
44976
  }
44507
- function parseQueryString(query) {
44508
- const options = {};
44977
+ function parseFilterParams(query) {
44509
44978
  const filter = /* @__PURE__ */ Object.create(null);
44510
44979
  for (const [key, value] of Object.entries(query)) {
44511
44980
  if (key.startsWith("sort[")) {
44512
44981
  throw new InvalidQueryParameterError("Query parameter 'sort' must be a single comma-separated string");
44513
44982
  }
44514
44983
  if (!key.startsWith("filter[")) continue;
44515
- const filterMatch = key.match(/^filter\[([^\]]*)\](.*)$/s);
44516
- if (!filterMatch) throw new UnknownFieldError("filter", key.slice("filter[".length));
44517
- const [, field, rest] = filterMatch;
44518
- if (rest === "") {
44519
- filter[field] = value;
44520
- continue;
44521
- }
44522
- const operator = rest.match(/^\[([^[\]]*)\]$/)?.[1];
44523
- if (operator === void 0 || !Object.hasOwn(operatorMap, operator)) throw filterValueError(field);
44524
- if (!filter[field] || typeof filter[field] !== "object") {
44525
- filter[field] = {};
44526
- }
44527
- filter[field][operator] = value;
44984
+ addFilterKey(filter, key, value);
44985
+ }
44986
+ return filter;
44987
+ }
44988
+ function addFilterKey(filter, key, value) {
44989
+ const filterMatch = key.match(/^filter\[([^\]]*)\](.*)$/s);
44990
+ if (!filterMatch) throw new UnknownFieldError("filter", key.slice("filter[".length));
44991
+ const [, field, rest] = filterMatch;
44992
+ if (rest === "") {
44993
+ filter[field] = value;
44994
+ return;
44995
+ }
44996
+ const operator = rest.match(/^\[([^[\]]*)\]$/)?.[1];
44997
+ if (operator === void 0 || !Object.hasOwn(operatorMap, operator)) throw filterValueError(field);
44998
+ if (!filter[field] || typeof filter[field] !== "object") {
44999
+ filter[field] = {};
44528
45000
  }
45001
+ filter[field][operator] = value;
45002
+ }
45003
+ function parseQueryString(query) {
45004
+ const options = {};
45005
+ const filter = parseFilterParams(query);
44529
45006
  if (Object.keys(filter).length > 0) {
44530
45007
  options.filter = filter;
44531
45008
  }
44532
45009
  if (query.sort) options.sort = query.sort;
45010
+ if (query.search !== void 0) options.search = query.search;
45011
+ if (query.sort_dir !== void 0) options.sortDir = query.sort_dir;
44533
45012
  if (query.page) options.page = parseInt(query.page, 10);
44534
45013
  if (query.limit) options.limit = parseInt(query.limit, 10);
44535
45014
  if (query.offset !== void 0) {
@@ -44665,12 +45144,12 @@ var init_validation = __esm({
44665
45144
  });
44666
45145
 
44667
45146
  // src/autoCrud.ts
44668
- function allowListedBody(fields, columnOf, pkField, body, isCreate) {
45147
+ function allowListedBody(fields, columnOf2, pkField, body, isCreate) {
44669
45148
  const pkDef = fields[pkField];
44670
45149
  const stripPk = isCreate ? pkDef?.autoIncrement === true : true;
44671
45150
  const allowed = {};
44672
45151
  for (const [key, value] of Object.entries(body ?? {})) {
44673
- const field = resolveField(fields, columnOf, key);
45152
+ const field = resolveField(fields, columnOf2, key);
44674
45153
  if (field === null) continue;
44675
45154
  if (field === "is_deleted") continue;
44676
45155
  if (stripPk && field === pkField) continue;
@@ -44714,6 +45193,9 @@ function generateCrudRoutes(models, options = {}) {
44714
45193
  let built;
44715
45194
  try {
44716
45195
  const qp = parseQueryString(req2.query ?? {});
45196
+ if ((qp.search ?? "").trim() !== "") {
45197
+ qp.searchColumns = Object.entries(fields).filter(([, def]) => def.type === "string" || def.type === "text").map(([name]) => getDbCol(name));
45198
+ }
44717
45199
  built = buildQuery(tableName, qp, extraConditions, q, (key) => resolveFieldColumn(fields, getDbCol, key));
44718
45200
  } catch (err) {
44719
45201
  if (err instanceof UnknownFieldError) {
@@ -44961,6 +45443,567 @@ var init_autoCrud = __esm({
44961
45443
  }
44962
45444
  });
44963
45445
 
45446
+ // src/crudHelpers.ts
45447
+ function columnOf(model, prop) {
45448
+ return model.fieldMapping?.[prop] ?? prop;
45449
+ }
45450
+ function primaryKeyProp(model) {
45451
+ for (const [name, def] of Object.entries(model.fields)) {
45452
+ if (def.primaryKey) return name;
45453
+ }
45454
+ return "id";
45455
+ }
45456
+ function searchableProps(model) {
45457
+ return Object.entries(model.fields).filter(([, def]) => def.type === "string" || def.type === "text").map(([name]) => name);
45458
+ }
45459
+ function columnAlignment(model, col) {
45460
+ const type2 = model?.fields?.[col]?.type;
45461
+ return type2 === "integer" || type2 === "number" || type2 === "float" || type2 === "decimal" ? "text-end" : "text-start";
45462
+ }
45463
+ function prettyLabel(col) {
45464
+ return String(col).split("_").map((p) => p ? p.charAt(0).toUpperCase() + p.slice(1) : p).join(" ");
45465
+ }
45466
+ function crudSortProp(model, requested, pkProp) {
45467
+ const req2 = (requested ?? "").trim();
45468
+ if (req2 === "") return pkProp;
45469
+ if (Object.hasOwn(model.fields, req2)) return req2;
45470
+ for (const name of Object.keys(model.fields)) {
45471
+ if (columnOf(model, name) === req2) return name;
45472
+ }
45473
+ return pkProp;
45474
+ }
45475
+ function cellValue(record, col) {
45476
+ return record[col];
45477
+ }
45478
+ function buildCells(columns, record, editable, aligns) {
45479
+ return columns.map((col, index) => {
45480
+ const raw = cellValue(record, col);
45481
+ const value = h(raw === null || raw === void 0 ? "" : raw);
45482
+ const css = aligns[index];
45483
+ return editable ? `<td class="${css}" contenteditable="true" data-field="${h(col)}">${value}</td>` : `<td class="${css}">${value}</td>`;
45484
+ }).join("");
45485
+ }
45486
+ function stripOrderAndLimit(sql) {
45487
+ const cut = (line, keyword) => {
45488
+ const hasNewline = line.endsWith("\n");
45489
+ const content = hasNewline ? line.slice(0, -1) : line;
45490
+ const at = content.toUpperCase().indexOf(keyword);
45491
+ if (at < 0) return line;
45492
+ const after = at + keyword.length;
45493
+ if (content.length <= after) return line;
45494
+ return content.slice(0, at) + (hasNewline ? "\n" : "");
45495
+ };
45496
+ const src = String(sql);
45497
+ const lines = [];
45498
+ let start2 = 0;
45499
+ for (let i = 0; i < src.length; i++) {
45500
+ if (src[i] === "\n") {
45501
+ lines.push(src.slice(start2, i + 1));
45502
+ start2 = i + 1;
45503
+ }
45504
+ }
45505
+ lines.push(src.slice(start2));
45506
+ return lines.map((line) => cut(cut(line, "ORDER BY "), "LIMIT ")).join("").trim();
45507
+ }
45508
+ function extractColumns(sql) {
45509
+ const colsStr = selectProjection(sql);
45510
+ if (colsStr === null || colsStr === "*") return ["*"];
45511
+ return colsStr.split(",").map((c) => columnAlias(c.trim()));
45512
+ }
45513
+ function selectProjection(sql) {
45514
+ const upper = sql.toUpperCase();
45515
+ const selectAt = keywordIndex(upper, "SELECT", 0);
45516
+ if (selectAt < 0) return null;
45517
+ const colsStart = selectAt + "SELECT".length;
45518
+ const fromAt = keywordIndex(upper, "FROM", colsStart);
45519
+ if (fromAt < 0) return null;
45520
+ return sql.slice(colsStart, fromAt).trim();
45521
+ }
45522
+ function columnAlias(trimmed) {
45523
+ const asAt = keywordIndex(trimmed.toUpperCase(), "AS", 0);
45524
+ if (asAt >= 0) {
45525
+ const after = trimmed.slice(asAt + "AS".length).trim();
45526
+ let endIdx = 0;
45527
+ while (endIdx < after.length && !SQL_WS.includes(after[endIdx]) && after[endIdx] !== ",") endIdx++;
45528
+ const word = after.slice(0, endIdx);
45529
+ if (word) return word;
45530
+ }
45531
+ if (trimmed.includes(".")) return trimmed.split(".").pop().trim();
45532
+ return trimmed;
45533
+ }
45534
+ function keywordIndex(upperHaystack, keyword, from) {
45535
+ let at = upperHaystack.indexOf(keyword, from);
45536
+ while (at >= 0) {
45537
+ const afterIdx = at + keyword.length;
45538
+ const boundaryBefore = at === 0 || SQL_WS.includes(upperHaystack[at - 1]);
45539
+ const boundaryAfter = afterIdx >= upperHaystack.length || SQL_WS.includes(upperHaystack[afterIdx]);
45540
+ if (boundaryBefore && boundaryAfter) return at;
45541
+ at = upperHaystack.indexOf(keyword, at + 1);
45542
+ }
45543
+ return -1;
45544
+ }
45545
+ var h, SQL_WS;
45546
+ var init_crudHelpers = __esm({
45547
+ "src/crudHelpers.ts"() {
45548
+ "use strict";
45549
+ init_engine();
45550
+ h = escapeHtml;
45551
+ SQL_WS = " \n\r\f\v";
45552
+ }
45553
+ });
45554
+
45555
+ // src/crudTable.ts
45556
+ function sortIndicator(sortCol, col, sortDir) {
45557
+ if (String(sortCol) !== String(col)) return "";
45558
+ const arrow = sortDir === "asc" ? "&#9650;" : "&#9660;";
45559
+ return ` <span class="sort-indicator">${arrow}</span>`;
45560
+ }
45561
+ function sortUrl(requestPath, col, nextDir, page, search, limit) {
45562
+ const query = `sort=${encodeURIComponent(col)}&sort_dir=${nextDir}&page=${page}&search=${encodeURIComponent(search)}&limit=${limit}`;
45563
+ return h(`${requestPath}?${query}`);
45564
+ }
45565
+ function pageUrl(requestPath, p, search, sortCol, sortDir, limit) {
45566
+ const query = `page=${p}&search=${encodeURIComponent(search)}&sort=${encodeURIComponent(sortCol)}&sort_dir=${sortDir}&limit=${limit}`;
45567
+ return h(`${requestPath}?${query}`);
45568
+ }
45569
+ function pageControls(page, totalPages, requestPath, search, sortCol, sortDir, limit) {
45570
+ if (totalPages <= 1) return [];
45571
+ const controls = [];
45572
+ if (page > 1) {
45573
+ controls.push({
45574
+ label: "Prev",
45575
+ page: page - 1,
45576
+ active: false,
45577
+ inactive: true,
45578
+ url: pageUrl(requestPath, page - 1, search, sortCol, sortDir, limit)
45579
+ });
45580
+ }
45581
+ let startPage = Math.max(page - 3, 1);
45582
+ const endPage = Math.min(startPage + 6, totalPages);
45583
+ startPage = Math.max(endPage - 6, 1);
45584
+ for (let p = startPage; p <= endPage; p++) {
45585
+ controls.push({
45586
+ label: p,
45587
+ page: p,
45588
+ active: p === page,
45589
+ inactive: p !== page,
45590
+ url: pageUrl(requestPath, p, search, sortCol, sortDir, limit)
45591
+ });
45592
+ }
45593
+ if (page < totalPages) {
45594
+ controls.push({
45595
+ label: "Next",
45596
+ page: page + 1,
45597
+ active: false,
45598
+ inactive: true,
45599
+ url: pageUrl(requestPath, page + 1, search, sortCol, sortDir, limit)
45600
+ });
45601
+ }
45602
+ return controls;
45603
+ }
45604
+ function jsConfig(opts) {
45605
+ const aligns = {};
45606
+ const labels = {};
45607
+ for (const col of opts.columns) {
45608
+ aligns[col] = columnAlignment(opts.model, col);
45609
+ labels[col] = prettyLabel(col);
45610
+ }
45611
+ const config = {
45612
+ api: opts.apiPath,
45613
+ pk: opts.pk,
45614
+ columns: opts.columns,
45615
+ editable: opts.editable,
45616
+ aligns,
45617
+ labels,
45618
+ limit: opts.limit,
45619
+ search: opts.search,
45620
+ sort: opts.sortCol,
45621
+ sort_dir: opts.sortDir,
45622
+ page: opts.page
45623
+ };
45624
+ return JSON.stringify(config).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
45625
+ }
45626
+ function headerCell(opts, col, align) {
45627
+ const header = { label: h(prettyLabel(col)), align };
45628
+ if (!opts.sortable) {
45629
+ header.plain = true;
45630
+ return header;
45631
+ }
45632
+ const nextDir = String(opts.sortCol) === String(col) && opts.sortDir === "asc" ? "desc" : "asc";
45633
+ header.sortable = true;
45634
+ header.col = h(col);
45635
+ header.next_dir = nextDir;
45636
+ header.url = sortUrl(opts.requestPath, col, nextDir, opts.page, opts.search, opts.limit);
45637
+ header.indicator = sortIndicator(opts.sortCol, col, opts.sortDir);
45638
+ return header;
45639
+ }
45640
+ function tableData(opts) {
45641
+ const aligns = opts.columns.map((col) => columnAlignment(opts.model, col));
45642
+ const headers = opts.columns.map((col, index) => headerCell(opts, col, aligns[index]));
45643
+ const rows = opts.records.map((record) => ({
45644
+ id: h(cellValue(record, opts.pk) ?? ""),
45645
+ cells: buildCells(opts.columns, record, opts.editable, aligns)
45646
+ }));
45647
+ return {
45648
+ headers,
45649
+ rows,
45650
+ empty: opts.records.length === 0,
45651
+ colspan: opts.columns.length + 1,
45652
+ editable: opts.editable,
45653
+ readonly: !opts.editable,
45654
+ inline_script: opts.inlineScript,
45655
+ table_name: h(opts.tableName),
45656
+ table_id_attr: opts.tableId ? ` id="${h(opts.tableId)}"` : ""
45657
+ };
45658
+ }
45659
+ var init_crudTable = __esm({
45660
+ "src/crudTable.ts"() {
45661
+ "use strict";
45662
+ init_crudHelpers();
45663
+ }
45664
+ });
45665
+
45666
+ // src/crudRender.ts
45667
+ import { existsSync as existsSync28 } from "node:fs";
45668
+ import { resolve as pathResolve, join as pathJoin, dirname as dirname15 } from "node:path";
45669
+ import { fileURLToPath as fileURLToPath9 } from "node:url";
45670
+ function frondFor(dir) {
45671
+ let engine = frondCache2.get(dir);
45672
+ if (!engine) {
45673
+ setCspNonceProvider(currentCspNonce);
45674
+ engine = new Frond(dir);
45675
+ frondCache2.set(dir, engine);
45676
+ }
45677
+ return engine;
45678
+ }
45679
+ function renderCrudTemplate(name, data) {
45680
+ const templateFile = `crud/${name}.twig`;
45681
+ const userDir = pathResolve(process.cwd(), "src", "templates");
45682
+ if (existsSync28(pathJoin(userDir, templateFile))) {
45683
+ return frondFor(userDir).render(templateFile, data);
45684
+ }
45685
+ return frondFor(FRAMEWORK_TEMPLATES_DIR).render(templateFile, data);
45686
+ }
45687
+ function renderModalForm(mode, columns, pk, edit) {
45688
+ const fields = columns.map((col) => {
45689
+ const label = prettyLabel(col);
45690
+ return {
45691
+ id: `${mode}-${col}`,
45692
+ name: h(col),
45693
+ label: h(label),
45694
+ value: "",
45695
+ placeholder: h(`Enter ${label.toLowerCase()}`),
45696
+ type: "text",
45697
+ required_attr: "",
45698
+ input: true
45699
+ };
45700
+ });
45701
+ return renderCrudTemplate("form", {
45702
+ wrap: true,
45703
+ form_id_attr: ` id="form-${mode}"`,
45704
+ action: "",
45705
+ form_method: "POST",
45706
+ method_override: null,
45707
+ edit,
45708
+ mode,
45709
+ pk: h(pk),
45710
+ modal_footer: true,
45711
+ submit_button: false,
45712
+ fields
45713
+ });
45714
+ }
45715
+ function renderModals(editableColumns, pk) {
45716
+ return renderCrudTemplate("modals", {
45717
+ create_form: renderModalForm("create", editableColumns, pk, false),
45718
+ edit_form: renderModalForm("edit", editableColumns, pk, true)
45719
+ });
45720
+ }
45721
+ function generateTable(records, options = {}) {
45722
+ const rows = records ?? [];
45723
+ const tableName = String(options.tableName ?? "data");
45724
+ const primaryKey = String(options.primaryKey ?? "id");
45725
+ const editable = options.editable ?? true;
45726
+ const columns = rows.length === 0 ? [] : Object.keys(rows[0]);
45727
+ return renderCrudTemplate(
45728
+ "table",
45729
+ tableData({
45730
+ columns,
45731
+ records: rows,
45732
+ pk: primaryKey,
45733
+ tableName,
45734
+ editable,
45735
+ sortable: false,
45736
+ inlineScript: editable,
45737
+ requestPath: "",
45738
+ search: "",
45739
+ sortCol: "",
45740
+ sortDir: "asc",
45741
+ page: 1,
45742
+ limit: 10,
45743
+ tableId: `crud-${tableName}`,
45744
+ model: null
45745
+ })
45746
+ );
45747
+ }
45748
+ function buildOptions(options, selectedValue) {
45749
+ return (options ?? []).map((opt) => {
45750
+ const selected = String(opt.value) === String(selectedValue) ? " selected" : "";
45751
+ return `<option value="${h(opt.value)}"${selected}>${h(opt.label)}</option>`;
45752
+ }).join("");
45753
+ }
45754
+ function fieldInputShape(field, base) {
45755
+ const value = field.value;
45756
+ switch (field.type ?? "string") {
45757
+ case "text":
45758
+ return { ...base, textarea: true };
45759
+ case "boolean":
45760
+ return { ...base, checkbox: true, checked_attr: value ? " checked" : "" };
45761
+ case "select":
45762
+ return { ...base, select: true, options_html: buildOptions(field.options, value) };
45763
+ case "date":
45764
+ return { ...base, input: true, type: "date" };
45765
+ case "integer":
45766
+ case "number":
45767
+ case "float":
45768
+ case "decimal":
45769
+ return { ...base, input: true, type: "number" };
45770
+ default:
45771
+ return { ...base, input: true, type: "text" };
45772
+ }
45773
+ }
45774
+ function buildCustomField(field) {
45775
+ const name = String(field.name ?? "");
45776
+ const label = field.label ?? name.charAt(0).toUpperCase() + name.slice(1);
45777
+ const base = {
45778
+ id: h(name),
45779
+ name: h(name),
45780
+ label: h(String(label)),
45781
+ value: h(field.value ?? ""),
45782
+ placeholder: "",
45783
+ required_attr: field.required ? " required" : ""
45784
+ };
45785
+ return fieldInputShape(field, base);
45786
+ }
45787
+ function generateForm2(fields, options = {}) {
45788
+ const verb = String(options.method ?? "POST").toUpperCase();
45789
+ return renderCrudTemplate("form", {
45790
+ wrap: true,
45791
+ form_id_attr: "",
45792
+ action: h(options.action ?? "/"),
45793
+ form_method: h(verb),
45794
+ method_override: ["PUT", "PATCH", "DELETE"].includes(verb) ? verb : null,
45795
+ edit: false,
45796
+ modal_footer: false,
45797
+ submit_button: true,
45798
+ fields: (fields ?? []).map((f) => buildCustomField(f))
45799
+ });
45800
+ }
45801
+ var FRAMEWORK_TEMPLATES_DIR, frondCache2;
45802
+ var init_crudRender = __esm({
45803
+ "src/crudRender.ts"() {
45804
+ "use strict";
45805
+ init_engine();
45806
+ init_csp();
45807
+ init_crudHelpers();
45808
+ init_crudTable();
45809
+ FRAMEWORK_TEMPLATES_DIR = pathResolve(
45810
+ dirname15(fileURLToPath9(import.meta.url)),
45811
+ "..",
45812
+ "..",
45813
+ "core",
45814
+ "templates"
45815
+ );
45816
+ frondCache2 = /* @__PURE__ */ new Map();
45817
+ }
45818
+ });
45819
+
45820
+ // src/crud.ts
45821
+ function _resetCrudRegistrations() {
45822
+ registeredTables.clear();
45823
+ }
45824
+ function discoveredFrom(model) {
45825
+ return {
45826
+ filePath: `src/models/${model.name ?? model.tableName}.ts`,
45827
+ modelClass: model,
45828
+ definition: {
45829
+ tableName: model.tableName,
45830
+ className: model.name,
45831
+ fields: model.fields,
45832
+ fieldMapping: model.fieldMapping,
45833
+ softDelete: model.softDelete ?? false,
45834
+ tableFilter: model.tableFilter,
45835
+ dbName: model._db
45836
+ }
45837
+ };
45838
+ }
45839
+ async function registerBackend(model, options = {}) {
45840
+ const prefix = options.prefix ?? "/api";
45841
+ const key = `${prefix}::${model.tableName}`;
45842
+ if (registeredTables.has(key)) return;
45843
+ const routes = generateCrudRoutes([discoveredFrom(model)], { public: options.public === true });
45844
+ for (const route of routes) defaultRouter.addRoute(route);
45845
+ registeredTables.add(key);
45846
+ }
45847
+ async function fetchModelData(model, opts) {
45848
+ const orderBy = `${opts.sort} ${opts.sortDir.toUpperCase()}`;
45849
+ const search = opts.search.trim();
45850
+ const searchable = search === "" ? [] : searchableProps(model);
45851
+ let rows;
45852
+ if (searchable.length === 0) {
45853
+ rows = await model.all(opts.limit, opts.offset, void 0, orderBy);
45854
+ } else {
45855
+ const whereClause = searchable.map((prop) => `${columnOf(model, prop)} LIKE ?`).join(" OR ");
45856
+ const params = searchable.map(() => `%${search}%`);
45857
+ rows = await model.where(whereClause, params, opts.limit, opts.offset, void 0, orderBy);
45858
+ }
45859
+ const total = typeof rows.getTotalRecords === "function" ? rows.getTotalRecords() : rows.length;
45860
+ const records = rows.map((r) => typeof r.toDict === "function" ? r.toDict() : r);
45861
+ return [records, total];
45862
+ }
45863
+ async function fetchSqlData(sql, opts) {
45864
+ let adapter;
45865
+ try {
45866
+ adapter = getAdapter();
45867
+ } catch {
45868
+ return [[], 0];
45869
+ }
45870
+ const base = stripOrderAndLimit(sql);
45871
+ const dir = opts.sortDir.toUpperCase();
45872
+ const search = opts.search.trim();
45873
+ if (search === "") {
45874
+ const countRow2 = await adapterQuery(adapter, `SELECT COUNT(*) as cnt FROM (${base}) AS _crud_cnt`, []);
45875
+ const total2 = Number(countRow2[0]?.cnt ?? 0);
45876
+ const rows2 = await adapterFetch(adapter, `${base} ORDER BY ${opts.sort} ${dir}`, [], opts.limit, opts.offset);
45877
+ return [rows2, total2];
45878
+ }
45879
+ const columns = extractColumns(sql);
45880
+ const searchParts = columns.map((col) => `CAST(${col} AS TEXT) LIKE ?`);
45881
+ const params = columns.map(() => `%${search}%`);
45882
+ const countRow = await adapterQuery(
45883
+ adapter,
45884
+ `SELECT COUNT(*) as cnt FROM (${base}) AS _crud_cnt WHERE ${searchParts.join(" OR ")}`,
45885
+ params
45886
+ );
45887
+ const total = Number(countRow[0]?.cnt ?? 0);
45888
+ const rows = await adapterFetch(
45889
+ adapter,
45890
+ `SELECT * FROM (${base}) AS _crud_sub WHERE ${searchParts.join(" OR ")} ORDER BY ${opts.sort} ${dir}`,
45891
+ params,
45892
+ opts.limit,
45893
+ opts.offset
45894
+ );
45895
+ return [rows, total];
45896
+ }
45897
+ function parseCrudRequest(request, model, pkProp, limit) {
45898
+ const query = request && request.query || {};
45899
+ const page = Math.max(parseInt(String(query.page ?? "1"), 10) || 1, 1);
45900
+ const search = String(query.search ?? "").trim();
45901
+ const sortProp = crudSortProp(model, query.sort, pkProp);
45902
+ const sortColumn = columnOf(model, sortProp);
45903
+ const sortDir = query.sort_dir === "desc" ? "desc" : "asc";
45904
+ const requestPath = request && typeof request.path === "string" ? request.path : "/";
45905
+ return { page, search, sortProp, sortColumn, sortDir, offset: (page - 1) * limit, requestPath };
45906
+ }
45907
+ function clampLimit(raw) {
45908
+ const limit = Number(raw ?? 10);
45909
+ return !Number.isFinite(limit) || limit <= 0 ? 10 : limit;
45910
+ }
45911
+ async function toCrud(request, options) {
45912
+ const model = options.model;
45913
+ if (!model || !model.tableName || !model.fields) {
45914
+ throw new Error("Crud.toCrud requires a model (an ORM class)");
45915
+ }
45916
+ const sql = options.sql;
45917
+ const title = String(options.title ?? "CRUD");
45918
+ const prefix = String(options.prefix ?? "/api");
45919
+ const limit = clampLimit(options.limit);
45920
+ const tableName = String(model.tableName);
45921
+ const pkProp = primaryKeyProp(model);
45922
+ const columns = Object.keys(model.fields);
45923
+ await registerBackend(model, { prefix, public: options.public });
45924
+ const req2 = parseCrudRequest(request, model, pkProp, limit);
45925
+ const fetchOpts = {
45926
+ search: req2.search,
45927
+ sort: req2.sortColumn,
45928
+ sortDir: req2.sortDir,
45929
+ limit,
45930
+ offset: req2.offset
45931
+ };
45932
+ const [records, total] = sql ? await fetchSqlData(sql, fetchOpts) : await fetchModelData(model, fetchOpts);
45933
+ const totalPages = total > 0 ? Math.ceil(total / limit) : 1;
45934
+ const apiPath = `${prefix}/${tableName}`;
45935
+ const editableColumns = columns.filter((c) => c !== pkProp);
45936
+ const tableHtml = renderCrudTemplate(
45937
+ "table",
45938
+ tableData({
45939
+ columns,
45940
+ records,
45941
+ pk: pkProp,
45942
+ tableName,
45943
+ editable: false,
45944
+ sortable: true,
45945
+ inlineScript: false,
45946
+ requestPath: req2.requestPath,
45947
+ search: req2.search,
45948
+ sortCol: req2.sortProp,
45949
+ sortDir: req2.sortDir,
45950
+ page: req2.page,
45951
+ limit,
45952
+ tableId: null,
45953
+ model
45954
+ })
45955
+ );
45956
+ return renderCrudTemplate("page", {
45957
+ title: h(title),
45958
+ search: h(req2.search),
45959
+ request_path: h(req2.requestPath),
45960
+ info_count: records.length,
45961
+ info_total: total,
45962
+ info_page: req2.page,
45963
+ info_total_pages: totalPages,
45964
+ table_html: tableHtml,
45965
+ modals_html: renderModals(editableColumns, pkProp),
45966
+ show_pagination: totalPages > 1,
45967
+ controls: pageControls(req2.page, totalPages, req2.requestPath, req2.search, req2.sortProp, req2.sortDir, limit),
45968
+ config_json: jsConfig({
45969
+ apiPath,
45970
+ pk: pkProp,
45971
+ columns,
45972
+ editable: editableColumns,
45973
+ model,
45974
+ limit,
45975
+ search: req2.search,
45976
+ sortCol: req2.sortProp,
45977
+ sortDir: req2.sortDir,
45978
+ page: req2.page
45979
+ })
45980
+ });
45981
+ }
45982
+ var registeredTables, Crud, CRUD;
45983
+ var init_crud = __esm({
45984
+ "src/crud.ts"() {
45985
+ "use strict";
45986
+ init_router();
45987
+ init_autoCrud();
45988
+ init_database();
45989
+ init_crudHelpers();
45990
+ init_crudTable();
45991
+ init_crudRender();
45992
+ init_crudRender();
45993
+ init_crudHelpers();
45994
+ registeredTables = /* @__PURE__ */ new Set();
45995
+ Crud = {
45996
+ toCrud,
45997
+ registerBackend,
45998
+ generateTable,
45999
+ generateForm: generateForm2,
46000
+ stripOrderAndLimit,
46001
+ _resetCrudRegistrations
46002
+ };
46003
+ CRUD = Crud;
46004
+ }
46005
+ });
46006
+
44964
46007
  // src/queryBuilder.ts
44965
46008
  var QueryBuilder;
44966
46009
  var init_queryBuilder = __esm({
@@ -47313,7 +48356,7 @@ var init_seeder = __esm({
47313
48356
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
47314
48357
  import { randomBytes as randomBytes11 } from "node:crypto";
47315
48358
  import { mkdirSync as mkdirSync20 } from "node:fs";
47316
- import { dirname as dirname14, isAbsolute as isAbsolute6, join as join32 } from "node:path";
48359
+ import { dirname as dirname16, isAbsolute as isAbsolute6, join as join32 } from "node:path";
47317
48360
  function iso(d) {
47318
48361
  return d.toISOString();
47319
48362
  }
@@ -47584,7 +48627,7 @@ function resolveStorePath(dbPath) {
47584
48627
  let path10 = dbPath;
47585
48628
  if (!isAbsolute6(path10)) {
47586
48629
  path10 = join32(process.cwd(), path10);
47587
- mkdirSync20(dirname14(path10), { recursive: true });
48630
+ mkdirSync20(dirname16(path10), { recursive: true });
47588
48631
  }
47589
48632
  return path10;
47590
48633
  }
@@ -49334,7 +50377,9 @@ __export(index_exports, {
49334
50377
  AutoCrud: () => AutoCrud,
49335
50378
  BaseModel: () => BaseModel,
49336
50379
  CONNECT_TIMEOUT_TOLERANCE_MS: () => CONNECT_TIMEOUT_TOLERANCE_MS,
50380
+ CRUD: () => CRUD,
49337
50381
  CachedDatabaseAdapter: () => CachedDatabaseAdapter,
50382
+ Crud: () => Crud,
49338
50383
  Cursor: () => Cursor,
49339
50384
  DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
49340
50385
  DEFAULT_GRAPH_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_GRAPH_CONNECT_TIMEOUT_SECONDS,
@@ -49381,6 +50426,7 @@ __export(index_exports, {
49381
50426
  SqliteCollection: () => SqliteCollection,
49382
50427
  SqliteDatabase: () => SqliteDatabase,
49383
50428
  UnknownFieldError: () => UnknownFieldError,
50429
+ _resetCrudRegistrations: () => _resetCrudRegistrations,
49384
50430
  adapterColumns: () => adapterColumns,
49385
50431
  adapterCommit: () => adapterCommit,
49386
50432
  adapterCreateTable: () => adapterCreateTable,
@@ -49413,6 +50459,8 @@ __export(index_exports, {
49413
50459
  ensureMigrationTable: () => ensureMigrationTable,
49414
50460
  extractLastInsertId: () => extractLastInsertId,
49415
50461
  generateCrudRoutes: () => generateCrudRoutes,
50462
+ generateForm: () => generateForm2,
50463
+ generateTable: () => generateTable,
49416
50464
  getAdapter: () => getAdapter,
49417
50465
  getAppliedMigrations: () => getAppliedMigrations,
49418
50466
  getCollection: () => getCollection,
@@ -49432,10 +50480,13 @@ __export(index_exports, {
49432
50480
  realtime: () => realtime,
49433
50481
  recordMigration: () => recordMigration,
49434
50482
  redactCredentials: () => redactCredentials,
50483
+ registerBackend: () => registerBackend,
49435
50484
  removeMigrationRecord: () => removeMigrationRecord,
49436
50485
  resetDefaultStore: () => resetDefaultStore,
49437
50486
  resetRequestCaches: () => resetRequestCaches2,
49438
50487
  resolveDbPool: () => resolveDbPool,
50488
+ resolveField: () => resolveField,
50489
+ resolveFieldColumn: () => resolveFieldColumn,
49439
50490
  resolveFirebirdCharset: () => resolveFirebirdCharset,
49440
50491
  resolveGraphConnectTimeout: () => resolveGraphConnectTimeout,
49441
50492
  resolveSqlitePath: () => resolveSqlitePath,
@@ -49453,8 +50504,10 @@ __export(index_exports, {
49453
50504
  splitStatements: () => splitStatements,
49454
50505
  status: () => status,
49455
50506
  storageKey: () => storageKey,
50507
+ stripOrderAndLimit: () => stripOrderAndLimit,
49456
50508
  stripTrailingSemicolons: () => stripTrailingSemicolons,
49457
50509
  syncModels: () => syncModels,
50510
+ toCrud: () => toCrud,
49458
50511
  validate: () => validate,
49459
50512
  withConnectTimeout: () => withConnectTimeout,
49460
50513
  wrapWithCache: () => wrapWithCache
@@ -49470,6 +50523,7 @@ var init_index = __esm({
49470
50523
  init_model();
49471
50524
  init_migration();
49472
50525
  init_autoCrud();
50526
+ init_crud();
49473
50527
  init_query();
49474
50528
  init_validation();
49475
50529
  init_baseModel();
@@ -49501,7 +50555,9 @@ export {
49501
50555
  AutoCrud,
49502
50556
  BaseModel,
49503
50557
  CONNECT_TIMEOUT_TOLERANCE_MS,
50558
+ CRUD,
49504
50559
  CachedDatabaseAdapter,
50560
+ Crud,
49505
50561
  Cursor,
49506
50562
  DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
49507
50563
  DEFAULT_GRAPH_CONNECT_TIMEOUT_SECONDS,
@@ -49548,6 +50604,7 @@ export {
49548
50604
  SqliteCollection,
49549
50605
  SqliteDatabase,
49550
50606
  UnknownFieldError,
50607
+ _resetCrudRegistrations,
49551
50608
  adapterColumns,
49552
50609
  adapterCommit,
49553
50610
  adapterCreateTable,
@@ -49580,6 +50637,8 @@ export {
49580
50637
  ensureMigrationTable,
49581
50638
  extractLastInsertId,
49582
50639
  generateCrudRoutes,
50640
+ generateForm2 as generateForm,
50641
+ generateTable,
49583
50642
  getAdapter,
49584
50643
  getAppliedMigrations,
49585
50644
  getCollection,
@@ -49599,10 +50658,13 @@ export {
49599
50658
  realtime,
49600
50659
  recordMigration,
49601
50660
  redactCredentials,
50661
+ registerBackend,
49602
50662
  removeMigrationRecord,
49603
50663
  resetDefaultStore,
49604
50664
  resetRequestCaches2 as resetRequestCaches,
49605
50665
  resolveDbPool,
50666
+ resolveField,
50667
+ resolveFieldColumn,
49606
50668
  resolveFirebirdCharset,
49607
50669
  resolveGraphConnectTimeout,
49608
50670
  resolveSqlitePath,
@@ -49620,8 +50682,10 @@ export {
49620
50682
  splitStatements,
49621
50683
  status,
49622
50684
  storageKey,
50685
+ stripOrderAndLimit,
49623
50686
  stripTrailingSemicolons,
49624
50687
  syncModels,
50688
+ toCrud,
49625
50689
  validate,
49626
50690
  withConnectTimeout,
49627
50691
  wrapWithCache