tina4-nodejs 3.13.120 → 3.13.122

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.
@@ -3150,15 +3150,15 @@ function findOutsideQuotes(expr, needle) {
3150
3150
  }
3151
3151
  return -1;
3152
3152
  }
3153
- function splitOutsideQuotes(expr, sep6) {
3154
- if (!expr.includes(sep6)) return [expr];
3153
+ function splitOutsideQuotes(expr, sep7) {
3154
+ if (!expr.includes(sep7)) return [expr];
3155
3155
  const parts = [];
3156
3156
  let currentStart = 0;
3157
3157
  let inQuote = null;
3158
3158
  let depth = 0;
3159
3159
  let bracketDepth = 0;
3160
3160
  let i = 0;
3161
- const sepLen = sep6.length;
3161
+ const sepLen = sep7.length;
3162
3162
  const lastStart = expr.length - sepLen;
3163
3163
  while (i <= lastStart) {
3164
3164
  const ch = expr[i];
@@ -3179,7 +3179,7 @@ function splitOutsideQuotes(expr, sep6) {
3179
3179
  else if (ch === ")") depth--;
3180
3180
  else if (ch === "[") bracketDepth++;
3181
3181
  else if (ch === "]") bracketDepth--;
3182
- if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep6, i)) {
3182
+ if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep7, i)) {
3183
3183
  parts.push(expr.slice(currentStart, i));
3184
3184
  i += sepLen;
3185
3185
  currentStart = i;
@@ -3955,8 +3955,8 @@ var init_engine = __esm({
3955
3955
  },
3956
3956
  first: (v) => Array.isArray(v) ? v[0] ?? null : null,
3957
3957
  last: (v) => Array.isArray(v) ? v[v.length - 1] ?? null : null,
3958
- join: (v, sep6) => Array.isArray(v) ? v.map(String).join(sep6 !== void 0 ? String(sep6) : ", ") : String(v),
3959
- split: (v, sep6) => String(v).split(sep6 !== void 0 ? String(sep6) : " "),
3958
+ join: (v, sep7) => Array.isArray(v) ? v.map(String).join(sep7 !== void 0 ? String(sep7) : ", ") : String(v),
3959
+ split: (v, sep7) => String(v).split(sep7 !== void 0 ? String(sep7) : " "),
3960
3960
  replace: (v, from, to) => {
3961
3961
  const s = String(v);
3962
3962
  if (from !== void 0 && typeof from === "object" && from !== null && !Array.isArray(from)) {
@@ -6657,6 +6657,9 @@ var init_middleware = __esm({
6657
6657
  `max-age=${hsts}; includeSubDomains`
6658
6658
  );
6659
6659
  }
6660
+ if (process.env.TINA4_CSP === void 0) {
6661
+ _SecurityHeadersMiddleware.warnCspDefaultOnce();
6662
+ }
6660
6663
  res.header(
6661
6664
  "Content-Security-Policy",
6662
6665
  process.env.TINA4_CSP ?? "default-src 'self'"
@@ -6672,6 +6675,30 @@ var init_middleware = __esm({
6672
6675
  );
6673
6676
  return [req2, res];
6674
6677
  }
6678
+ /** Warn-once ledger for the default-CSP heads-up (per process). */
6679
+ static cspDefaultWarned = false;
6680
+ /**
6681
+ * Warn once per process that the default CSP is in force (TINA4_CSP unset).
6682
+ *
6683
+ * Secure-by-default keeps `default-src 'self'` (SECHDR-DEC-01), but that
6684
+ * default is invisible: it blocks runtime-injected inline styles, cross-origin
6685
+ * fonts/scripts/CDNs, `data:` URIs, and cross-origin WebSocket/XHR (a separate
6686
+ * API or LiveKit host) — and the failure surfaces only in the browser at
6687
+ * runtime, long after a deploy has gone green. So the framework says so once,
6688
+ * naming the escape hatch. It NEVER fails the boot or a request — logging a
6689
+ * heads-up must not be the reason the server or a request dies. Fires only when
6690
+ * TINA4_CSP is ABSENT; setting it (even to empty) is an explicit opt-in.
6691
+ */
6692
+ static warnCspDefaultOnce() {
6693
+ if (_SecurityHeadersMiddleware.cspDefaultWarned) return;
6694
+ _SecurityHeadersMiddleware.cspDefaultWarned = true;
6695
+ const message = `TINA4_CSP is not set, so Tina4 is serving the default Content-Security-Policy "default-src 'self'" on every response. That default blocks runtime-injected inline styles, cross-origin fonts/scripts/CDNs, data: URIs, and cross-origin WebSocket/XHR (e.g. a separate API or LiveKit host). If your app uses any of these, set TINA4_CSP to a policy that allows them (see https://tina4.com); to silence this notice without changing behaviour, set TINA4_CSP="default-src 'self'".`;
6696
+ try {
6697
+ Log.warning(message);
6698
+ } catch {
6699
+ console.warn(message);
6700
+ }
6701
+ }
6675
6702
  /**
6676
6703
  * True when the client request is HTTPS. Proxy-aware and byte-parity with
6677
6704
  * Python (request.is_secure_scheme), PHP (Request::isSecureScheme) and Ruby
@@ -7161,7 +7188,7 @@ ${s}\r
7161
7188
  connect() {
7162
7189
  if (this.connected) return Promise.resolve();
7163
7190
  if (this.connecting) return this.connecting;
7164
- this.connecting = new Promise((resolve20, reject) => {
7191
+ this.connecting = new Promise((resolve21, reject) => {
7165
7192
  const sock = net.createConnection({ host: this.host, port: this.port });
7166
7193
  sock.setNoDelay(true);
7167
7194
  const onError = (err) => {
@@ -7198,7 +7225,7 @@ ${s}\r
7198
7225
  sock.on("error", (e) => {
7199
7226
  this.brokenError = e;
7200
7227
  });
7201
- resolve20();
7228
+ resolve21();
7202
7229
  } catch (e) {
7203
7230
  onError(e);
7204
7231
  }
@@ -7261,12 +7288,12 @@ ${s}\r
7261
7288
  }
7262
7289
  /** Send one command and await its reply (assumes socket is up). */
7263
7290
  raw(args) {
7264
- return new Promise((resolve20, reject) => {
7291
+ return new Promise((resolve21, reject) => {
7265
7292
  if (!this.sock || this.sock.destroyed) {
7266
7293
  reject(this.brokenError ?? new Error("redis socket not connected"));
7267
7294
  return;
7268
7295
  }
7269
- this.waiters.push({ resolve: resolve20, reject });
7296
+ this.waiters.push({ resolve: resolve21, reject });
7270
7297
  this.sock.write(_RespClient.encode(args));
7271
7298
  });
7272
7299
  }
@@ -7608,7 +7635,7 @@ ${s}\r
7608
7635
  connect() {
7609
7636
  if (this.connected) return Promise.resolve();
7610
7637
  if (this.connecting) return this.connecting;
7611
- this.connecting = new Promise((resolve20, reject) => {
7638
+ this.connecting = new Promise((resolve21, reject) => {
7612
7639
  const sock = net.createConnection({ host: this.host, port: this.port });
7613
7640
  sock.setNoDelay(true);
7614
7641
  sock.once("error", (err) => {
@@ -7629,7 +7656,7 @@ ${s}\r
7629
7656
  p.resolve(this.buffer.toString("utf-8"));
7630
7657
  }
7631
7658
  });
7632
- resolve20();
7659
+ resolve21();
7633
7660
  });
7634
7661
  });
7635
7662
  return this.connecting;
@@ -7662,13 +7689,13 @@ ${s}\r
7662
7689
  async send(payload, terminator) {
7663
7690
  await this.connect();
7664
7691
  if (!this.sock || this.sock.destroyed) return "";
7665
- return new Promise((resolve20) => {
7692
+ return new Promise((resolve21) => {
7666
7693
  this.buffer = Buffer.alloc(0);
7667
- this.pending = { terminator, resolve: resolve20 };
7694
+ this.pending = { terminator, resolve: resolve21 };
7668
7695
  const timer = setTimeout(() => {
7669
- if (this.pending && this.pending.resolve === resolve20) {
7696
+ if (this.pending && this.pending.resolve === resolve21) {
7670
7697
  this.pending = null;
7671
- resolve20(this.buffer.toString("utf-8"));
7698
+ resolve21(this.buffer.toString("utf-8"));
7672
7699
  }
7673
7700
  }, 4e3);
7674
7701
  if (timer.unref) timer.unref();
@@ -9434,7 +9461,7 @@ async function parseBody(req2) {
9434
9461
  }
9435
9462
  const contentType = req2.headers["content-type"] ?? "";
9436
9463
  const chunks = [];
9437
- await new Promise((resolve20, reject) => {
9464
+ await new Promise((resolve21, reject) => {
9438
9465
  let received = 0;
9439
9466
  let refused = false;
9440
9467
  req2.on("data", (chunk) => {
@@ -9449,7 +9476,7 @@ async function parseBody(req2) {
9449
9476
  chunks.push(chunk);
9450
9477
  });
9451
9478
  req2.on("end", () => {
9452
- if (!refused) resolve20();
9479
+ if (!refused) resolve21();
9453
9480
  });
9454
9481
  req2.on("error", reject);
9455
9482
  });
@@ -12682,6 +12709,2038 @@ var init_version = __esm({
12682
12709
  }
12683
12710
  });
12684
12711
 
12712
+ // ../cli/src/commands/generate.ts
12713
+ var generate_exports = {};
12714
+ __export(generate_exports, {
12715
+ DEFAULT_FIELDS: () => DEFAULT_FIELDS,
12716
+ GENERATORS: () => GENERATORS,
12717
+ RESOLUTION_ENVELOPE_VERSION: () => RESOLUTION_ENVELOPE_VERSION,
12718
+ SQL_RESERVED_TABLE_NAMES: () => SQL_RESERVED_TABLE_NAMES,
12719
+ aiFill: () => aiFill,
12720
+ currentResolution: () => currentResolution,
12721
+ extend: () => extend,
12722
+ fieldsOrDefault: () => fieldsOrDefault,
12723
+ generate: () => generate,
12724
+ generateMigration: () => generateMigration,
12725
+ generateProgrammatic: () => generateProgrammatic,
12726
+ parseCliArgs: () => parseCliArgs,
12727
+ parseEvery: () => parseEvery,
12728
+ parseFields: () => parseFields,
12729
+ pluralizeReserved: () => pluralizeReserved,
12730
+ toPascal: () => toPascal,
12731
+ toSnake: () => toSnake,
12732
+ toTableName: () => toTableName
12733
+ });
12734
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "node:fs";
12735
+ import { join as join15, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
12736
+ function ensureDir(dir) {
12737
+ if (__resolution.dryRun) return;
12738
+ if (!existsSync13(dir)) {
12739
+ mkdirSync8(dir, { recursive: true });
12740
+ }
12741
+ }
12742
+ function writeFileSafe(path8, content) {
12743
+ captureEditHints(path8, content);
12744
+ if (__resolution.dryRun) {
12745
+ return;
12746
+ }
12747
+ if (existsSync13(path8)) {
12748
+ if (!__resolution.jsonMode) console.log(` File already exists: ${path8}`);
12749
+ return;
12750
+ }
12751
+ writeFileSync7(path8, content, "utf-8");
12752
+ __resolution.actionsTaken.push(`wrote ${path8}`);
12753
+ if (!__resolution.jsonMode) console.log(` Created ${path8}`);
12754
+ }
12755
+ function toSnake(name) {
12756
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
12757
+ }
12758
+ function pluralizeReserved(name) {
12759
+ if (name.endsWith("y") && !/[aeiouy]y$/i.test(name)) return name.slice(0, -1) + "ies";
12760
+ if (/(s|x|z|ch|sh)$/.test(name)) return name + "es";
12761
+ return name + "s";
12762
+ }
12763
+ function toTableName(name) {
12764
+ const raw = toSnake(name);
12765
+ if (SQL_RESERVED_TABLE_NAMES.has(raw)) {
12766
+ const safe = pluralizeReserved(raw);
12767
+ recordTransformation({
12768
+ kind: "reserved_word_pluralize",
12769
+ from: raw,
12770
+ to: safe,
12771
+ reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
12772
+ override: `--table ${raw} --quote (requires quoted-identifier mode, not yet implemented)`
12773
+ });
12774
+ return safe;
12775
+ }
12776
+ return raw;
12777
+ }
12778
+ function resetResolution(target, input, opts) {
12779
+ __resolution.target = target;
12780
+ __resolution.input = input;
12781
+ __resolution.body = { transformations: [] };
12782
+ __resolution.actionsTaken = [];
12783
+ __resolution.dryRun = opts.dryRun;
12784
+ __resolution.jsonMode = opts.jsonMode;
12785
+ }
12786
+ function recordTransformation(t) {
12787
+ __resolution.body.transformations.push(t);
12788
+ }
12789
+ function currentResolution() {
12790
+ const body = {
12791
+ ...__resolution.body,
12792
+ transformations: [...__resolution.body.transformations]
12793
+ };
12794
+ if (__resolution.body.edit_hints) {
12795
+ body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
12796
+ }
12797
+ if (__resolution.body.next) {
12798
+ body.next = [...__resolution.body.next];
12799
+ }
12800
+ if (__resolution.body.test_paths) {
12801
+ body.test_paths = [...__resolution.body.test_paths];
12802
+ }
12803
+ if (__resolution.body.routes) {
12804
+ body.routes = [...__resolution.body.routes];
12805
+ }
12806
+ return {
12807
+ command: "generate",
12808
+ target: __resolution.target,
12809
+ input: { ...__resolution.input },
12810
+ resolution: body,
12811
+ actions_taken: [...__resolution.actionsTaken],
12812
+ dry_run: __resolution.dryRun
12813
+ };
12814
+ }
12815
+ function setResolutionField(key, value) {
12816
+ __resolution.body[key] = value;
12817
+ }
12818
+ function pushRoute(routePattern) {
12819
+ if (!__resolution.body.routes) __resolution.body.routes = [];
12820
+ __resolution.body.routes.push(routePattern);
12821
+ }
12822
+ function pushTestPath(path8) {
12823
+ if (!__resolution.body.test_paths) __resolution.body.test_paths = [];
12824
+ __resolution.body.test_paths.push(path8);
12825
+ }
12826
+ function pushEditHint(hint) {
12827
+ if (!__resolution.body.edit_hints) __resolution.body.edit_hints = [];
12828
+ __resolution.body.edit_hints.push(hint);
12829
+ }
12830
+ function setNextSteps(steps) {
12831
+ if (steps.length === 0) return;
12832
+ __resolution.body.next = [...steps];
12833
+ }
12834
+ function toRelPath(absPath) {
12835
+ const cwd = process.cwd();
12836
+ const rel = relative2(cwd, absPath);
12837
+ if (!rel) return absPath;
12838
+ return sep3 === "/" ? rel : rel.split(sep3).join("/");
12839
+ }
12840
+ function captureEditHints(absPath, content) {
12841
+ if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
12842
+ const relPath = toRelPath(absPath);
12843
+ const lines = content.split("\n");
12844
+ for (let i = 0; i < lines.length; i++) {
12845
+ const match = TINA4_EDIT_MARKER.exec(lines[i]);
12846
+ if (match) {
12847
+ pushEditHint({ file: relPath, line: i + 1, label: match[1].trim() });
12848
+ }
12849
+ }
12850
+ }
12851
+ function printResolution() {
12852
+ if (__resolution.jsonMode) {
12853
+ process.stdout.write(JSON.stringify(currentResolution(), null, 2) + "\n");
12854
+ return;
12855
+ }
12856
+ const b = __resolution.body;
12857
+ const lines = [];
12858
+ lines.push("");
12859
+ lines.push(`Generated ${__resolution.target} ${__resolution.input.name}`);
12860
+ if (b.class_name || b.file_path) {
12861
+ const where = b.file_path ? ` (in ${b.file_path})` : "";
12862
+ lines.push(` class ${b.class_name ?? __resolution.input.name}${where}`);
12863
+ }
12864
+ if (b.table_name) {
12865
+ const t = b.transformations.find((x) => x.kind === "reserved_word_pluralize");
12866
+ const note = t ? ` (auto-pluralized: '${t.from}' is a SQL reserved word)` : "";
12867
+ lines.push(` table ${b.table_name}${note}`);
12868
+ }
12869
+ if (b.routes && b.routes.length) {
12870
+ lines.push(` routes ${b.routes.join(", ")}`);
12871
+ }
12872
+ if (b.migration_path) {
12873
+ lines.push(` migration ${b.migration_path}`);
12874
+ }
12875
+ const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
12876
+ if (reserved && reserved.from && reserved.override) {
12877
+ lines.push("");
12878
+ lines.push(` To keep the raw name '${reserved.from}' as the table:`);
12879
+ lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
12880
+ }
12881
+ if (b.test_paths && b.test_paths.length > 0) {
12882
+ lines.push("");
12883
+ lines.push(" Tests:");
12884
+ for (const testPath of b.test_paths) lines.push(` ${testPath}`);
12885
+ }
12886
+ if (b.edit_hints && b.edit_hints.length > 0) {
12887
+ lines.push("");
12888
+ lines.push(" Edit these lines:");
12889
+ for (const hint of b.edit_hints) {
12890
+ lines.push(` ${hint.file}:${hint.line} ${hint.label}`);
12891
+ }
12892
+ }
12893
+ if (b.next && b.next.length > 0) {
12894
+ lines.push("");
12895
+ lines.push(" Next:");
12896
+ for (const step of b.next) lines.push(` ${step}`);
12897
+ }
12898
+ lines.push("");
12899
+ process.stderr.write(lines.join("\n"));
12900
+ }
12901
+ function toPlural(name) {
12902
+ const lower = name.toLowerCase();
12903
+ if (lower.endsWith("s")) return lower;
12904
+ if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
12905
+ return lower + "s";
12906
+ }
12907
+ function toCamel(name) {
12908
+ return name.charAt(0).toLowerCase() + name.slice(1);
12909
+ }
12910
+ function toPascal(name) {
12911
+ return name.split(/[^0-9a-zA-Z]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
12912
+ }
12913
+ function parseFields(fieldsStr) {
12914
+ if (!fieldsStr || !fieldsStr.trim()) return [];
12915
+ const result = [];
12916
+ for (const part of fieldsStr.split(",")) {
12917
+ const trimmed = part.trim();
12918
+ if (trimmed.includes(":")) {
12919
+ const [fname, ftype] = trimmed.split(":", 2);
12920
+ if (fname.trim()) result.push([fname.trim(), ftype.trim().toLowerCase()]);
12921
+ } else if (trimmed) {
12922
+ result.push([trimmed, "string"]);
12923
+ }
12924
+ }
12925
+ return result;
12926
+ }
12927
+ function fieldsOrDefault(fieldsStr) {
12928
+ const parsed = parseFields(fieldsStr);
12929
+ return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
12930
+ }
12931
+ function parseCliArgs(args) {
12932
+ const booleanFlags = /* @__PURE__ */ new Set([
12933
+ "no-browser",
12934
+ "no-reload",
12935
+ "production",
12936
+ "managed",
12937
+ "all",
12938
+ "clear",
12939
+ "public",
12940
+ "no-migration",
12941
+ // Suppress the co-emitted migration test (used by the migrate:create
12942
+ // delegation — a plain migrate:create is "just a migration, no test",
12943
+ // matching its pre-3.13.121 UX now that it routes through generate migration).
12944
+ "no-test",
12945
+ // Resolution transparency (Feature B, 3.13.117): both accept NO value.
12946
+ "json",
12947
+ "dry-run"
12948
+ ]);
12949
+ const flags = {};
12950
+ const positional = [];
12951
+ let i = 0;
12952
+ while (i < args.length) {
12953
+ if (args[i].startsWith("--")) {
12954
+ const key = args[i].slice(2);
12955
+ if (booleanFlags.has(key)) {
12956
+ flags[key] = true;
12957
+ i += 1;
12958
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
12959
+ flags[key] = args[i + 1];
12960
+ i += 2;
12961
+ } else {
12962
+ flags[key] = true;
12963
+ i += 1;
12964
+ }
12965
+ } else {
12966
+ positional.push(args[i]);
12967
+ i += 1;
12968
+ }
12969
+ }
12970
+ return { flags, positional };
12971
+ }
12972
+ function parseEvery(every) {
12973
+ if (!every || every === true) return 60;
12974
+ const s = String(every).trim().toLowerCase();
12975
+ const units = { s: 1, m: 60, h: 3600, d: 86400 };
12976
+ const unit = s.slice(-1);
12977
+ if (unit in units) {
12978
+ const n2 = parseFloat(s.slice(0, -1));
12979
+ return Number.isFinite(n2) ? Math.max(1, Math.round(n2 * units[unit])) : 60;
12980
+ }
12981
+ const n = parseFloat(s);
12982
+ return Number.isFinite(n) ? Math.max(1, Math.round(n)) : 60;
12983
+ }
12984
+ function aiFill(fn, spec, indent = " ") {
12985
+ const rule = (label) => "\u2500".repeat(Math.max(4, 46 - label.length));
12986
+ const lines = [`${indent}// \u2500\u2500\u2500 AI-FILL: ${fn} ${rule(fn)}`];
12987
+ lines.push(`${indent}// Intent: ${spec.intent}`);
12988
+ if (spec.given) lines.push(`${indent}// Given: ${spec.given}`);
12989
+ lines.push(`${indent}// Use: ${spec.use}`);
12990
+ if (spec.ret) lines.push(`${indent}// Return: ${spec.ret}`);
12991
+ lines.push(`${indent}// Ground: ${spec.ground}`);
12992
+ lines.push(`${indent}throw new Error(${JSON.stringify(spec.raise)}); // remove when implemented`);
12993
+ lines.push(`${indent}// ${"\u2500".repeat(52)}`);
12994
+ return lines.join("\n") + "\n";
12995
+ }
12996
+ function extend(note, hint = "", indent = " ") {
12997
+ let out = `${indent}// \u2500\u2500\u2500 EXTEND: ${note} ${"\u2500".repeat(Math.max(4, 46 - note.length))}
12998
+ `;
12999
+ if (hint) out += `${indent}// ${hint}
13000
+ `;
13001
+ return out;
13002
+ }
13003
+ function timestamp() {
13004
+ const now = /* @__PURE__ */ new Date();
13005
+ return now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, "0") + String(now.getDate()).padStart(2, "0") + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
13006
+ }
13007
+ function isoNow() {
13008
+ return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
13009
+ }
13010
+ async function generate(what, name, extraArgs = []) {
13011
+ if (!what) {
13012
+ console.error(" Usage: tina4nodejs generate <what> <name> [options]");
13013
+ console.error(` Generators: ${GENERATOR_LIST}`);
13014
+ console.error(' Options: --fields "name:string,price:float" --model ModelName');
13015
+ console.error(" --public open a route's writes (default: secure)");
13016
+ console.error(' --every 5m | --cron "\u2026" service schedule');
13017
+ console.error(" --json emit machine-readable resolution envelope on stdout");
13018
+ console.error(" --dry-run report resolution without writing any files");
13019
+ process.exit(1);
13020
+ }
13021
+ const noNameGenerators = /* @__PURE__ */ new Set(["auth"]);
13022
+ if (noNameGenerators.has(what) && name.startsWith("--")) {
13023
+ extraArgs = [name, ...extraArgs];
13024
+ name = "";
13025
+ }
13026
+ if (!noNameGenerators.has(what) && !name) {
13027
+ console.error(` Usage: tina4nodejs generate ${what} <name> [options]`);
13028
+ process.exit(1);
13029
+ }
13030
+ const { flags } = parseCliArgs(extraArgs);
13031
+ const jsonMode = Boolean(flags.json);
13032
+ const dryRun = Boolean(flags["dry-run"]);
13033
+ resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode });
13034
+ const spec = GENERATORS[what];
13035
+ if (spec) {
13036
+ spec.handler(name, flags);
13037
+ } else {
13038
+ console.error(` Unknown generator: ${what}`);
13039
+ console.error(` Available: ${GENERATOR_LIST}`);
13040
+ process.exit(1);
13041
+ }
13042
+ const nextFn = NEXT_STEPS[what];
13043
+ if (nextFn) {
13044
+ const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
13045
+ setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
13046
+ }
13047
+ printResolution();
13048
+ }
13049
+ async function generateProgrammatic(what, name, extraArgs = []) {
13050
+ const spec = GENERATORS[what];
13051
+ if (!spec) throw new Error(`Unknown generator: ${what} (available: ${GENERATOR_LIST})`);
13052
+ const { flags } = parseCliArgs(extraArgs);
13053
+ const dryRun = Boolean(flags["dry-run"]);
13054
+ resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode: true });
13055
+ spec.handler(name, flags);
13056
+ const nextFn = NEXT_STEPS[what];
13057
+ if (nextFn) {
13058
+ const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
13059
+ setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
13060
+ }
13061
+ return currentResolution();
13062
+ }
13063
+ function generateModel(name, flags, emitTest = true) {
13064
+ const fields = fieldsOrDefault(flags.fields || "");
13065
+ const table2 = toTableName(name);
13066
+ const dir = resolve6("src/models");
13067
+ ensureDir(dir);
13068
+ const path8 = join15(dir, `${name}.ts`);
13069
+ setResolutionField("class_name", name);
13070
+ setResolutionField("table_name", table2);
13071
+ setResolutionField("file_path", `src/models/${name}.ts`);
13072
+ pushTestPath(`tests/${table2}_model.test.ts`);
13073
+ const fieldLines = [
13074
+ ` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`,
13075
+ ` // tina4:edit add or change fields for this model (string,int,float,bool,text,datetime)`
13076
+ ];
13077
+ for (const [fname, ftype] of fields) {
13078
+ const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
13079
+ fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
13080
+ }
13081
+ fieldLines.push(` created_at: { type: "datetime" as const },`);
13082
+ const content = `import { BaseModel } from "tina4-nodejs/orm";
13083
+
13084
+ export default class ${name} extends BaseModel {
13085
+ static tableName = "${table2}";
13086
+ static fields = {
13087
+ ${fieldLines.join("\n")}
13088
+ };
13089
+ }
13090
+ `;
13091
+ writeFileSafe(path8, content);
13092
+ if (!flags["no-migration"]) {
13093
+ generateMigration(`create_${table2}`, flags, fields, table2, false);
13094
+ }
13095
+ if (emitTest) emitModelTest(name, table2, fields);
13096
+ }
13097
+ function secureOptOut(isPublic) {
13098
+ return isPublic ? `export const secure = false;
13099
+
13100
+ ` : "";
13101
+ }
13102
+ function generateRoute(name, flags, emitTest = true) {
13103
+ const routePath = name.replace(/^\//, "");
13104
+ const singular = routePath.endsWith("s") ? routePath.slice(0, -1) : routePath;
13105
+ const model = flags.model;
13106
+ const isPublic = Boolean(flags.public);
13107
+ const base = resolve6("src/routes/api", routePath);
13108
+ const idDir = join15(base, "[id]");
13109
+ ensureDir(base);
13110
+ ensureDir(idDir);
13111
+ pushRoute(`/api/${routePath}`);
13112
+ pushRoute(`/api/${routePath}/{id}`);
13113
+ if (__resolution.target === "route") {
13114
+ setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
13115
+ }
13116
+ const table2 = model ? toTableName(model) : "";
13117
+ const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
13118
+ ` : "";
13119
+ const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
13120
+ ` : "";
13121
+ const writeDoc = isPublic ? "Public (--public): no token required." : "Secure by default: requires a Bearer token (use --public to open).";
13122
+ if (model) {
13123
+ writeFileSafe(
13124
+ join15(base, "get.ts"),
13125
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13126
+ ${modelImportBase}
13127
+ export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
13128
+
13129
+ export default async function (req: Tina4Request, res: Tina4Response) {
13130
+ // tina4:edit tune pagination defaults or add filter/sort parsing here
13131
+ const page = parseInt(req.query.page as string) || 1;
13132
+ const limit = parseInt(req.query.limit as string) || 20;
13133
+ const offset = (page - 1) * limit;
13134
+ const rows = await ${model}.select("SELECT * FROM ${table2} LIMIT ? OFFSET ?", [limit, offset]);
13135
+ res.json({ data: rows.map((r) => r.toObject()), page, limit });
13136
+ }
13137
+ `
13138
+ );
13139
+ } else {
13140
+ writeFileSafe(
13141
+ join15(base, "get.ts"),
13142
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13143
+
13144
+ export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
13145
+
13146
+ export default async function (req: Tina4Request, res: Tina4Response) {
13147
+ ${aiFill(`list_${routePath}`, {
13148
+ intent: `return the ${routePath} collection (add pagination if it grows)`,
13149
+ given: "req.query -> filters/paging",
13150
+ use: `Model.select("SELECT \u2026 LIMIT ? OFFSET ?", [limit, offset]) then r.toObject()`,
13151
+ ret: "res.json({ data: rows })",
13152
+ ground: `tina4_context("list ORM records with pagination", "nodejs") \xB7 skill tina4-developer-nodejs`,
13153
+ raise: `${routePath} list not implemented`
13154
+ })}}
13155
+ `
13156
+ );
13157
+ }
13158
+ if (model) {
13159
+ writeFileSafe(
13160
+ join15(base, "post.ts"),
13161
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13162
+ ${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
13163
+
13164
+ // ${writeDoc}
13165
+ export default async function (req: Tina4Request, res: Tina4Response) {
13166
+ // tina4:edit validate the body before persist (Validator or hand-checks)
13167
+ ${extend(
13168
+ "validate / business rules before persist",
13169
+ `e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`
13170
+ )} const item = new ${model}(req.body as Record<string, unknown>);
13171
+ // save() returns false on failure rather than throwing - check it, or a failed
13172
+ // write is reported to the client as a 201 carrying unsaved data.
13173
+ if ((await item.save()) === false) {
13174
+ res.json({ error: "Could not create ${singular}" }, 400);
13175
+ return;
13176
+ }
13177
+ res.json({ data: item.toObject() }, 201);
13178
+ }
13179
+ `
13180
+ );
13181
+ } else {
13182
+ writeFileSafe(
13183
+ join15(base, "post.ts"),
13184
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13185
+
13186
+ ${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
13187
+
13188
+ // ${writeDoc}
13189
+ export default async function (req: Tina4Request, res: Tina4Response) {
13190
+ // tina4:edit fill the create handler (see AI-FILL fill-spec below)
13191
+ ${aiFill(`create_${singular}`, {
13192
+ intent: `validate the body and persist a new ${singular}`,
13193
+ given: "req.body -> the posted fields",
13194
+ use: "new Model(req.body).save() then item.toObject() (import your model)",
13195
+ ret: "res.json({ data: item }, 201)",
13196
+ ground: `tina4_context("create ORM record and return 201", "nodejs") \xB7 skill tina4-developer-nodejs`,
13197
+ raise: `create ${singular} not implemented`
13198
+ })}}
13199
+ `
13200
+ );
13201
+ }
13202
+ if (model) {
13203
+ writeFileSafe(
13204
+ join15(idDir, "get.ts"),
13205
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13206
+ ${modelImportId}
13207
+ export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
13208
+
13209
+ export default async function (req: Tina4Request, res: Tina4Response) {
13210
+ const { id } = req.params;
13211
+ const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
13212
+ if (!item) {
13213
+ res.json({ error: "Not found" }, 404);
13214
+ return;
13215
+ }
13216
+ res.json({ data: item.toObject() });
13217
+ }
13218
+ `
13219
+ );
13220
+ } else {
13221
+ writeFileSafe(
13222
+ join15(idDir, "get.ts"),
13223
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13224
+
13225
+ export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
13226
+
13227
+ export default async function (req: Tina4Request, res: Tina4Response) {
13228
+ ${aiFill(`get_${singular}`, {
13229
+ intent: `fetch one ${singular} by id`,
13230
+ given: "req.params.id -> the record id",
13231
+ use: `Model.selectOne("SELECT \u2026 WHERE id = ?", [req.params.id])`,
13232
+ ret: "res.json({ data: item }) or res.json({ error: 'Not found' }, 404)",
13233
+ ground: `tina4_context("find ORM record by id", "nodejs") \xB7 skill tina4-developer-nodejs`,
13234
+ raise: `get ${singular} not implemented`
13235
+ })}}
13236
+ `
13237
+ );
13238
+ }
13239
+ if (model) {
13240
+ writeFileSafe(
13241
+ join15(idDir, "put.ts"),
13242
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13243
+ ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
13244
+
13245
+ // ${writeDoc}
13246
+ export default async function (req: Tina4Request, res: Tina4Response) {
13247
+ const { id } = req.params;
13248
+ const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
13249
+ if (!item) {
13250
+ res.json({ error: "Not found" }, 404);
13251
+ return;
13252
+ }
13253
+ // tina4:edit guard which fields may be updated and who may update this row
13254
+ ${extend(
13255
+ "guard which fields / who may update",
13256
+ `e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`
13257
+ )} Object.assign(item, req.body as Record<string, unknown>);
13258
+ // save() returns false on failure rather than throwing - check it, or a failed
13259
+ // write is reported to the client as a 200 carrying unsaved data.
13260
+ if ((await item.save()) === false) {
13261
+ res.json({ error: "Could not update ${singular}" }, 400);
13262
+ return;
13263
+ }
13264
+ res.json({ data: item.toObject() });
13265
+ }
13266
+ `
13267
+ );
13268
+ } else {
13269
+ writeFileSafe(
13270
+ join15(idDir, "put.ts"),
13271
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13272
+
13273
+ ${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
13274
+
13275
+ // ${writeDoc}
13276
+ export default async function (req: Tina4Request, res: Tina4Response) {
13277
+ // tina4:edit fill the update handler (see AI-FILL fill-spec below)
13278
+ ${aiFill(`update_${singular}`, {
13279
+ intent: `load, mutate and save an existing ${singular}`,
13280
+ given: "req.params.id -> id; req.body -> changed fields",
13281
+ use: "Model.selectOne(\u2026) then Object.assign(item, req.body) then item.save()",
13282
+ ret: "res.json({ data: item }) or 404",
13283
+ ground: `tina4_context("update ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
13284
+ raise: `update ${singular} not implemented`
13285
+ })}}
13286
+ `
13287
+ );
13288
+ }
13289
+ if (model) {
13290
+ writeFileSafe(
13291
+ join15(idDir, "delete.ts"),
13292
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13293
+ ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
13294
+
13295
+ // ${writeDoc}
13296
+ export default async function (req: Tina4Request, res: Tina4Response) {
13297
+ const { id } = req.params;
13298
+ const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
13299
+ if (!item) {
13300
+ res.json({ error: "Not found" }, 404);
13301
+ return;
13302
+ }
13303
+ await item.delete();
13304
+ res.json({ message: "deleted", id });
13305
+ }
13306
+ `
13307
+ );
13308
+ } else {
13309
+ writeFileSafe(
13310
+ join15(idDir, "delete.ts"),
13311
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13312
+
13313
+ ${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
13314
+
13315
+ // ${writeDoc}
13316
+ export default async function (req: Tina4Request, res: Tina4Response) {
13317
+ ${aiFill(`delete_${singular}`, {
13318
+ intent: `delete a ${singular} by id`,
13319
+ given: "req.params.id -> id",
13320
+ use: "Model.selectOne(\u2026) then item.delete()",
13321
+ ret: "res.json({ message: 'deleted', id }) or 404",
13322
+ ground: `tina4_context("delete ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
13323
+ raise: `delete ${singular} not implemented`
13324
+ })}}
13325
+ `
13326
+ );
13327
+ }
13328
+ if (emitTest) {
13329
+ if (model) {
13330
+ generateTest(routePath, { model, "secure-writes": true, public: isPublic });
13331
+ } else {
13332
+ emitRouteStubTest(routePath);
13333
+ }
13334
+ }
13335
+ }
13336
+ function generateCrud(name, flags) {
13337
+ const table2 = toTableName(name);
13338
+ const routeName = toPlural(table2);
13339
+ const isPublic = Boolean(flags.public);
13340
+ if (!__resolution.jsonMode) console.log(`
13341
+ Generating CRUD for ${name}...
13342
+ `);
13343
+ generateModel(name, flags, false);
13344
+ generateRoute(routeName, { ...flags, model: name }, false);
13345
+ generateForm(name, flags);
13346
+ generateView(name, flags);
13347
+ generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
13348
+ if (!__resolution.jsonMode) {
13349
+ console.log(`
13350
+ CRUD generation complete for ${name}.`);
13351
+ console.log(" Run: tina4nodejs migrate");
13352
+ console.log(" Visit: /swagger to see the API docs");
13353
+ }
13354
+ }
13355
+ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
13356
+ const ts = timestamp();
13357
+ const dir = resolve6("migrations");
13358
+ ensureDir(dir);
13359
+ let table2;
13360
+ if (tableOverride) {
13361
+ table2 = tableOverride;
13362
+ } else {
13363
+ const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
13364
+ table2 = toTableName(raw);
13365
+ }
13366
+ if (__resolution.target === "migration") {
13367
+ setResolutionField("table_name", table2);
13368
+ }
13369
+ const fields = fieldsOverride || parseFields(flags.fields || "");
13370
+ const isCreate = name.startsWith("create_") || fieldsOverride !== void 0;
13371
+ const fileName = `${ts}_${name}.sql`;
13372
+ const path8 = join15(dir, fileName);
13373
+ setResolutionField("migration_path", `migrations/${fileName}`);
13374
+ if (__resolution.target === "migration") {
13375
+ setResolutionField("file_path", `migrations/${fileName}`);
13376
+ pushTestPath(`tests/${table2}_migration.test.ts`);
13377
+ }
13378
+ let upSql;
13379
+ let downSql;
13380
+ if (isCreate) {
13381
+ const colLines = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
13382
+ for (const [fname, ftype] of fields) {
13383
+ const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
13384
+ const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
13385
+ colLines.push(` ${fname} ${info.sql}${defaultClause}`);
13386
+ }
13387
+ colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
13388
+ upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
13389
+ -- tina4:edit add columns beyond id + created_at
13390
+ ${colLines.join(",\n")}
13391
+ );`;
13392
+ downSql = `-- tina4:edit mirror the CREATE's added columns in the rollback
13393
+ DROP TABLE IF EXISTS ${table2};`;
13394
+ } else {
13395
+ upSql = `-- tina4:edit write your UP migration SQL here
13396
+ -- Example: ALTER TABLE ${table2} ADD COLUMN new_col TEXT DEFAULT '';`;
13397
+ downSql = `-- tina4:edit write your DOWN rollback SQL here
13398
+ -- Example: ALTER TABLE ${table2} DROP COLUMN new_col;`;
13399
+ }
13400
+ const now = isoNow();
13401
+ const content = `-- Migration: ${name}
13402
+ -- Created: ${now}
13403
+
13404
+ -- UP
13405
+ ${upSql}
13406
+
13407
+ -- DOWN
13408
+ ${downSql}
13409
+ `;
13410
+ writeFileSafe(path8, content);
13411
+ const downPath = join15(dir, `${ts}_${name}.down.sql`);
13412
+ const downContent = `-- Rollback: ${name}
13413
+ -- Created: ${now}
13414
+
13415
+ ${downSql}
13416
+ `;
13417
+ writeFileSafe(downPath, downContent);
13418
+ if (emitTest && isCreate) emitMigrationTest(name, table2);
13419
+ }
13420
+ function generateMiddleware(name, _flags) {
13421
+ const snake = toSnake(name);
13422
+ const dir = resolve6("src/middleware");
13423
+ ensureDir(dir);
13424
+ const path8 = join15(dir, `${snake}.ts`);
13425
+ if (__resolution.target === "middleware") {
13426
+ setResolutionField("class_name", name);
13427
+ setResolutionField("file_path", `src/middleware/${snake}.ts`);
13428
+ pushTestPath(`tests/${snake}.test.ts`);
13429
+ }
13430
+ const content = `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13431
+
13432
+ /**
13433
+ * ${name} middleware \u2014 runs before and after route handlers.
13434
+ *
13435
+ * Usage:
13436
+ * import { before${name}, after${name} } from "../middleware/${snake}.js";
13437
+ */
13438
+
13439
+ export async function before${name}(
13440
+ req: Tina4Request,
13441
+ res: Tina4Response,
13442
+ next: () => Promise<void>,
13443
+ ): Promise<void> {
13444
+ // tina4:edit replace the Authorization check with the real pre-request rule
13445
+ const auth = req.headers["authorization"];
13446
+ if (!auth) {
13447
+ res.json({ error: "Unauthorized" }, 401);
13448
+ return;
13449
+ }
13450
+ await next();
13451
+ }
13452
+
13453
+ export async function after${name}(
13454
+ req: Tina4Request,
13455
+ res: Tina4Response,
13456
+ next: () => Promise<void>,
13457
+ ): Promise<void> {
13458
+ // tina4:edit add post-processing (logging, header injection, telemetry)
13459
+ await next();
13460
+ }
13461
+ `;
13462
+ writeFileSafe(path8, content);
13463
+ emitMiddlewareTest(name, snake);
13464
+ }
13465
+ function generateTest(name, flags) {
13466
+ const snake = toSnake(name);
13467
+ const singular = snake.endsWith("s") ? snake.slice(0, -1) : snake;
13468
+ const model = flags.model;
13469
+ const dir = resolve6("tests");
13470
+ ensureDir(dir);
13471
+ const path8 = join15(dir, `${snake}.test.ts`);
13472
+ if (model && flags["secure-writes"]) {
13473
+ const isPublic = Boolean(flags.public);
13474
+ const posture = isPublic ? "open (--public)" : "gated";
13475
+ const writeCase = isPublic ? ` // --public opened the write: an anonymous POST creates -> 201.
13476
+ assert("anonymous POST is public -> 201",
13477
+ (await client.post("/api/${snake}", { json: { name: "test" } })).status === 201);` : ` // Secure by default: a tokenless POST is rejected with 401.
13478
+ assert("anonymous POST is gated -> 401",
13479
+ (await client.post("/api/${snake}", { json: { name: "test" } })).status === 401);
13480
+ // A valid Bearer token passes the gate and creates -> 201.
13481
+ const token = getToken({ userId: 1 });
13482
+ assert("authenticated POST creates -> 201",
13483
+ (await client.post("/api/${snake}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status === 201);`;
13484
+ const content2 = `/**
13485
+ * ${name} CRUD \u2014 reads public, writes ${posture} (secure by default).
13486
+ *
13487
+ * Real end-to-end via TestClient: no mocks \u2014 real Router, real auth gate, real
13488
+ * JWT, real SQLite DB + table. Run with: npx tsx tests/${snake}.test.ts
13489
+ */
13490
+ import { dirname, resolve } from "node:path";
13491
+ import { fileURLToPath } from "node:url";
13492
+ import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
13493
+ import { initDatabase } from "tina4-nodejs/orm";
13494
+ import ${model} from "../src/models/${model}.js";
13495
+
13496
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
13497
+ const here = dirname(fileURLToPath(import.meta.url));
13498
+
13499
+ let pass = 0;
13500
+ let fail = 0;
13501
+ function assert(label: string, ok: boolean): void {
13502
+ if (ok) { pass++; console.log(\` PASS \${label}\`); }
13503
+ else { fail++; console.log(\` FAIL \${label}\`); }
13504
+ }
13505
+
13506
+ await initDatabase({ url: "sqlite:///data/test_${snake}.db" });
13507
+ await ${model}.createTable();
13508
+
13509
+ const router = new Router();
13510
+ for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
13511
+ const client = new TestClient(router);
13512
+
13513
+ // Reads are public.
13514
+ assert("GET list is public -> 200", (await client.get("/api/${snake}")).status === 200);
13515
+ ${writeCase}
13516
+
13517
+ console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
13518
+ process.exit(fail > 0 ? 1 : 0);
13519
+ `;
13520
+ writeFileSafe(path8, content2);
13521
+ return;
13522
+ }
13523
+ let content;
13524
+ if (model) {
13525
+ content = `import { tests, assertTrue } from "tina4-nodejs";
13526
+
13527
+ /**
13528
+ * Tests for ${name} CRUD operations.
13529
+ */
13530
+
13531
+ const list${model}s = tests(
13532
+ assertTrue([]),
13533
+ )(function list${model}s() {
13534
+ // tina4:edit assert against a real GET /api/${toSnake(name)} response (rows, count)
13535
+ return true;
13536
+ });
13537
+
13538
+ const get${model} = tests(
13539
+ assertTrue([]),
13540
+ )(function get${model}() {
13541
+ // tina4:edit assert against GET /api/${toSnake(name)}/{id} for one seeded row
13542
+ return true;
13543
+ });
13544
+
13545
+ const create${model} = tests(
13546
+ assertTrue([]),
13547
+ )(function create${model}() {
13548
+ // tina4:edit POST a valid + an invalid body, assert 201 vs 400
13549
+ return true;
13550
+ });
13551
+
13552
+ const update${model} = tests(
13553
+ assertTrue([]),
13554
+ )(function update${model}() {
13555
+ // tina4:edit PUT changed fields, assert the row was persisted
13556
+ return true;
13557
+ });
13558
+
13559
+ const delete${model} = tests(
13560
+ assertTrue([]),
13561
+ )(function delete${model}() {
13562
+ // tina4:edit DELETE the id, assert 200 then GET returns 404
13563
+ return true;
13564
+ });
13565
+
13566
+ void [list${model}s, get${model}, create${model}, update${model}, delete${model}];
13567
+ `;
13568
+ } else {
13569
+ const titleName = name.charAt(0).toUpperCase() + name.slice(1);
13570
+ content = `import { tests, assertTrue } from "tina4-nodejs";
13571
+
13572
+ /**
13573
+ * Tests for ${name}.
13574
+ */
13575
+
13576
+ const test${titleName} = tests(
13577
+ assertTrue([]),
13578
+ )(function test${titleName}() {
13579
+ // tina4:edit assert against the real behaviour under test (no mocks)
13580
+ return true;
13581
+ });
13582
+
13583
+ void test${titleName};
13584
+ `;
13585
+ }
13586
+ writeFileSafe(path8, content);
13587
+ }
13588
+ function generateForm(name, flags) {
13589
+ const fields = fieldsOrDefault(flags.fields || "");
13590
+ const table2 = toTableName(name);
13591
+ const routeName = toPlural(table2);
13592
+ const inputTypes = {
13593
+ string: "text",
13594
+ str: "text",
13595
+ text: "textarea",
13596
+ int: "number",
13597
+ integer: "number",
13598
+ float: "number",
13599
+ numeric: "number",
13600
+ decimal: "number",
13601
+ bool: "checkbox",
13602
+ boolean: "checkbox",
13603
+ datetime: "datetime-local",
13604
+ blob: "file"
13605
+ };
13606
+ const dir = resolve6("src/templates/forms");
13607
+ ensureDir(dir);
13608
+ const path8 = join15(dir, `${table2}.twig`);
13609
+ let fieldHtml = "";
13610
+ for (const [fname, ftype] of fields) {
13611
+ const itype = inputTypes[ftype] || "text";
13612
+ const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
13613
+ const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
13614
+ if (itype === "textarea") {
13615
+ fieldHtml += ` <div class="form-group mb-3">
13616
+ <label for="${fname}">${label}</label>
13617
+ <textarea id="${fname}" name="${fname}" class="form-control" rows="4" placeholder="${label}">{{ item.${fname} }}</textarea>
13618
+ </div>
13619
+ `;
13620
+ } else if (itype === "checkbox") {
13621
+ fieldHtml += ` <div class="form-group mb-3">
13622
+ <label>
13623
+ <input type="checkbox" id="${fname}" name="${fname}" value="1" {% if item.${fname} %}checked{% endif %}>
13624
+ ${label}
13625
+ </label>
13626
+ </div>
13627
+ `;
13628
+ } else {
13629
+ fieldHtml += ` <div class="form-group mb-3">
13630
+ <label for="${fname}">${label}</label>
13631
+ <input type="${itype}" id="${fname}" name="${fname}" class="form-control"${step} value="{{ item.${fname} }}" placeholder="${label}">
13632
+ </div>
13633
+ `;
13634
+ }
13635
+ }
13636
+ const content = `{% extends "base.twig" %}
13637
+ {% block title %}${name} {% if item.id %}Edit{% else %}Create{% endif %}{% endblock %}
13638
+ {% block content %}
13639
+ <div class="container mt-4">
13640
+ <h1>{% if item.id %}Edit ${name}{% else %}Create ${name}{% endif %}</h1>
13641
+ {# tina4:edit restyle the form beyond the scaffolded defaults #}
13642
+ <form method="post" action="/api/${routeName}{% if item.id %}/{{ item.id }}{% endif %}">
13643
+ {{ form_token() }}
13644
+ ` + fieldHtml + ` <button type="submit" class="btn btn-primary">
13645
+ {% if item.id %}Update{% else %}Create{% endif %}
13646
+ </button>
13647
+ <a href="/api/${routeName}" class="btn btn-secondary">Cancel</a>
13648
+ </form>
13649
+ </div>
13650
+ {% endblock %}
13651
+ `;
13652
+ writeFileSafe(path8, content);
13653
+ }
13654
+ function generateView(name, flags) {
13655
+ const fields = fieldsOrDefault(flags.fields || "");
13656
+ const table2 = toTableName(name);
13657
+ const routeName = toPlural(table2);
13658
+ const cols = fields.map(([f]) => f);
13659
+ const dir = resolve6("src/templates/pages");
13660
+ ensureDir(dir);
13661
+ const listPath = join15(dir, `${routeName}.twig`);
13662
+ const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
13663
+ const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
13664
+ const listContent = `{% extends "base.twig" %}
13665
+ {% block title %}${name}s{% endblock %}
13666
+ {% block content %}
13667
+ <div class="container mt-4">
13668
+ {# tina4:edit add sort / filter / pagination controls to the list #}
13669
+ <div class="d-flex justify-content-between align-items-center mb-3">
13670
+ <h1>${name}s</h1>
13671
+ <a href="/${routeName}/create" class="btn btn-primary">Add ${name}</a>
13672
+ </div>
13673
+ <table class="table">
13674
+ <thead>
13675
+ <tr>
13676
+ <th>ID</th>
13677
+ ${th}
13678
+ <th>Actions</th>
13679
+ </tr>
13680
+ </thead>
13681
+ <tbody>
13682
+ {% for item in items %}
13683
+ <tr>
13684
+ <td>{{ item.id }}</td>
13685
+ ${td}
13686
+ <td>
13687
+ <a href="/${routeName}/{{ item.id }}" class="btn btn-sm btn-primary">View</a>
13688
+ <a href="/${routeName}/{{ item.id }}/edit" class="btn btn-sm btn-secondary">Edit</a>
13689
+ </td>
13690
+ </tr>
13691
+ {% endfor %}
13692
+ </tbody>
13693
+ </table>
13694
+ </div>
13695
+ {% endblock %}
13696
+ `;
13697
+ writeFileSafe(listPath, listContent);
13698
+ const detailPath = join15(dir, `${table2}.twig`);
13699
+ const detailFields = cols.map((c) => ` <div class="mb-3"><strong>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}:</strong> {{ item.${c} }}</div>`).join("\n");
13700
+ const detailContent = `{% extends "base.twig" %}
13701
+ {% block title %}${name} Detail{% endblock %}
13702
+ {% block content %}
13703
+ <div class="container mt-4">
13704
+ {# tina4:edit extend the detail view with related records or actions #}
13705
+ <div class="d-flex justify-content-between align-items-center mb-3">
13706
+ <h1>${name} #{{ item.id }}</h1>
13707
+ <div>
13708
+ <a href="/${routeName}/{{ item.id }}/edit" class="btn btn-secondary">Edit</a>
13709
+ <a href="/${routeName}" class="btn btn-outline-secondary">Back</a>
13710
+ </div>
13711
+ </div>
13712
+ ${detailFields}
13713
+ </div>
13714
+ {% endblock %}
13715
+ `;
13716
+ writeFileSafe(detailPath, detailContent);
13717
+ }
13718
+ function generateAuth(_flags) {
13719
+ if (!__resolution.jsonMode) console.log("\n Generating authentication scaffolding...\n");
13720
+ generateModel("User", { fields: "email:string,password:string,role:string" }, false);
13721
+ const registerDir = resolve6("src/routes/api/auth/register");
13722
+ const loginDir = resolve6("src/routes/api/auth/login");
13723
+ const meDir = resolve6("src/routes/api/auth/me");
13724
+ ensureDir(registerDir);
13725
+ ensureDir(loginDir);
13726
+ ensureDir(meDir);
13727
+ writeFileSafe(
13728
+ join15(registerDir, "post.ts"),
13729
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13730
+ import { hashPassword } from "tina4-nodejs";
13731
+ import User from "../../../../models/User.js";
13732
+
13733
+ // Public: registration mints an account for a user who has no token yet.
13734
+ export const secure = false;
13735
+
13736
+ export const meta = { summary: "Register a new user", tags: ["auth"] };
13737
+
13738
+ export default async function (req: Tina4Request, res: Tina4Response) {
13739
+ // tina4:edit add password-strength / email-format / captcha rules before mint
13740
+ const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
13741
+
13742
+ if (!email || !password) {
13743
+ res.json({ error: "Email and password required" }, 400);
13744
+ return;
13745
+ }
13746
+
13747
+ const existing = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
13748
+ if (existing) {
13749
+ res.json({ error: "Email already registered" }, 409);
13750
+ return;
13751
+ }
13752
+
13753
+ const user = new User({ email, password: hashPassword(password), role: "user" });
13754
+ await user.save();
13755
+ res.json({ message: "Registered", id: user.toObject().id }, 201);
13756
+ }
13757
+ `
13758
+ );
13759
+ writeFileSafe(
13760
+ join15(loginDir, "post.ts"),
13761
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13762
+ import { checkPassword, getToken } from "tina4-nodejs";
13763
+ import User from "../../../../models/User.js";
13764
+
13765
+ // Public: login authenticates by password and mints the token.
13766
+ export const secure = false;
13767
+
13768
+ export const meta = { summary: "Login and receive JWT token", tags: ["auth"] };
13769
+
13770
+ export default async function (req: Tina4Request, res: Tina4Response) {
13771
+ // tina4:edit add rate-limit / lock-after-N-failures / 2FA before password check
13772
+ const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
13773
+
13774
+ if (!email || !password) {
13775
+ res.json({ error: "Email and password required" }, 400);
13776
+ return;
13777
+ }
13778
+
13779
+ const user = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
13780
+ if (!user || !checkPassword(password, user.toObject().password as string)) {
13781
+ res.json({ error: "Invalid credentials" }, 401);
13782
+ return;
13783
+ }
13784
+
13785
+ const data = user.toObject();
13786
+ // tina4:edit set token TTL (getToken(payload, secret, expiresInMinutes)) and add scopes if needed
13787
+ const token = getToken({ userId: data.id, email: data.email, role: data.role });
13788
+ res.json({ token });
13789
+ }
13790
+ `
13791
+ );
13792
+ writeFileSafe(
13793
+ join15(meDir, "get.ts"),
13794
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13795
+ import { authenticateRequest } from "tina4-nodejs";
13796
+
13797
+ export const meta = { summary: "Get current authenticated user", tags: ["auth"] };
13798
+
13799
+ export default async function (req: Tina4Request, res: Tina4Response) {
13800
+ const payload = authenticateRequest(req.headers as Record<string, string | string[] | undefined>);
13801
+ if (!payload) {
13802
+ res.json({ error: "Unauthorized" }, 401);
13803
+ return;
13804
+ }
13805
+ res.json({ user: payload });
13806
+ }
13807
+ `
13808
+ );
13809
+ const formsDir = resolve6("src/templates/forms");
13810
+ ensureDir(formsDir);
13811
+ writeFileSafe(
13812
+ join15(formsDir, "login.twig"),
13813
+ `{% extends "base.twig" %}
13814
+ {% block title %}Login{% endblock %}
13815
+ {% block content %}
13816
+ <div class="container mt-4" style="max-width:400px">
13817
+ <h1>Login</h1>
13818
+ <form method="post" action="/api/auth/login">
13819
+ {{ form_token() }}
13820
+ <div class="form-group mb-3">
13821
+ <label for="email">Email</label>
13822
+ <input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
13823
+ </div>
13824
+ <div class="form-group mb-3">
13825
+ <label for="password">Password</label>
13826
+ <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
13827
+ </div>
13828
+ <button type="submit" class="btn btn-primary w-100">Login</button>
13829
+ <p class="mt-3 text-center"><a href="/register">Create an account</a></p>
13830
+ </form>
13831
+ </div>
13832
+ {% endblock %}
13833
+ `
13834
+ );
13835
+ writeFileSafe(
13836
+ join15(formsDir, "register.twig"),
13837
+ `{% extends "base.twig" %}
13838
+ {% block title %}Register{% endblock %}
13839
+ {% block content %}
13840
+ <div class="container mt-4" style="max-width:400px">
13841
+ <h1>Register</h1>
13842
+ <form method="post" action="/api/auth/register">
13843
+ {{ form_token() }}
13844
+ <div class="form-group mb-3">
13845
+ <label for="email">Email</label>
13846
+ <input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
13847
+ </div>
13848
+ <div class="form-group mb-3">
13849
+ <label for="password">Password</label>
13850
+ <input type="password" id="password" name="password" class="form-control" placeholder="Password" minlength="8" required>
13851
+ </div>
13852
+ <button type="submit" class="btn btn-primary w-100">Register</button>
13853
+ <p class="mt-3 text-center"><a href="/login">Already have an account?</a></p>
13854
+ </form>
13855
+ </div>
13856
+ {% endblock %}
13857
+ `
13858
+ );
13859
+ emitAuthTest();
13860
+ if (!__resolution.jsonMode) {
13861
+ console.log("\n Authentication scaffolding complete.");
13862
+ console.log(" Run: tina4nodejs migrate");
13863
+ console.log(" POST /api/auth/register \u2014 create account (public)");
13864
+ console.log(" POST /api/auth/login \u2014 get JWT token (public)");
13865
+ console.log(" GET /api/auth/me \u2014 get profile (requires token)");
13866
+ }
13867
+ }
13868
+ function generateService(name, flags) {
13869
+ const snake = toSnake(name);
13870
+ const camel = toCamel(toPascal(name)) || snake;
13871
+ const cron = flags.cron;
13872
+ const dir = resolve6("src/services");
13873
+ ensureDir(dir);
13874
+ const path8 = join15(dir, `${snake}.ts`);
13875
+ let scheduleField;
13876
+ let note;
13877
+ if (cron && cron !== true) {
13878
+ scheduleField = ` timing: ${JSON.stringify(String(cron))},`;
13879
+ note = `cron '${cron}'`;
13880
+ } else {
13881
+ const seconds = parseEvery(flags.every);
13882
+ scheduleField = ` interval: ${seconds},`;
13883
+ note = `every ${seconds}s`;
13884
+ }
13885
+ const body = aiFill(`${camel}Task`, {
13886
+ intent: "do the scheduled work for this service",
13887
+ given: "context -> ServiceContext (.name, .running, .lastRun)",
13888
+ use: "your ORM / Api / Messenger code (re-run on schedule)",
13889
+ ground: `tina4_context("background service scheduled task", "nodejs") \xB7 skill tina4-developer-nodejs`,
13890
+ raise: `service ${snake} not implemented`
13891
+ });
13892
+ const content = `import type { ServiceContext } from "tina4-nodejs";
13893
+
13894
+ /**
13895
+ * ${name} background service \u2014 runs ${note} via ServiceRunner.
13896
+ *
13897
+ * Wire a runner once (e.g. in app.ts) to actually run it \u2014 \`tina4nodejs serve\`
13898
+ * does NOT auto-start services:
13899
+ *
13900
+ * import { ServiceRunner } from "tina4-nodejs";
13901
+ * await ServiceRunner.discover("src/services"); // registers this default export
13902
+ * ServiceRunner.start();
13903
+ */
13904
+
13905
+ export async function ${camel}Task(context: ServiceContext): Promise<void> {
13906
+ // tina4:edit replace the AI-FILL stub below with the scheduled work
13907
+ ${body}}
13908
+
13909
+ // Discovered by ServiceRunner.discover("src/services") \u2014 it reads name/handler
13910
+ // (+ interval or timing) off this default export.
13911
+ export default {
13912
+ name: "${snake}",
13913
+ handler: ${camel}Task,
13914
+ ${scheduleField}
13915
+ };
13916
+ `;
13917
+ writeFileSafe(path8, content);
13918
+ emitServiceTest(name, snake, camel);
13919
+ }
13920
+ function generateQueue(name, _flags) {
13921
+ const topic = name.replace(/^\//, "");
13922
+ const slug = toSnake(topic.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
13923
+ const pascal = toPascal(topic) || "Topic";
13924
+ const dir = resolve6("src/services");
13925
+ ensureDir(dir);
13926
+ const path8 = join15(dir, `${slug}_consumer.ts`);
13927
+ const body = aiFill(`handle${pascal}`, {
13928
+ intent: `process ONE ${topic} job payload`,
13929
+ given: "payload -> the produced job data (job.payload)",
13930
+ use: "your ORM / Messenger code; return to ack (job.complete), throw to nack (job.fail)",
13931
+ ground: `tina4_context("process a queue job", "nodejs") \xB7 skill tina4-developer-nodejs`,
13932
+ raise: `queue ${topic} handler not implemented`
13933
+ });
13934
+ const content = `import { Queue } from "tina4-nodejs";
13935
+ import type { ServiceContext } from "tina4-nodejs";
13936
+
13937
+ /**
13938
+ * ${topic} queue \u2014 producer + consumer worker.
13939
+ *
13940
+ * Produce from anywhere: publish${pascal}({ ... })
13941
+ * The consumer is a long-running worker wired as a ServiceRunner daemon:
13942
+ * await ServiceRunner.discover("src/services"); ServiceRunner.start();
13943
+ */
13944
+
13945
+ /** Enqueue a ${topic} job for the worker below to process. Returns the job id. */
13946
+ export function publish${pascal}(payload: Record<string, unknown>): string {
13947
+ return new Queue({ topic: "${topic}" }).produce("${topic}", payload);
13948
+ }
13949
+
13950
+ /** Process ONE ${topic} job payload. */
13951
+ export async function handle${pascal}(payload: unknown): Promise<void> {
13952
+ // tina4:edit implement the per-job handler; return to ack, throw to nack
13953
+ ${body}}
13954
+
13955
+ /** Long-running ${topic} worker \u2014 consume() yields jobs; ack/nack each. */
13956
+ export async function consume${pascal}(_context?: ServiceContext): Promise<void> {
13957
+ const queue = new Queue({ topic: "${topic}" });
13958
+ for await (const job of queue.consume("${topic}")) {
13959
+ const one = Array.isArray(job) ? job[0] : job;
13960
+ try {
13961
+ await handle${pascal}(one.payload);
13962
+ one.complete(); // ack \u2014 remove from the queue
13963
+ } catch (err) {
13964
+ one.fail(String(err)); // nack \u2014 retry / dead-letter
13965
+ }
13966
+ }
13967
+ }
13968
+
13969
+ // Discovered by ServiceRunner.discover("src/services"); daemon:true because
13970
+ // consume${pascal} owns its own loop. The topic + per-job handle keys let
13971
+ // \`tina4nodejs queue work ${topic}\` drive this consumer directly (own the poll
13972
+ // loop / bounded --once drain) without wiring a ServiceRunner.
13973
+ export default {
13974
+ name: "${topic}-consumer",
13975
+ topic: "${topic}",
13976
+ handler: consume${pascal},
13977
+ handle: handle${pascal},
13978
+ daemon: true,
13979
+ };
13980
+ `;
13981
+ writeFileSafe(path8, content);
13982
+ emitQueueTest(topic, slug, pascal);
13983
+ }
13984
+ function generateValidator(name, _flags) {
13985
+ const dir = resolve6("src/validators");
13986
+ ensureDir(dir);
13987
+ const path8 = join15(dir, `${toSnake(name)}.ts`);
13988
+ const rules = extend(
13989
+ "add / adjust the validation rules for this payload",
13990
+ `e.g. .email("email").minLength("name", 2).integer("age"); ground: tina4_context("validate request body with Validator", "nodejs")`
13991
+ );
13992
+ const content = `import { Validator } from "tina4-nodejs";
13993
+
13994
+ /**
13995
+ * Validate a ${name} payload. Returns a Validator (chainable rules).
13996
+ *
13997
+ * Usage in a route:
13998
+ * const v = validate${toPascal(name)}(req.body as Record<string, unknown>);
13999
+ * if (!v.isValid()) return res.json({ error: v.errors()[0]?.message }, 400);
14000
+ */
14001
+ export function validate${toPascal(name)}(data: Record<string, unknown>): Validator {
14002
+ const validator = new Validator(data);
14003
+ // tina4:edit add rules for this payload (.email/.minLength/.integer/.inList/.pattern)
14004
+ ${rules} validator.required("name"); // starter rule (matches the model's default field)
14005
+ return validator;
14006
+ }
14007
+ `;
14008
+ writeFileSafe(path8, content);
14009
+ emitValidatorTest(name, toSnake(name), toPascal(name));
14010
+ }
14011
+ function generateSeeder(name, _flags) {
14012
+ const table2 = toTableName(name);
14013
+ const dir = resolve6("src/seeds");
14014
+ ensureDir(dir);
14015
+ const path8 = join15(dir, `${table2}_seeder.ts`);
14016
+ const overrides = extend(
14017
+ "override fields that need a specific shape (seedOrm auto-fills the rest)",
14018
+ `e.g. return { email: (f) => f.email(), status: "active" }; ground: tina4_context("seed ORM model with FakeData", "nodejs")`
14019
+ );
14020
+ const content = `import { pathToFileURL } from "node:url";
14021
+ import { FakeData, seedOrm, initDatabase } from "tina4-nodejs/orm";
14022
+ import ${name} from "../models/${name}.js";
14023
+
14024
+ /**
14025
+ * Seeder for ${name} \u2014 run with: tina4nodejs seed
14026
+ *
14027
+ * seedOrm auto-fills every field by type/name; override the ones that need a
14028
+ * specific shape below. Each callable receives a FakeData instance.
14029
+ */
14030
+ export function fieldOverrides(fake: FakeData): Record<string, unknown> {
14031
+ // tina4:edit override any fields that need a specific shape (seedOrm auto-fills the rest)
14032
+ ${overrides} void fake; // available for overrides above
14033
+ return {};
14034
+ }
14035
+
14036
+ /** Seed rows. Invoked when this file is run directly by \`tina4nodejs seed\`. */
14037
+ export async function run(): Promise<void> {
14038
+ await initDatabase({ url: process.env.TINA4_DATABASE_URL ?? "sqlite:///data/app.db" });
14039
+ const summary = await seedOrm(${name} as never, 20, fieldOverrides(new FakeData()));
14040
+ console.log(\`Seeded \${summary.seeded} ${name} row(s), \${summary.failed} failed\`);
14041
+ }
14042
+
14043
+ // Only seed when executed as a script (\`tina4nodejs seed\` runs it via tsx) \u2014
14044
+ // importing this module (e.g. in a test) must NOT trigger seeding.
14045
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
14046
+ await run();
14047
+ }
14048
+ `;
14049
+ writeFileSafe(path8, content);
14050
+ emitSeederTest(name, table2);
14051
+ }
14052
+ function generateWebsocket(name, _flags) {
14053
+ const raw = name.trim();
14054
+ const wsPath = raw.startsWith("/") ? raw : "/ws/" + raw.replace(/^\/+/, "");
14055
+ let slug = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
14056
+ const base = slug.startsWith("ws_") ? slug.slice(3) : slug;
14057
+ const handlerName = `${toCamel(toPascal(base))}Ws`;
14058
+ const dir = resolve6("src/routes");
14059
+ ensureDir(dir);
14060
+ const path8 = join15(dir, `ws_${base}.ts`);
14061
+ const body = aiFill(handlerName, {
14062
+ intent: `handle an inbound "message" frame on ${wsPath}`,
14063
+ given: "data -> the message payload (string); connection -> WebSocketConnection",
14064
+ use: "connection.broadcast(data) or connection.sendJson({ ... })",
14065
+ ground: `tina4_context("websocket broadcast message", "nodejs") \xB7 skill tina4-developer-nodejs`,
14066
+ raise: `websocket ${wsPath} not implemented`
14067
+ });
14068
+ const content = `import { websocket } from "tina4-nodejs";
14069
+ import type { WebSocketConnection } from "tina4-nodejs";
14070
+
14071
+ /**
14072
+ * ${wsPath} WebSocket route.
14073
+ *
14074
+ * Registered on import by websocket(). Node has NO file-based WS
14075
+ * auto-discovery, so IMPORT this module once from app.ts to activate it (add
14076
+ * \`.secure()\` to require a JWT on the upgrade):
14077
+ *
14078
+ * import "./src/routes/ws_${base}.js";
14079
+ *
14080
+ * The server invokes the handler as (connection, event, data) for each event:
14081
+ * "open" (connect), "message" (inbound frame), "close" (disconnect).
14082
+ */
14083
+ export async function ${handlerName}(
14084
+ connection: WebSocketConnection,
14085
+ event: "open" | "message" | "close",
14086
+ data: string,
14087
+ ): Promise<void> {
14088
+ if (event === "open") {
14089
+ // tina4:edit customize the welcome frame (or drop it)
14090
+ connection.sendJson({ type: "welcome" });
14091
+ return;
14092
+ }
14093
+ if (event === "close") {
14094
+ return;
14095
+ }
14096
+ // event === "message"
14097
+ // tina4:edit handle the inbound "message" frame (broadcast, echo, route, etc.)
14098
+ ${body}}
14099
+
14100
+ websocket("${wsPath}", ${handlerName});
14101
+ `;
14102
+ writeFileSafe(path8, content);
14103
+ emitWebsocketTest(wsPath, base, handlerName);
14104
+ }
14105
+ function generateListener(name, _flags) {
14106
+ const event = name.trim();
14107
+ const slug = toSnake(event.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
14108
+ const handlerName = `on${toPascal(slug)}`;
14109
+ const dir = resolve6("src/listeners");
14110
+ ensureDir(dir);
14111
+ const path8 = join15(dir, `${slug}.ts`);
14112
+ const body = aiFill(handlerName, {
14113
+ intent: `react to the '${event}' event`,
14114
+ given: `args -> whatever Events.emit("${event}", ...args) passed`,
14115
+ use: "your app code \u2014 Messenger().send(...), an ORM write, or Events.emit(...) a follow-up",
14116
+ ground: `tina4_context("event listener reaction", "nodejs") \xB7 skill tina4-developer-nodejs`,
14117
+ raise: `listener ${event} not implemented`
14118
+ });
14119
+ const content = `import { Events } from "tina4-nodejs";
14120
+
14121
+ /**
14122
+ * Listener for the '${event}' event.
14123
+ *
14124
+ * Registered on import by Events.on(). Node has NO src/listeners/
14125
+ * auto-discovery, so IMPORT this module once from app.ts to activate it:
14126
+ *
14127
+ * import "./src/listeners/${slug}.js";
14128
+ *
14129
+ * Fires when something calls Events.emit("${event}", ...args).
14130
+ */
14131
+ export function ${handlerName}(...args: unknown[]): void {
14132
+ // tina4:edit implement the reaction to '${event}' (email, ORM write, follow-up emit)
14133
+ ${body}}
14134
+
14135
+ Events.on("${event}", ${handlerName});
14136
+ `;
14137
+ writeFileSafe(path8, content);
14138
+ emitListenerTest(event, slug);
14139
+ }
14140
+ function writeTest(testName, content) {
14141
+ const dir = resolve6("tests");
14142
+ ensureDir(dir);
14143
+ writeFileSafe(join15(dir, `${testName}.test.ts`), content);
14144
+ }
14145
+ function standaloneTest(doc, body) {
14146
+ return `${doc}
14147
+
14148
+ let pass = 0;
14149
+ let fail = 0;
14150
+ function assert(label: string, ok: boolean): void {
14151
+ if (ok) { pass++; console.log(\` PASS \${label}\`); }
14152
+ else { fail++; console.log(\` FAIL \${label}\`); }
14153
+ }
14154
+ async function assertThrows(label: string, fn: () => unknown | Promise<unknown>): Promise<void> {
14155
+ try { await fn(); assert(label, false); }
14156
+ catch { assert(label, true); }
14157
+ }
14158
+
14159
+ ${body}
14160
+
14161
+ console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
14162
+ process.exit(fail > 0 ? 1 : 0);
14163
+ `;
14164
+ }
14165
+ function sampleLiteral(fieldType) {
14166
+ switch ((fieldType || "string").toLowerCase()) {
14167
+ case "int":
14168
+ case "integer":
14169
+ return "1";
14170
+ case "float":
14171
+ case "number":
14172
+ case "numeric":
14173
+ case "decimal":
14174
+ return "1.5";
14175
+ case "bool":
14176
+ case "boolean":
14177
+ return "true";
14178
+ case "datetime":
14179
+ return '"2020-01-01 00:00:00"';
14180
+ case "blob":
14181
+ return '"x"';
14182
+ default:
14183
+ return '"sample"';
14184
+ }
14185
+ }
14186
+ function emitModelTest(model, table2, fields) {
14187
+ const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
14188
+ const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
14189
+ const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
14190
+ const valueAssert = stringField ? `
14191
+ assert("string field round-trips", fetched !== null && (fetched.toObject() as Record<string, unknown>).${stringField} === "sample");` : "";
14192
+ const doc = `/**
14193
+ * Real ORM roundtrip for ${model} \u2014 no mocks, real SQLite.
14194
+ *
14195
+ * Generated with src/models/${model}.ts by \`tina4nodejs generate model
14196
+ * ${model}\`. The model scaffold is working code, so this passes on generation:
14197
+ * binds a real in-memory SQLite DB, creates the table, saves a row, reads it
14198
+ * back. Run with: npx tsx tests/${table2}_model.test.ts
14199
+ */
14200
+ import ${model} from "../src/models/${model}.js";
14201
+ import { initDatabase } from "tina4-nodejs/orm";`;
14202
+ const body = `await initDatabase({ url: "sqlite:///:memory:" });
14203
+ await ${model}.createTable();
14204
+
14205
+ const row = new ${model}({ ${payload} });
14206
+ const saved = await row.save();
14207
+ assert("create() persists and returns the row", saved !== false && Boolean(row.toObject().id));
14208
+
14209
+ const id = row.toObject().id;
14210
+ const fetched = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
14211
+ assert("row reads back by id", fetched !== null);
14212
+ assert("read-back id matches", fetched !== null && (fetched.toObject() as Record<string, unknown>).id === id);${valueAssert}
14213
+
14214
+ const missing = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [999999]);
14215
+ assert("find missing returns null", missing === null);`;
14216
+ writeTest(`${table2}_model`, standaloneTest(doc, body));
14217
+ }
14218
+ function emitRouteStubTest(route) {
14219
+ const doc = `/**
14220
+ * Routing test for ${route} \u2014 no mocks, real Router + route discovery.
14221
+ *
14222
+ * Generated with src/routes/api/${route}/ by \`tina4nodejs generate route
14223
+ * ${route}\` (no --model). The handlers are AI-FILL stubs that throw until you
14224
+ * implement them, so this tests what IS live on generation: all five routes
14225
+ * register on the REAL Router, and the list handler fails loud until filled.
14226
+ * Run with: npx tsx tests/${route}.test.ts
14227
+ */
14228
+ import { dirname, resolve } from "node:path";
14229
+ import { fileURLToPath } from "node:url";
14230
+ import { Router, discoverRoutes } from "tina4-nodejs";
14231
+ import listHandler from "../src/routes/api/${route}/get.js";`;
14232
+ const body = `const here = dirname(fileURLToPath(import.meta.url));
14233
+ const router = new Router();
14234
+ const defs = await discoverRoutes(resolve(here, "../src/routes"));
14235
+ for (const def of defs) router.addRoute(def);
14236
+
14237
+ const sigs = defs.map((d) => \`\${d.method} \${d.pattern}\`);
14238
+ for (const sig of ["GET /api/${route}", "POST /api/${route}", "GET /api/${route}/{id}", "PUT /api/${route}/{id}", "DELETE /api/${route}/{id}"]) {
14239
+ assert(\`route registered: \${sig}\`, sigs.includes(sig));
14240
+ }
14241
+
14242
+ // The scaffolded list handler is a loud AI-FILL stub \u2014 it throws until filled.
14243
+ await assertThrows("list handler is a live stub (throws until filled)",
14244
+ () => listHandler({} as never, {} as never));`;
14245
+ writeTest(route, standaloneTest(doc, body));
14246
+ }
14247
+ function emitMiddlewareTest(name, snake) {
14248
+ const doc = `/**
14249
+ * Real dispatch test for the ${name} middleware \u2014 no mocks.
14250
+ *
14251
+ * Generated with src/middleware/${snake}.ts by \`tina4nodejs generate middleware
14252
+ * ${name}\`. Drives the scaffolded before/after functions through the REAL
14253
+ * MiddlewareChain with a real Tina4Request/Response (built from real node http
14254
+ * objects) \u2014 the same continuation dispatch the live server runs.
14255
+ * Run with: npx tsx tests/${snake}.test.ts
14256
+ */
14257
+ import { IncomingMessage, ServerResponse } from "node:http";
14258
+ import { Socket } from "node:net";
14259
+ import { MiddlewareChain, createRequest, createResponse } from "tina4-nodejs";
14260
+ import type { Tina4Request, Tina4Response } from "tina4-nodejs";
14261
+ import { before${name}, after${name} } from "../src/middleware/${snake}.js";`;
14262
+ const body = `function realPair(headers: Record<string, string>): { req: Tina4Request; res: Tina4Response; raw: ServerResponse } {
14263
+ const socket = new Socket();
14264
+ const rawReq = new IncomingMessage(socket);
14265
+ rawReq.method = "GET";
14266
+ rawReq.url = "/";
14267
+ rawReq.headers = { ...headers, host: "localhost" };
14268
+ rawReq.push(null);
14269
+ const rawRes = new ServerResponse(rawReq);
14270
+ rawRes.write = (() => true) as typeof rawRes.write;
14271
+ rawRes.end = (function (this: ServerResponse) { return this; }) as typeof rawRes.end;
14272
+ return { req: createRequest(rawReq), res: createResponse(rawRes), raw: rawRes };
14273
+ }
14274
+
14275
+ // before(): blocks an unauthenticated request (401, does not call next()).
14276
+ {
14277
+ const chain = new MiddlewareChain();
14278
+ chain.use(before${name});
14279
+ let reached = false;
14280
+ chain.use(async (_r, _s, next) => { reached = true; next(); });
14281
+ const { req, res, raw } = realPair({});
14282
+ await chain.run(req, res);
14283
+ assert("before blocks unauthenticated (401, chain short-circuits)", raw.statusCode === 401 && reached === false);
14284
+ }
14285
+
14286
+ // before(): lets an authenticated request through to the next middleware.
14287
+ {
14288
+ const chain = new MiddlewareChain();
14289
+ chain.use(before${name});
14290
+ let reached = false;
14291
+ chain.use(async (_r, _s, next) => { reached = true; next(); });
14292
+ const { req, res } = realPair({ authorization: "Bearer test" });
14293
+ await chain.run(req, res);
14294
+ assert("before passes an authenticated request through", reached === true);
14295
+ }
14296
+
14297
+ // after(): always continues the chain.
14298
+ {
14299
+ const chain = new MiddlewareChain();
14300
+ chain.use(after${name});
14301
+ let reached = false;
14302
+ chain.use(async (_r, _s, next) => { reached = true; next(); });
14303
+ const { req, res } = realPair({});
14304
+ await chain.run(req, res);
14305
+ assert("after runs and continues the chain", reached === true);
14306
+ }`;
14307
+ writeTest(snake, standaloneTest(doc, body));
14308
+ }
14309
+ function emitServiceTest(name, snake, camel) {
14310
+ const doc = `/**
14311
+ * Real ServiceRunner test for the ${name} service \u2014 no mocks.
14312
+ *
14313
+ * Generated with src/services/${snake}.ts by \`tina4nodejs generate service
14314
+ * ${name}\`. Registers the scaffold on a REAL ServiceRunner and confirms the
14315
+ * descriptor; the task body is an AI-FILL stub that throws until filled.
14316
+ * Run with: npx tsx tests/${snake}.test.ts
14317
+ */
14318
+ import { ServiceRunner } from "tina4-nodejs";
14319
+ import service, { ${camel}Task } from "../src/services/${snake}.js";`;
14320
+ const body = `assert("descriptor has a name + callable handler",
14321
+ service.name === "${snake}" && typeof service.handler === "function");
14322
+
14323
+ // Register on a REAL ServiceRunner and confirm it is listed.
14324
+ ServiceRunner.register(service.name, service.handler, { interval: (service as { interval?: number }).interval });
14325
+ assert("registers on a real ServiceRunner", ServiceRunner.list().some((s) => s.name === "${snake}"));
14326
+ ServiceRunner.remove("${snake}");
14327
+
14328
+ // The scaffolded task body is an AI-FILL stub \u2014 it throws until filled.
14329
+ await assertThrows("task is a live stub (throws until filled)", () => ${camel}Task({} as never));`;
14330
+ writeTest(snake, standaloneTest(doc, body));
14331
+ }
14332
+ function emitQueueTest(topic, slug, pascal) {
14333
+ const doc = `/**
14334
+ * Real file-backed Queue test for the ${topic} worker \u2014 no mocks.
14335
+ *
14336
+ * Generated with src/services/${slug}_consumer.ts by \`tina4nodejs generate
14337
+ * queue ${topic}\`. Pushes a REAL job onto the real file-backed Queue and
14338
+ * asserts it is enqueued, and that the consumer is wired as a daemon. The
14339
+ * per-job handle is an AI-FILL stub that throws until filled.
14340
+ * Run with: npx tsx tests/${slug}.test.ts
14341
+ */
14342
+ import { Queue } from "tina4-nodejs";
14343
+ import worker, { publish${pascal}, handle${pascal} } from "../src/services/${slug}_consumer.js";`;
14344
+ const body = `const jobId = publish${pascal}({ hello: "world" });
14345
+ assert("publish enqueues a real job (returns an id)", typeof jobId === "string" && jobId.length > 0);
14346
+ assert("the job is really on the queue", new Queue({ topic: "${topic}" }).size() >= 1);
14347
+
14348
+ assert("consumer default export is a daemon", worker.daemon === true);
14349
+ assert("consumer handler is wired", typeof worker.handler === "function");
14350
+
14351
+ // The per-job handler is an AI-FILL stub \u2014 it throws until filled.
14352
+ await assertThrows("handle is a live stub (throws until filled)", () => handle${pascal}({}));`;
14353
+ writeTest(slug, standaloneTest(doc, body));
14354
+ }
14355
+ function emitValidatorTest(name, snake, pascal) {
14356
+ const doc = `/**
14357
+ * Real validation test for validate${pascal} \u2014 no mocks.
14358
+ *
14359
+ * Generated with src/validators/${snake}.ts by \`tina4nodejs generate validator
14360
+ * ${name}\`. The scaffold ships a starter rule (required "name"), so this passes
14361
+ * on generation \u2014 adjust the rules for your payload and update these cases.
14362
+ * Run with: npx tsx tests/${snake}.test.ts
14363
+ */
14364
+ import { validate${pascal} } from "../src/validators/${snake}.js";`;
14365
+ const body = `assert("valid input passes", validate${pascal}({ name: "Ada" }).isValid());
14366
+
14367
+ const bad = validate${pascal}({});
14368
+ assert("invalid input fails", bad.isValid() === false);
14369
+ assert("invalid input reports errors", bad.errors().length > 0);`;
14370
+ writeTest(snake, standaloneTest(doc, body));
14371
+ }
14372
+ function emitSeederTest(model, table2) {
14373
+ const doc = `/**
14374
+ * Real seeding test for the ${model} seeder \u2014 no mocks, real SQLite.
14375
+ *
14376
+ * Generated with src/seeds/${table2}_seeder.ts by \`tina4nodejs generate seeder
14377
+ * ${model}\`. Binds a real SQLite DB, creates the table, runs the scaffolded
14378
+ * seeder (auto-fills every field via FakeData) and asserts rows were created.
14379
+ * Run with: npx tsx tests/${table2}_seeder.test.ts
14380
+ */
14381
+ import { initDatabase, FakeData } from "tina4-nodejs/orm";
14382
+ import ${model} from "../src/models/${model}.js";
14383
+ import { fieldOverrides, run } from "../src/seeds/${table2}_seeder.js";`;
14384
+ const body = `process.env.TINA4_DATABASE_URL = "sqlite:///test_${table2}_seeder.db";
14385
+ await initDatabase({ url: process.env.TINA4_DATABASE_URL });
14386
+ await ${model}.createTable();
14387
+
14388
+ assert("fieldOverrides returns an object", typeof fieldOverrides(new FakeData()) === "object");
14389
+
14390
+ await run(); // run() re-binds the same DB URL and seeds via seedOrm
14391
+ const rows = await ${model}.all();
14392
+ assert("run() seeds real rows", rows.length >= 1);`;
14393
+ writeTest(`${table2}_seeder`, standaloneTest(doc, body));
14394
+ }
14395
+ function emitWebsocketTest(wsPath, base, handler) {
14396
+ const doc = `/**
14397
+ * Real handler test for the ${wsPath} WebSocket route \u2014 no mocks.
14398
+ *
14399
+ * Generated with src/routes/ws_${base}.ts by \`tina4nodejs generate websocket
14400
+ * ...\`. Confirms the handler registers on the REAL Router (importing runs the
14401
+ * module-level websocket() call) and drives the real handler for the "close"
14402
+ * event (no socket needed). The "message" branch is an AI-FILL stub that throws
14403
+ * until filled. Run with: npx tsx tests/ws_${base}.test.ts
14404
+ */
14405
+ import { Router } from "tina4-nodejs";
14406
+ import { ${handler} } from "../src/routes/ws_${base}.js"; // importing registers via websocket()`;
14407
+ const body = `assert("handler registered on the real router",
14408
+ Router.getWebSocketRoutes().some((r) => r.pattern === "${wsPath}"));
14409
+
14410
+ // The "close" branch returns cleanly without a live connection.
14411
+ const closed = await ${handler}(null as never, "close", "");
14412
+ assert("close event handled cleanly", closed === undefined);
14413
+
14414
+ // The "message" branch is an AI-FILL stub \u2014 it throws until filled.
14415
+ await assertThrows("message branch is a live stub (throws until filled)",
14416
+ () => ${handler}(null as never, "message", "hi"));`;
14417
+ writeTest(`ws_${base}`, standaloneTest(doc, body));
14418
+ }
14419
+ function emitListenerTest(event, slug) {
14420
+ const doc = `/**
14421
+ * Real event-bus test for the '${event}' listener \u2014 no mocks.
14422
+ *
14423
+ * Generated with src/listeners/${slug}.ts by \`tina4nodejs generate listener
14424
+ * ${event}\`. Confirms the listener binds on the REAL event bus (importing runs
14425
+ * the module-level Events.on) and that emitting the event reaches it. The
14426
+ * reaction body is an AI-FILL stub, so a strict emit re-raises here (proving it
14427
+ * ran). Run with: npx tsx tests/${slug}.test.ts
14428
+ */
14429
+ import { Events } from "tina4-nodejs";
14430
+ import "../src/listeners/${slug}.js"; // importing registers the listener via Events.on()`;
14431
+ const body = `assert("listener registered on the real event bus", Events.listeners("${event}").length >= 1);
14432
+
14433
+ // strict emit re-raises the stub error, proving the listener actually ran.
14434
+ await assertThrows("emitting the event reaches the (stub) listener",
14435
+ () => Events.emit("${event}", { strict: true }, { id: 1 }));`;
14436
+ writeTest(slug, standaloneTest(doc, body));
14437
+ }
14438
+ function emitAuthTest() {
14439
+ const doc = `/**
14440
+ * Real auth test \u2014 register / login / me via the real TestClient.
14441
+ *
14442
+ * Generated with the auth scaffold by \`tina4nodejs generate auth\`. No mocks:
14443
+ * real Router + route discovery, real Auth (PBKDF2 + JWT), real SQLite. register
14444
+ * + login are public; the token from login authenticates GET /api/auth/me.
14445
+ * Run with: npx tsx tests/auth.test.ts
14446
+ */
14447
+ import { dirname, resolve } from "node:path";
14448
+ import { fileURLToPath } from "node:url";
14449
+ import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
14450
+ import { initDatabase } from "tina4-nodejs/orm";
14451
+ import User from "../src/models/User.js";
14452
+
14453
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
14454
+ delete process.env.TINA4_API_KEY;
14455
+ const here = dirname(fileURLToPath(import.meta.url));`;
14456
+ const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
14457
+ await User.createTable();
14458
+ for (const existing of await User.all()) await existing.delete(); // start from an empty table
14459
+
14460
+ const router = new Router();
14461
+ for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
14462
+ const client = new TestClient(router);
14463
+
14464
+ const registered = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
14465
+ assert("register a new user -> 201", registered.status === 201);
14466
+
14467
+ const duplicate = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
14468
+ assert("duplicate register -> 409", duplicate.status === 409);
14469
+
14470
+ const login = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "secret12" } });
14471
+ assert("login -> 200", login.status === 200);
14472
+ const token = (login.json() as { token?: string }).token;
14473
+ assert("login returns a token", typeof token === "string" && token.length > 0);
14474
+
14475
+ const me = await client.get("/api/auth/me", { headers: { authorization: \`Bearer \${token}\` } });
14476
+ assert("authenticated GET /api/auth/me -> 200 (token accepted)", me.status === 200);
14477
+ assert("me returns the authenticated user's email",
14478
+ ((me.json() as { user?: { email?: string } }).user?.email) === "a@b.c");
14479
+
14480
+ const anon = await client.get("/api/auth/me");
14481
+ assert("anonymous GET /api/auth/me -> 401", anon.status === 401);
14482
+
14483
+ const bad = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "WRONG" } });
14484
+ assert("wrong password -> 401", bad.status === 401);`;
14485
+ writeTest("auth", standaloneTest(doc, body));
14486
+ }
14487
+ function emitMigrationTest(migrationName, table2) {
14488
+ const doc = `/**
14489
+ * Real migration test for ${migrationName} \u2014 no mocks, real SQLite.
14490
+ *
14491
+ * Generated with the migration by \`tina4nodejs generate migration
14492
+ * ${migrationName}\`. Applies the generated UP SQL against a fresh real
14493
+ * in-memory SQLite database and asserts the table exists, then applies the DOWN
14494
+ * SQL and asserts it is gone \u2014 the raw SQL the migration runner executes.
14495
+ * Run with: npx tsx tests/${table2}_migration.test.ts
14496
+ */
14497
+ import { dirname, join, resolve } from "node:path";
14498
+ import { fileURLToPath } from "node:url";
14499
+ import { readdirSync, readFileSync } from "node:fs";
14500
+ import { SQLiteAdapter } from "tina4-nodejs/orm";`;
14501
+ const body = `const here = dirname(fileURLToPath(import.meta.url));
14502
+ const migrationsDir = resolve(here, "../migrations");
14503
+
14504
+ const upFile = readdirSync(migrationsDir).find((f) => f.endsWith("_${migrationName}.sql") && !f.endsWith(".down.sql"));
14505
+ assert("generated UP migration file exists", Boolean(upFile));
14506
+ const downFile = upFile!.replace(/\\.sql$/, ".down.sql");
14507
+
14508
+ function statements(sql: string): string[] {
14509
+ const noComments = sql.split("\\n").filter((l) => !l.trim().startsWith("--")).join("\\n");
14510
+ return noComments.split(";").map((s) => s.trim()).filter(Boolean);
14511
+ }
14512
+
14513
+ const upText = readFileSync(join(migrationsDir, upFile!), "utf-8");
14514
+ const upSql = upText.split("-- UP")[1].split("-- DOWN")[0];
14515
+ const downSql = readFileSync(join(migrationsDir, downFile), "utf-8");
14516
+
14517
+ const db = new SQLiteAdapter(":memory:");
14518
+ for (const stmt of statements(upSql)) db.execute(stmt);
14519
+ assert("UP creates the ${table2} table", db.tableExists("${table2}"));
14520
+
14521
+ for (const stmt of statements(downSql)) db.execute(stmt);
14522
+ assert("DOWN drops the ${table2} table", db.tableExists("${table2}") === false);`;
14523
+ writeTest(`${table2}_migration`, standaloneTest(doc, body));
14524
+ }
14525
+ var FIELD_TYPE_MAP, SQL_RESERVED_TABLE_NAMES, RESOLUTION_ENVELOPE_VERSION, __resolution, TINA4_EDIT_MARKER, DEFAULT_FIELDS, GENERATORS, GENERATOR_LIST, NEXT_STEPS;
14526
+ var init_generate = __esm({
14527
+ "../cli/src/commands/generate.ts"() {
14528
+ "use strict";
14529
+ FIELD_TYPE_MAP = {
14530
+ string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
14531
+ str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
14532
+ int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
14533
+ integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
14534
+ float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
14535
+ number: { orm: '"number"', sql: "REAL", defaultVal: "0" },
14536
+ numeric: { orm: '"number"', sql: "REAL", defaultVal: "0" },
14537
+ decimal: { orm: '"number"', sql: "REAL", defaultVal: "0" },
14538
+ bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
14539
+ boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
14540
+ text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
14541
+ datetime: { orm: '"datetime"', sql: "TEXT", defaultVal: "NULL" },
14542
+ blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
14543
+ };
14544
+ SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
14545
+ "order",
14546
+ "group",
14547
+ "user",
14548
+ "table",
14549
+ "select",
14550
+ "from",
14551
+ "where",
14552
+ "index",
14553
+ "key",
14554
+ "values",
14555
+ "column",
14556
+ "constraint",
14557
+ "check",
14558
+ "default",
14559
+ "primary",
14560
+ "foreign",
14561
+ "references",
14562
+ "unique",
14563
+ "join",
14564
+ "union",
14565
+ "having",
14566
+ "limit",
14567
+ "offset",
14568
+ "desc",
14569
+ "asc",
14570
+ "case",
14571
+ "when",
14572
+ "then",
14573
+ "else",
14574
+ "end",
14575
+ "and",
14576
+ "or",
14577
+ "not",
14578
+ "null",
14579
+ "insert",
14580
+ "update",
14581
+ "delete",
14582
+ "create",
14583
+ "drop",
14584
+ "alter",
14585
+ "grant",
14586
+ "revoke",
14587
+ "commit",
14588
+ "rollback",
14589
+ "view",
14590
+ "trigger",
14591
+ "procedure",
14592
+ "function",
14593
+ "database",
14594
+ "schema",
14595
+ "session",
14596
+ "set",
14597
+ "into",
14598
+ "as",
14599
+ "on",
14600
+ "by",
14601
+ "inner",
14602
+ "outer",
14603
+ "left",
14604
+ "right",
14605
+ "full",
14606
+ "natural",
14607
+ "using",
14608
+ "with",
14609
+ "distinct",
14610
+ "between",
14611
+ "exists",
14612
+ "like",
14613
+ "in",
14614
+ "is",
14615
+ "all",
14616
+ "any",
14617
+ "cross",
14618
+ "add",
14619
+ "row",
14620
+ "rows",
14621
+ "range",
14622
+ "current",
14623
+ "to"
14624
+ ]);
14625
+ RESOLUTION_ENVELOPE_VERSION = "generate_v1_1";
14626
+ __resolution = {
14627
+ target: "",
14628
+ input: { name: "", fields: null },
14629
+ body: { transformations: [] },
14630
+ actionsTaken: [],
14631
+ dryRun: false,
14632
+ jsonMode: false
14633
+ };
14634
+ TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
14635
+ DEFAULT_FIELDS = [["name", "string"]];
14636
+ GENERATORS = {
14637
+ model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
14638
+ route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
14639
+ crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
14640
+ migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
14641
+ middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
14642
+ test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
14643
+ form: { handler: generateForm, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" },
14644
+ view: { handler: generateView, usage: '<Name> [--fields "..."]', summary: "List + detail view templates" },
14645
+ auth: { handler: (_n, f) => generateAuth(f), usage: "", summary: "Login/register routes (public) + User model + templates" },
14646
+ service: { handler: generateService, usage: '<Name> [--every 5m | --cron "..."]', summary: "Scheduled ServiceRunner task (src/services/)" },
14647
+ queue: { handler: generateQueue, usage: "<topic>", summary: "Producer + consumer daemon worker (src/services/)" },
14648
+ validator: { handler: generateValidator, usage: "<Name>", summary: "Request-body Validator (src/validators/)" },
14649
+ seeder: { handler: generateSeeder, usage: "<Model>", summary: "FakeData + seedOrm seeder (src/seeds/)" },
14650
+ websocket: { handler: generateWebsocket, usage: "<path>", summary: "websocket() handler (src/routes/)" },
14651
+ listener: { handler: generateListener, usage: "<event>", summary: "Events.on(event) listener (src/listeners/)" }
14652
+ };
14653
+ GENERATOR_LIST = Object.keys(GENERATORS).join(", ");
14654
+ NEXT_STEPS = {
14655
+ model: ({ name, table: table2 }) => [
14656
+ `Edit src/models/${name}.ts to add fields beyond the default 'name'`,
14657
+ `Apply the migration: npx tina4nodejs migrate`,
14658
+ `Run its test: npx tsx tests/${table2}_model.test.ts`,
14659
+ `Add CRUD scaffolding: npx tina4nodejs generate crud ${name}`
14660
+ ],
14661
+ route: ({ name, table: table2 }) => [
14662
+ `Fill the AI-FILL stubs in src/routes/api/${name.replace(/^\//, "")}/`,
14663
+ `Run its test: npx tsx tests/${table2}.test.ts`,
14664
+ `Serve and try: npx tina4nodejs serve -> curl http://localhost:7148/api/${name.replace(/^\//, "")}`
14665
+ ],
14666
+ crud: ({ name, table: table2 }) => [
14667
+ `Apply the migration: npx tina4nodejs migrate`,
14668
+ `Serve and try: npx tina4nodejs serve -> visit /swagger`,
14669
+ `Run the gate test: npx tsx tests/${toPlural(table2)}.test.ts`,
14670
+ `Change fields: edit src/models/${name}.ts then re-run generate crud`
14671
+ ],
14672
+ migration: () => [
14673
+ `Apply pending migrations: npx tina4nodejs migrate`,
14674
+ `Check status: npx tina4nodejs migrate:status`,
14675
+ `Roll back the batch: npx tina4nodejs migrate:rollback`
14676
+ ],
14677
+ middleware: ({ name }) => [
14678
+ `Wire it: router.middleware(before${name}, after${name}) \u2014 or bind per-route`,
14679
+ `Run its test: npx tsx tests/${toSnake(name)}.test.ts`
14680
+ ],
14681
+ test: ({ name }) => [
14682
+ `Fill the TODOs in tests/${toSnake(name)}.test.ts`,
14683
+ `Run it: npx tsx tests/${toSnake(name)}.test.ts`
14684
+ ],
14685
+ form: ({ name, table: table2 }) => [
14686
+ `Render from a route: res.render("forms/${table2}.twig", { item })`,
14687
+ `Add the POST route: npx tina4nodejs generate route ${toPlural(table2)} --model ${name}`
14688
+ ],
14689
+ view: ({ table: table2 }) => [
14690
+ `Wire routes to render list -> ${toPlural(table2)}.twig, detail -> ${table2}.twig`,
14691
+ `Customize the templates in src/templates/pages/`
14692
+ ],
14693
+ auth: () => [
14694
+ `Apply the migration: npx tina4nodejs migrate`,
14695
+ `Run the auth test: npx tsx tests/auth.test.ts`,
14696
+ `Try register: curl -X POST http://localhost:7148/api/auth/register -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`,
14697
+ `Login: curl -X POST http://localhost:7148/api/auth/login -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`
14698
+ ],
14699
+ service: ({ name }) => [
14700
+ `Wire ServiceRunner in app.ts: await ServiceRunner.discover("src/services"); ServiceRunner.start();`,
14701
+ `Fill the task body in src/services/${toSnake(name)}.ts`,
14702
+ `Run its test: npx tsx tests/${toSnake(name)}.test.ts`
14703
+ ],
14704
+ queue: ({ name }) => {
14705
+ const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
14706
+ return [
14707
+ `Fill handle${toPascal(name)}() in src/services/${slug}_consumer.ts`,
14708
+ `Produce a job: publish${toPascal(name)}({ ... })`,
14709
+ `Run the worker: npx tina4nodejs queue work ${name}`,
14710
+ `Run its test: npx tsx tests/${slug}.test.ts`
14711
+ ];
14712
+ },
14713
+ validator: ({ name }) => [
14714
+ `Add rules in src/validators/${toSnake(name)}.ts (.email/.minLength/.integer/.inList/.pattern)`,
14715
+ `Run its test: npx tsx tests/${toSnake(name)}.test.ts`
14716
+ ],
14717
+ seeder: ({ name, table: table2 }) => [
14718
+ `Override any fields that need a specific shape in src/seeds/${table2}_seeder.ts`,
14719
+ `Seed the table: npx tina4nodejs seed`,
14720
+ `Run its test: npx tsx tests/${table2}_seeder.test.ts`
14721
+ ],
14722
+ websocket: ({ name }) => {
14723
+ const raw = name.trim();
14724
+ const slugRaw = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
14725
+ const base = slugRaw.startsWith("ws_") ? slugRaw.slice(3) : slugRaw;
14726
+ return [
14727
+ `Import once in app.ts to register: import "./src/routes/ws_${base}.js";`,
14728
+ `Fill the "message" branch in src/routes/ws_${base}.ts`,
14729
+ `Run its test: npx tsx tests/ws_${base}.test.ts`
14730
+ ];
14731
+ },
14732
+ listener: ({ name }) => {
14733
+ const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
14734
+ return [
14735
+ `Import once in app.ts to register: import "./src/listeners/${slug}.js";`,
14736
+ `Fill the reaction in src/listeners/${slug}.ts`,
14737
+ `Run its test: npx tsx tests/${slug}.test.ts`
14738
+ ];
14739
+ }
14740
+ };
14741
+ }
14742
+ });
14743
+
12685
14744
  // ../core/src/mcp.ts
12686
14745
  var mcp_exports = {};
12687
14746
  __export(mcp_exports, {
@@ -13182,10 +15241,10 @@ function registerDevTools(server) {
13182
15241
  "swagger_spec",
13183
15242
  (_args) => {
13184
15243
  try {
13185
- const { generate: generate2 } = reqSibling("swagger");
15244
+ const { generate: generate3 } = reqSibling("swagger");
13186
15245
  const { defaultRouter: defaultRouter2 } = req("./router.js");
13187
15246
  const routes = defaultRouter2?.getRoutes?.() ?? [];
13188
- return generate2?.(routes, []) ?? { info: "Swagger not available" };
15247
+ return generate3?.(routes, []) ?? { info: "Swagger not available" };
13189
15248
  } catch (e) {
13190
15249
  return { error: e.message };
13191
15250
  }
@@ -13344,20 +15403,43 @@ function registerDevTools(server) {
13344
15403
  );
13345
15404
  server.registerTool(
13346
15405
  "migration_create",
13347
- (args) => {
13348
- const desc = args.description.replace(/\s+/g, "_").toLowerCase();
13349
- const migrationsDir = path3.join(projectRoot3, "migrations");
13350
- if (!fs4.existsSync(migrationsDir)) {
13351
- fs4.mkdirSync(migrationsDir, { recursive: true });
13352
- }
13353
- const existing = fs4.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
13354
- const nextNum = String(existing.length + 1).padStart(6, "0");
13355
- const filename = `${nextNum}_${desc}.sql`;
13356
- fs4.writeFileSync(path3.join(migrationsDir, filename), `-- Migration: ${args.description}
13357
- `, "utf-8");
13358
- return { created: filename };
15406
+ async (args) => {
15407
+ try {
15408
+ const rawDesc = String(args.description ?? "").trim();
15409
+ if (!rawDesc) return { ok: false, error: "description is required" };
15410
+ const slug = rawDesc.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
15411
+ if (!slug) return { ok: false, error: "description sanitised to an empty slug" };
15412
+ const migrationsDir = path3.join(projectRoot3, "migrations");
15413
+ if (fs4.existsSync(migrationsDir)) {
15414
+ const upSuffix = `_${slug}.sql`;
15415
+ const downSuffix = `_${slug}.down.sql`;
15416
+ const existing = fs4.readdirSync(migrationsDir).filter(
15417
+ (f) => f.endsWith(upSuffix) && !f.endsWith(downSuffix) || f.endsWith(downSuffix)
15418
+ );
15419
+ if (existing.length > 0) {
15420
+ return {
15421
+ ok: false,
15422
+ error: `A migration with slug "${slug}" already exists`,
15423
+ existing
15424
+ };
15425
+ }
15426
+ }
15427
+ const originalCwd = process.cwd();
15428
+ try {
15429
+ process.chdir(projectRoot3);
15430
+ const gen = await Promise.resolve().then(() => (init_generate(), generate_exports));
15431
+ const envelope = await gen.generateProgrammatic("migration", slug, ["--no-test"]);
15432
+ const migrationPath = envelope.resolution?.migration_path;
15433
+ const created = migrationPath ? path3.basename(migrationPath) : "";
15434
+ return { ok: true, created, resolution: envelope };
15435
+ } finally {
15436
+ process.chdir(originalCwd);
15437
+ }
15438
+ } catch (e) {
15439
+ return { ok: false, error: e.message };
15440
+ }
13359
15441
  },
13360
- "Create a new migration file",
15442
+ "Create a new migration file (delegates to `generate migration` \u2014 emits the ADR-0063 generate_v1_1 envelope + timestamped filename)",
13361
15443
  schemaFromParams([{ name: "description", type: "string" }])
13362
15444
  );
13363
15445
  server.registerTool(
@@ -14125,14 +16207,14 @@ data: ${channel.buffer.shift()}
14125
16207
  `;
14126
16208
  continue;
14127
16209
  }
14128
- const gotMessage = await new Promise((resolve20) => {
16210
+ const gotMessage = await new Promise((resolve21) => {
14129
16211
  const timer = setTimeout(() => {
14130
16212
  channel.wake = null;
14131
- resolve20(false);
16213
+ resolve21(false);
14132
16214
  }, keepaliveMs);
14133
16215
  channel.wake = () => {
14134
16216
  clearTimeout(timer);
14135
- resolve20(true);
16217
+ resolve21(true);
14136
16218
  };
14137
16219
  });
14138
16220
  if (!gotMessage) yield `: keep-alive
@@ -15451,8 +17533,8 @@ __export(context_exports, {
15451
17533
  fts5Supported: () => fts5Supported
15452
17534
  });
15453
17535
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
15454
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
15455
- import { basename as basename4, dirname as dirname8, extname as extname4, isAbsolute as isAbsolute4, join as join16, relative as relative3, resolve as resolve7 } from "node:path";
17536
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
17537
+ 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";
15456
17538
  function fts5Supported() {
15457
17539
  try {
15458
17540
  const conn = new DatabaseSync2(":memory:");
@@ -15475,13 +17557,13 @@ function realResolve(abs) {
15475
17557
  } catch {
15476
17558
  }
15477
17559
  try {
15478
- return join16(realpathSync5(dirname8(abs)), basename4(abs));
17560
+ return join17(realpathSync5(dirname8(abs)), basename4(abs));
15479
17561
  } catch {
15480
17562
  return abs;
15481
17563
  }
15482
17564
  }
15483
17565
  function dbKey(db) {
15484
- return resolve7(db ? String(db) : join16(process.cwd(), ".tina4", "context.db"));
17566
+ return resolve8(db ? String(db) : join17(process.cwd(), ".tina4", "context.db"));
15485
17567
  }
15486
17568
  function defaultContext(root, db) {
15487
17569
  const key = dbKey(db);
@@ -15565,7 +17647,7 @@ var init_context = __esm({
15565
17647
  if (!this.available) return;
15566
17648
  const parent = dirname8(this.path);
15567
17649
  if (parent !== "" && parent !== ".") {
15568
- mkdirSync9(parent, { recursive: true });
17650
+ mkdirSync10(parent, { recursive: true });
15569
17651
  }
15570
17652
  this.conn = new DatabaseSync2(this.path);
15571
17653
  this.ensureTable();
@@ -15637,7 +17719,7 @@ var init_context = __esm({
15637
17719
  */
15638
17720
  indexRoot(root) {
15639
17721
  if (!this.available) return 0;
15640
- const rootAbs = realResolve(resolve7(String(root)));
17722
+ const rootAbs = realResolve(resolve8(String(root)));
15641
17723
  this.root = rootAbs;
15642
17724
  let total = 0;
15643
17725
  const walk2 = (dir) => {
@@ -15654,11 +17736,11 @@ var init_context = __esm({
15654
17736
  files.sort();
15655
17737
  for (const fn of files) {
15656
17738
  if (!_Context.eligible(fn)) continue;
15657
- const full = join16(dir, fn);
15658
- const rel = relative3(rootAbs, full);
17739
+ const full = join17(dir, fn);
17740
+ const rel = relative4(rootAbs, full);
15659
17741
  total += this.indexPath(full, rel);
15660
17742
  }
15661
- for (const d of subdirs) walk2(join16(dir, d));
17743
+ for (const d of subdirs) walk2(join17(dir, d));
15662
17744
  };
15663
17745
  walk2(rootAbs);
15664
17746
  return total;
@@ -15674,9 +17756,9 @@ var init_context = __esm({
15674
17756
  reindexFile(changedPath) {
15675
17757
  if (!this.available || this.root === null) return -1;
15676
17758
  const raw = String(changedPath);
15677
- const abs = isAbsolute4(raw) ? raw : join16(process.cwd(), raw);
15678
- const resolved = realResolve(resolve7(abs));
15679
- const rel = relative3(this.root, resolved);
17759
+ const abs = isAbsolute4(raw) ? raw : join17(process.cwd(), raw);
17760
+ const resolved = realResolve(resolve8(abs));
17761
+ const rel = relative4(this.root, resolved);
15680
17762
  if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) {
15681
17763
  return -1;
15682
17764
  }
@@ -15687,7 +17769,7 @@ var init_context = __esm({
15687
17769
  }
15688
17770
  if (!_Context.eligible(basename4(rel))) return -1;
15689
17771
  const stored = rel;
15690
- if (!existsSync14(abs)) {
17772
+ if (!existsSync15(abs)) {
15691
17773
  this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
15692
17774
  return 0;
15693
17775
  }
@@ -16433,7 +18515,7 @@ var init_websocket = __esm({
16433
18515
  * Start the WebSocket server.
16434
18516
  */
16435
18517
  async start() {
16436
- return new Promise((resolve20, reject) => {
18518
+ return new Promise((resolve21, reject) => {
16437
18519
  this.server = createServer((req2, res) => {
16438
18520
  res.writeHead(426, { "Content-Type": "text/plain" });
16439
18521
  res.end("Upgrade Required");
@@ -16443,7 +18525,7 @@ var init_websocket = __esm({
16443
18525
  });
16444
18526
  this.server.listen(this.port, () => {
16445
18527
  this.startIdleReaper();
16446
- resolve20();
18528
+ resolve21();
16447
18529
  });
16448
18530
  this.server.on("error", (err) => {
16449
18531
  this.emit("error", err);
@@ -17664,8 +19746,8 @@ var init_job = __esm({
17664
19746
  });
17665
19747
 
17666
19748
  // ../core/src/queueBackends/liteBackend.ts
17667
- import { mkdirSync as mkdirSync10, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8, unlinkSync as unlinkSync6, existsSync as existsSync15 } from "node:fs";
17668
- import { join as join17 } from "node:path";
19749
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
19750
+ import { join as join18 } from "node:path";
17669
19751
  import { randomUUID as randomUUID5 } from "node:crypto";
17670
19752
  var LiteBackend;
17671
19753
  var init_liteBackend = __esm({
@@ -17689,22 +19771,22 @@ var init_liteBackend = __esm({
17689
19771
  this.visibilityTimeout = visibilityTimeout;
17690
19772
  }
17691
19773
  ensureDir(queue) {
17692
- const dir = join17(this.basePath, queue);
17693
- mkdirSync10(dir, { recursive: true });
19774
+ const dir = join18(this.basePath, queue);
19775
+ mkdirSync11(dir, { recursive: true });
17694
19776
  return dir;
17695
19777
  }
17696
19778
  ensureFailedDir(queue) {
17697
- const dir = join17(this.basePath, queue, "failed");
17698
- mkdirSync10(dir, { recursive: true });
19779
+ const dir = join18(this.basePath, queue, "failed");
19780
+ mkdirSync11(dir, { recursive: true });
17699
19781
  return dir;
17700
19782
  }
17701
19783
  ensureReservedDir(queue) {
17702
- const dir = join17(this.basePath, queue, "reserved");
17703
- mkdirSync10(dir, { recursive: true });
19784
+ const dir = join18(this.basePath, queue, "reserved");
19785
+ mkdirSync11(dir, { recursive: true });
17704
19786
  return dir;
17705
19787
  }
17706
19788
  reservedPath(queue, jobId) {
17707
- return join17(this.ensureReservedDir(queue), `${jobId}.queue-data`);
19789
+ return join18(this.ensureReservedDir(queue), `${jobId}.queue-data`);
17708
19790
  }
17709
19791
  nowIso() {
17710
19792
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -17742,7 +19824,7 @@ var init_liteBackend = __esm({
17742
19824
  error: void 0
17743
19825
  };
17744
19826
  const prefix = this.nextPrefix();
17745
- writeFileSync8(join17(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
19827
+ writeFileSync9(join18(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
17746
19828
  return id;
17747
19829
  }
17748
19830
  /**
@@ -17761,7 +19843,7 @@ var init_liteBackend = __esm({
17761
19843
  }
17762
19844
  const candidates = [];
17763
19845
  for (const filename of filenames) {
17764
- const filePath = join17(dir, filename);
19846
+ const filePath = join18(dir, filename);
17765
19847
  let job;
17766
19848
  try {
17767
19849
  job = JSON.parse(readFileSync14(filePath, "utf-8"));
@@ -17804,7 +19886,7 @@ var init_liteBackend = __esm({
17804
19886
  createdAt: job.createdAt ?? now,
17805
19887
  topic: job.topic ?? queue
17806
19888
  };
17807
- writeFileSync8(this.reservedPath(queue, record.id), JSON.stringify(record, null, 2));
19889
+ writeFileSync9(this.reservedPath(queue, record.id), JSON.stringify(record, null, 2));
17808
19890
  }
17809
19891
  /**
17810
19892
  * Return expired reservations to the queue (at-least-once delivery).
@@ -17825,7 +19907,7 @@ var init_liteBackend = __esm({
17825
19907
  return;
17826
19908
  }
17827
19909
  for (const filename of filenames) {
17828
- const filePath = join17(reservedDir, filename);
19910
+ const filePath = join18(reservedDir, filename);
17829
19911
  let record;
17830
19912
  try {
17831
19913
  record = JSON.parse(readFileSync14(filePath, "utf-8"));
@@ -17863,7 +19945,7 @@ var init_liteBackend = __esm({
17863
19945
  this.reclaimExpired(queue, bridge.getMaxRetries(), this.nowIso());
17864
19946
  const now = this.nowIso();
17865
19947
  for (const [filename, job] of this.availableCandidates(queue, now)) {
17866
- const filePath = join17(dir, filename);
19948
+ const filePath = join18(dir, filename);
17867
19949
  job.topic = queue;
17868
19950
  job.priority = job.priority ?? 0;
17869
19951
  this.writeReserved(queue, job);
@@ -17888,7 +19970,7 @@ var init_liteBackend = __esm({
17888
19970
  const results = [];
17889
19971
  for (const [filename, job] of this.availableCandidates(queue, now)) {
17890
19972
  if (results.length >= count) break;
17891
- const filePath = join17(dir, filename);
19973
+ const filePath = join18(dir, filename);
17892
19974
  job.topic = queue;
17893
19975
  job.priority = job.priority ?? 0;
17894
19976
  this.writeReserved(queue, job);
@@ -17941,7 +20023,7 @@ var init_liteBackend = __esm({
17941
20023
  let count = 0;
17942
20024
  for (const file of files) {
17943
20025
  try {
17944
- const job = JSON.parse(readFileSync14(join17(scanDir, file), "utf-8"));
20026
+ const job = JSON.parse(readFileSync14(join18(scanDir, file), "utf-8"));
17945
20027
  if (job.status === status2) count++;
17946
20028
  } catch {
17947
20029
  }
@@ -17954,28 +20036,28 @@ var init_liteBackend = __esm({
17954
20036
  try {
17955
20037
  const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
17956
20038
  for (const file of files) {
17957
- unlinkSync6(join17(dir, file));
20039
+ unlinkSync6(join18(dir, file));
17958
20040
  count++;
17959
20041
  }
17960
20042
  } catch {
17961
20043
  }
17962
- const failedDir = join17(dir, "failed");
20044
+ const failedDir = join18(dir, "failed");
17963
20045
  try {
17964
- if (existsSync15(failedDir)) {
20046
+ if (existsSync16(failedDir)) {
17965
20047
  const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
17966
20048
  for (const file of files) {
17967
- unlinkSync6(join17(failedDir, file));
20049
+ unlinkSync6(join18(failedDir, file));
17968
20050
  count++;
17969
20051
  }
17970
20052
  }
17971
20053
  } catch {
17972
20054
  }
17973
- const reservedDir = join17(dir, "reserved");
20055
+ const reservedDir = join18(dir, "reserved");
17974
20056
  try {
17975
- if (existsSync15(reservedDir)) {
20057
+ if (existsSync16(reservedDir)) {
17976
20058
  const files = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
17977
20059
  for (const file of files) {
17978
- unlinkSync6(join17(reservedDir, file));
20060
+ unlinkSync6(join18(reservedDir, file));
17979
20061
  count++;
17980
20062
  }
17981
20063
  }
@@ -17998,7 +20080,7 @@ var init_liteBackend = __esm({
17998
20080
  const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
17999
20081
  for (const file of files) {
18000
20082
  try {
18001
- const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
20083
+ const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
18002
20084
  const attempts = job.attempts || 0;
18003
20085
  if (attempts > 0 && attempts < maxRetries) {
18004
20086
  results.push(job);
@@ -18021,9 +20103,9 @@ var init_liteBackend = __esm({
18021
20103
  try {
18022
20104
  const queues = readdirSync8(this.basePath);
18023
20105
  for (const q of queues) {
18024
- const failedDir = join17(this.basePath, q, "failed");
18025
- const filePath = join17(failedDir, `${jobId}.queue-data`);
18026
- if (existsSync15(filePath)) {
20106
+ const failedDir = join18(this.basePath, q, "failed");
20107
+ const filePath = join18(failedDir, `${jobId}.queue-data`);
20108
+ if (existsSync16(filePath)) {
18027
20109
  const job = JSON.parse(readFileSync14(filePath, "utf-8"));
18028
20110
  job.status = "pending";
18029
20111
  job.attempts = (job.attempts || 0) + 1;
@@ -18031,8 +20113,8 @@ var init_liteBackend = __esm({
18031
20113
  job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
18032
20114
  job.delayUntil = delaySeconds ? new Date(Date.now() + delaySeconds * 1e3).toISOString() : null;
18033
20115
  const prefix = this.nextPrefix();
18034
- const queueDir = join17(this.basePath, q);
18035
- writeFileSync8(join17(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
20116
+ const queueDir = join18(this.basePath, q);
20117
+ writeFileSync9(join18(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
18036
20118
  unlinkSync6(filePath);
18037
20119
  return true;
18038
20120
  }
@@ -18048,7 +20130,7 @@ var init_liteBackend = __esm({
18048
20130
  const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
18049
20131
  for (const file of files) {
18050
20132
  try {
18051
- const job = JSON.parse(readFileSync14(join17(failedDir, file), "utf-8"));
20133
+ const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
18052
20134
  if ((job.attempts || 0) >= maxRetries) {
18053
20135
  job.status = "dead";
18054
20136
  results.push(job);
@@ -18069,7 +20151,7 @@ var init_liteBackend = __esm({
18069
20151
  const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
18070
20152
  for (const file of files) {
18071
20153
  try {
18072
- unlinkSync6(join17(failedDir, file));
20154
+ unlinkSync6(join18(failedDir, file));
18073
20155
  count++;
18074
20156
  } catch {
18075
20157
  }
@@ -18082,9 +20164,9 @@ var init_liteBackend = __esm({
18082
20164
  const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
18083
20165
  for (const file of files) {
18084
20166
  try {
18085
- const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
20167
+ const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
18086
20168
  if (job.status === status2) {
18087
- unlinkSync6(join17(dir, file));
20169
+ unlinkSync6(join18(dir, file));
18088
20170
  count++;
18089
20171
  }
18090
20172
  } catch {
@@ -18108,7 +20190,7 @@ var init_liteBackend = __esm({
18108
20190
  const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
18109
20191
  for (const file of files) {
18110
20192
  try {
18111
- const filePath = join17(failedDir, file);
20193
+ const filePath = join18(failedDir, file);
18112
20194
  const job = JSON.parse(readFileSync14(filePath, "utf-8"));
18113
20195
  if ((job.attempts || 0) >= maxRetries) {
18114
20196
  continue;
@@ -18118,7 +20200,7 @@ var init_liteBackend = __esm({
18118
20200
  job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
18119
20201
  job.delayUntil = null;
18120
20202
  const prefix = this.nextPrefix();
18121
- writeFileSync8(join17(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
20203
+ writeFileSync9(join18(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
18122
20204
  unlinkSync6(filePath);
18123
20205
  count++;
18124
20206
  } catch {
@@ -18138,7 +20220,7 @@ var init_liteBackend = __esm({
18138
20220
  }
18139
20221
  for (const file of files) {
18140
20222
  if (!file.includes(id)) continue;
18141
- const filePath = join17(dir, file);
20223
+ const filePath = join18(dir, file);
18142
20224
  let job;
18143
20225
  try {
18144
20226
  job = JSON.parse(readFileSync14(filePath, "utf-8"));
@@ -18186,7 +20268,7 @@ var init_liteBackend = __esm({
18186
20268
  error
18187
20269
  };
18188
20270
  const prefix = this.nextPrefix();
18189
- writeFileSync8(join17(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
20271
+ writeFileSync9(join18(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
18190
20272
  }
18191
20273
  /**
18192
20274
  * Move the job to the dead-letter (failed/) directory. Terminal until a
@@ -18206,7 +20288,7 @@ var init_liteBackend = __esm({
18206
20288
  error,
18207
20289
  failedAt: (/* @__PURE__ */ new Date()).toISOString()
18208
20290
  };
18209
- writeFileSync8(join17(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
20291
+ writeFileSync9(join18(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
18210
20292
  }
18211
20293
  /**
18212
20294
  * Record a failed attempt.
@@ -18242,7 +20324,7 @@ var init_liteBackend = __esm({
18242
20324
  retryJob(queue, job, delaySeconds) {
18243
20325
  this.clearReservation(queue, job.id);
18244
20326
  try {
18245
- unlinkSync6(join17(this.ensureFailedDir(queue), `${job.id}.queue-data`));
20327
+ unlinkSync6(join18(this.ensureFailedDir(queue), `${job.id}.queue-data`));
18246
20328
  } catch {
18247
20329
  }
18248
20330
  job.attempts = (job.attempts || 0) + 1;
@@ -18661,7 +20743,7 @@ var init_queue = __esm({
18661
20743
  const jobs = this.popBatch(resolvedBatchSize);
18662
20744
  if (jobs.length === 0) {
18663
20745
  if (resolvedPollInterval <= 0) break;
18664
- await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
20746
+ await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
18665
20747
  continue;
18666
20748
  }
18667
20749
  yield jobs;
@@ -18671,7 +20753,7 @@ var init_queue = __esm({
18671
20753
  const raw = this.pop();
18672
20754
  if (raw === null) {
18673
20755
  if (resolvedPollInterval <= 0) break;
18674
- await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
20756
+ await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
18675
20757
  continue;
18676
20758
  }
18677
20759
  yield createJob(raw, this);
@@ -20746,8 +22828,8 @@ ${end}
20746
22828
 
20747
22829
  // ../core/src/devAdmin.ts
20748
22830
  import { cpus as osCpus } from "node:os";
20749
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, existsSync as existsSync19, readdirSync as readdirSync12, mkdirSync as mkdirSync13, copyFileSync, statSync as statSync14 } from "node:fs";
20750
- import { join as join21, dirname as dirname10, resolve as resolve11, relative as relative7 } from "node:path";
22831
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync12, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync14 } from "node:fs";
22832
+ import { join as join22, dirname as dirname10, resolve as resolve12, relative as relative8 } from "node:path";
20751
22833
  import { fileURLToPath as fileURLToPath5 } from "node:url";
20752
22834
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
20753
22835
  function escapeHtml(value) {
@@ -20861,12 +22943,12 @@ function mapQueueJob(job, topic, status2) {
20861
22943
  };
20862
22944
  }
20863
22945
  function readQueueDir(dir, topic, status2) {
20864
- if (!existsSync19(dir)) return [];
22946
+ if (!existsSync20(dir)) return [];
20865
22947
  const jobs = [];
20866
22948
  for (const filename of readdirSync12(dir).sort()) {
20867
22949
  if (!filename.endsWith(".queue-data")) continue;
20868
22950
  try {
20869
- jobs.push(mapQueueJob(JSON.parse(readFileSync18(join21(dir, filename), "utf-8")), topic, status2));
22951
+ jobs.push(mapQueueJob(JSON.parse(readFileSync18(join22(dir, filename), "utf-8")), topic, status2));
20870
22952
  } catch {
20871
22953
  }
20872
22954
  }
@@ -20983,8 +23065,8 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
20983
23065
  function resolveDevEnvVar(key) {
20984
23066
  const live = process.env[key];
20985
23067
  if (live !== void 0 && live !== "") return live;
20986
- const envPath = join21(process.cwd(), ".env");
20987
- if (!existsSync19(envPath)) return "";
23068
+ const envPath = join22(process.cwd(), ".env");
23069
+ if (!existsSync20(envPath)) return "";
20988
23070
  for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
20989
23071
  const t = line.trim();
20990
23072
  if (!t || t.startsWith("#") || !t.includes("=")) continue;
@@ -20994,8 +23076,8 @@ function resolveDevEnvVar(key) {
20994
23076
  return "";
20995
23077
  }
20996
23078
  function upsertDevEnvVar(key, value) {
20997
- const envPath = join21(process.cwd(), ".env");
20998
- const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
23079
+ const envPath = join22(process.cwd(), ".env");
23080
+ const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
20999
23081
  let found = false;
21000
23082
  const out = [];
21001
23083
  for (const line of lines) {
@@ -21010,7 +23092,7 @@ function upsertDevEnvVar(key, value) {
21010
23092
  } else out.push(line);
21011
23093
  }
21012
23094
  if (!found) out.push(`${key}=${value}`);
21013
- writeFileSync12(envPath, out.join("\n").replace(/\n+$/, "") + "\n");
23095
+ writeFileSync13(envPath, out.join("\n").replace(/\n+$/, "") + "\n");
21014
23096
  }
21015
23097
  function formatUptime(seconds) {
21016
23098
  const d = Math.floor(seconds / 86400);
@@ -21025,9 +23107,9 @@ function formatUptime(seconds) {
21025
23107
  return parts.join(" ");
21026
23108
  }
21027
23109
  function parseEnvFile() {
21028
- const envPath = join21(process.cwd(), ".env");
23110
+ const envPath = join22(process.cwd(), ".env");
21029
23111
  const result = {};
21030
- if (!existsSync19(envPath)) return result;
23112
+ if (!existsSync20(envPath)) return result;
21031
23113
  const lines = readFileSync18(envPath, "utf-8").split("\n");
21032
23114
  for (const line of lines) {
21033
23115
  const trimmed = line.trim();
@@ -21039,9 +23121,9 @@ function parseEnvFile() {
21039
23121
  }
21040
23122
  function walkDirRecursive(dir) {
21041
23123
  const results = [];
21042
- if (!existsSync19(dir)) return results;
23124
+ if (!existsSync20(dir)) return results;
21043
23125
  for (const entry of readdirSync12(dir)) {
21044
- const full = join21(dir, entry);
23126
+ const full = join22(dir, entry);
21045
23127
  if (statSync14(full).isDirectory()) {
21046
23128
  results.push(...walkDirRecursive(full));
21047
23129
  } else {
@@ -21058,25 +23140,25 @@ function handleGalleryDeploy(router) {
21058
23140
  res.json({ error: "No gallery item specified" }, 400);
21059
23141
  return;
21060
23142
  }
21061
- const galleryDir = resolve11(__devAdminDirname, "..", "gallery");
21062
- const gallerySrc = join21(galleryDir, name, "src");
21063
- if (!existsSync19(gallerySrc)) {
23143
+ const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
23144
+ const gallerySrc = join22(galleryDir, name, "src");
23145
+ if (!existsSync20(gallerySrc)) {
21064
23146
  res.json({ error: `Gallery item '${name}' not found` }, 404);
21065
23147
  return;
21066
23148
  }
21067
- const projectSrc = resolve11(process.cwd(), "src");
23149
+ const projectSrc = resolve12(process.cwd(), "src");
21068
23150
  const copied = [];
21069
23151
  const allFiles = walkDirRecursive(gallerySrc);
21070
23152
  for (const srcFile of allFiles) {
21071
- const rel = relative7(gallerySrc, srcFile);
21072
- const dest = join21(projectSrc, rel);
21073
- mkdirSync13(dirname10(dest), { recursive: true });
23153
+ const rel = relative8(gallerySrc, srcFile);
23154
+ const dest = join22(projectSrc, rel);
23155
+ mkdirSync14(dirname10(dest), { recursive: true });
21074
23156
  copyFileSync(srcFile, dest);
21075
23157
  copied.push(rel);
21076
23158
  }
21077
23159
  try {
21078
- const routesDir = resolve11(process.cwd(), "src", "routes");
21079
- if (existsSync19(routesDir)) {
23160
+ const routesDir = resolve12(process.cwd(), "src", "routes");
23161
+ if (existsSync20(routesDir)) {
21080
23162
  const { discoverRoutes: discoverRoutes2 } = await Promise.resolve().then(() => (init_routeDiscovery(), routeDiscovery_exports));
21081
23163
  const routes = await discoverRoutes2(routesDir);
21082
23164
  for (const route of routes) {
@@ -21092,7 +23174,7 @@ function handleGalleryDeploy(router) {
21092
23174
  };
21093
23175
  }
21094
23176
  function safeJoin(projectRoot3, rel) {
21095
- const resolved = resolve11(projectRoot3, rel);
23177
+ const resolved = resolve12(projectRoot3, rel);
21096
23178
  if (!resolved.startsWith(projectRoot3)) return null;
21097
23179
  return resolved;
21098
23180
  }
@@ -22055,16 +24137,16 @@ var init_devAdmin = __esm({
22055
24137
  failed: queue.size("failed"),
22056
24138
  reserved: queue.size("reserved")
22057
24139
  };
22058
- const topicDir = join21(queueBasePath2(), topic);
24140
+ const topicDir = join22(queueBasePath2(), topic);
22059
24141
  const jobs = [];
22060
24142
  if (!statusFilter || statusFilter === "pending") {
22061
24143
  jobs.push(...readQueueDir(topicDir, topic, "pending"));
22062
24144
  }
22063
24145
  if (!statusFilter || statusFilter === "reserved") {
22064
- jobs.push(...readQueueDir(join21(topicDir, "reserved"), topic, "reserved"));
24146
+ jobs.push(...readQueueDir(join22(topicDir, "reserved"), topic, "reserved"));
22065
24147
  }
22066
24148
  if (!statusFilter || statusFilter === "failed" || statusFilter === "dead") {
22067
- jobs.push(...readQueueDir(join21(topicDir, "failed"), topic, "dead_letter"));
24149
+ jobs.push(...readQueueDir(join22(topicDir, "failed"), topic, "dead_letter"));
22068
24150
  }
22069
24151
  res.json({ stats, jobs });
22070
24152
  } catch (e) {
@@ -22080,10 +24162,10 @@ var init_devAdmin = __esm({
22080
24162
  const { queueBasePath: queueBasePath2 } = await Promise.resolve().then(() => (init_queue(), queue_exports));
22081
24163
  const queueDir = queueBasePath2();
22082
24164
  let topics = [];
22083
- if (existsSync19(queueDir)) {
24165
+ if (existsSync20(queueDir)) {
22084
24166
  topics = readdirSync12(queueDir).filter((d) => {
22085
24167
  try {
22086
- return statSync14(join21(queueDir, d)).isDirectory();
24168
+ return statSync14(join22(queueDir, d)).isDirectory();
22087
24169
  } catch {
22088
24170
  return false;
22089
24171
  }
@@ -22398,7 +24480,7 @@ var init_devAdmin = __esm({
22398
24480
  const count = parseInt(String(body.count ?? "10"), 10) || 10;
22399
24481
  try {
22400
24482
  const orm = await Promise.resolve().then(() => (init_index(), index_exports));
22401
- const dirs = ["src/orm", "src/models"].map((d) => resolve11(process.cwd(), d)).filter((d) => existsSync19(d));
24483
+ const dirs = ["src/orm", "src/models"].map((d) => resolve12(process.cwd(), d)).filter((d) => existsSync20(d));
22402
24484
  const classes = [];
22403
24485
  for (const dir of dirs) {
22404
24486
  for (const m of await orm.discoverModels(dir)) classes.push(m.modelClass);
@@ -22425,7 +24507,7 @@ var init_devAdmin = __esm({
22425
24507
  const run = promisify(execFile);
22426
24508
  try {
22427
24509
  const { stdout, stderr } = await run("npm", ["test"], {
22428
- cwd: resolve11(process.cwd()),
24510
+ cwd: resolve12(process.cwd()),
22429
24511
  timeout: 18e4,
22430
24512
  encoding: "utf-8",
22431
24513
  maxBuffer: 8 * 1024 * 1024
@@ -22539,8 +24621,8 @@ var init_devAdmin = __esm({
22539
24621
  return;
22540
24622
  }
22541
24623
  try {
22542
- const envPath = join21(process.cwd(), ".env");
22543
- const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
24624
+ const envPath = join22(process.cwd(), ".env");
24625
+ const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
22544
24626
  const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
22545
24627
  const newLines = [];
22546
24628
  for (const line of lines) {
@@ -22567,7 +24649,7 @@ var init_devAdmin = __esm({
22567
24649
  for (const [key, found] of Object.entries(keysFound)) {
22568
24650
  if (!found) newLines.push(`${key}=${values[key]}`);
22569
24651
  }
22570
- writeFileSync12(envPath, newLines.join("\n") + "\n");
24652
+ writeFileSync13(envPath, newLines.join("\n") + "\n");
22571
24653
  res.json({ success: true });
22572
24654
  } catch (e) {
22573
24655
  const msg = e instanceof Error ? e.message : String(e);
@@ -22577,26 +24659,26 @@ var init_devAdmin = __esm({
22577
24659
  __devAdminFilename = fileURLToPath5(import.meta.url);
22578
24660
  __devAdminDirname = dirname10(__devAdminFilename);
22579
24661
  handleGalleryList = (_req, res) => {
22580
- const galleryDir = resolve11(__devAdminDirname, "..", "gallery");
24662
+ const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
22581
24663
  const items = [];
22582
- if (existsSync19(galleryDir)) {
24664
+ if (existsSync20(galleryDir)) {
22583
24665
  const entries = readdirSync12(galleryDir).sort();
22584
24666
  for (const entry of entries) {
22585
- const entryPath = join21(galleryDir, entry);
22586
- const metaFile = join21(entryPath, "meta.json");
22587
- if (statSync14(entryPath).isDirectory() && existsSync19(metaFile)) {
24667
+ const entryPath = join22(galleryDir, entry);
24668
+ const metaFile = join22(entryPath, "meta.json");
24669
+ if (statSync14(entryPath).isDirectory() && existsSync20(metaFile)) {
22588
24670
  try {
22589
24671
  const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
22590
24672
  meta.id = entry;
22591
- const srcDir = join21(entryPath, "src");
22592
- if (existsSync19(srcDir)) {
24673
+ const srcDir = join22(entryPath, "src");
24674
+ if (existsSync20(srcDir)) {
22593
24675
  const allFiles = walkDirRecursive(srcDir);
22594
- meta.files = allFiles.map((f) => relative7(srcDir, f));
24676
+ meta.files = allFiles.map((f) => relative8(srcDir, f));
22595
24677
  }
22596
- const projectSrc = resolve11(process.cwd(), "src");
22597
- if (existsSync19(srcDir) && meta.files) {
24678
+ const projectSrc = resolve12(process.cwd(), "src");
24679
+ if (existsSync20(srcDir) && meta.files) {
22598
24680
  meta.deployed = meta.files.every(
22599
- (f) => existsSync19(join21(projectSrc, f))
24681
+ (f) => existsSync20(join22(projectSrc, f))
22600
24682
  );
22601
24683
  } else {
22602
24684
  meta.deployed = false;
@@ -22692,10 +24774,10 @@ var init_devAdmin = __esm({
22692
24774
  handleFiles = async (req2, res) => {
22693
24775
  const url = new URL(req2.url ?? "/", "http://localhost");
22694
24776
  const rel = url.searchParams.get("path") ?? ".";
22695
- const root = resolve11(process.cwd());
24777
+ const root = resolve12(process.cwd());
22696
24778
  const target = safeJoin(root, rel);
22697
24779
  const { branch, gitRoot, status: gitStatus } = await devGitInfo(root);
22698
- if (!target || !existsSync19(target) || !statSync14(target).isDirectory()) {
24780
+ if (!target || !existsSync20(target) || !statSync14(target).isDirectory()) {
22699
24781
  res.json({ path: rel, branch, entries: [], error: "not a directory" });
22700
24782
  return;
22701
24783
  }
@@ -22708,8 +24790,8 @@ var init_devAdmin = __esm({
22708
24790
  const entries = [];
22709
24791
  for (const name of readdirSync12(target).sort()) {
22710
24792
  if (devFilesHidden(name)) continue;
22711
- const full = join21(target, name);
22712
- const entryRel = relative7(root, full).replace(/\\/g, "/");
24793
+ const full = join22(target, name);
24794
+ const entryRel = relative8(root, full).replace(/\\/g, "/");
22713
24795
  if (isSecretPath(entryRel)) continue;
22714
24796
  let isDir = false;
22715
24797
  let size = null;
@@ -22754,7 +24836,7 @@ var init_devAdmin = __esm({
22754
24836
  size
22755
24837
  });
22756
24838
  }
22757
- res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
24839
+ res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
22758
24840
  };
22759
24841
  DEV_ADMIN_LANG_MAP = {
22760
24842
  ".py": "python",
@@ -22799,15 +24881,15 @@ var init_devAdmin = __esm({
22799
24881
  res.json({ error: "Refused: secret file", path: rel, content: "", language: "text", bytes: 0 }, 403);
22800
24882
  return;
22801
24883
  }
22802
- const root = resolve11(process.cwd());
24884
+ const root = resolve12(process.cwd());
22803
24885
  const target = safeJoin(root, rel);
22804
- if (!target || !existsSync19(target) || !statSync14(target).isFile()) {
24886
+ if (!target || !existsSync20(target) || !statSync14(target).isFile()) {
22805
24887
  res.json({ error: `File not found: ${rel}` }, 404);
22806
24888
  return;
22807
24889
  }
22808
24890
  try {
22809
24891
  const content = readFileSync18(target, "utf-8");
22810
- const path8 = relative7(root, target);
24892
+ const path8 = relative8(root, target);
22811
24893
  res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
22812
24894
  } catch (e) {
22813
24895
  res.json({ error: e.message }, 500);
@@ -22817,22 +24899,22 @@ var init_devAdmin = __esm({
22817
24899
  const body = req2.body || {};
22818
24900
  const rel = body.path || "";
22819
24901
  const content = body.content ?? "";
22820
- const root = resolve11(process.cwd());
24902
+ const root = resolve12(process.cwd());
22821
24903
  const target = safeJoin(root, rel);
22822
24904
  if (!target) {
22823
24905
  res.json({ error: `Path escapes project directory: ${rel}` }, 400);
22824
24906
  return;
22825
24907
  }
22826
24908
  try {
22827
- mkdirSync13(dirname10(target), { recursive: true });
22828
- const existed = existsSync19(target);
22829
- writeFileSync12(target, content, "utf-8");
24909
+ mkdirSync14(dirname10(target), { recursive: true });
24910
+ const existed = existsSync20(target);
24911
+ writeFileSync13(target, content, "utf-8");
22830
24912
  try {
22831
24913
  const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
22832
- Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
24914
+ Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
22833
24915
  } catch {
22834
24916
  }
22835
- res.json({ ok: true, path: relative7(root, target), bytes: Buffer.byteLength(content, "utf-8") });
24917
+ res.json({ ok: true, path: relative8(root, target), bytes: Buffer.byteLength(content, "utf-8") });
22836
24918
  } catch (e) {
22837
24919
  res.json({ error: e.message }, 500);
22838
24920
  }
@@ -22844,9 +24926,9 @@ var init_devAdmin = __esm({
22844
24926
  res.json({ error: "Refused: secret file" }, 403);
22845
24927
  return;
22846
24928
  }
22847
- const root = resolve11(process.cwd());
24929
+ const root = resolve12(process.cwd());
22848
24930
  const target = safeJoin(root, rel);
22849
- if (!target || !existsSync19(target) || !statSync14(target).isFile()) {
24931
+ if (!target || !existsSync20(target) || !statSync14(target).isFile()) {
22850
24932
  res.raw.writeHead(404);
22851
24933
  res.raw.end("Not found");
22852
24934
  return;
@@ -22879,22 +24961,22 @@ var init_devAdmin = __esm({
22879
24961
  const body = req2.body || {};
22880
24962
  const from = body.from || "";
22881
24963
  const to = body.to || "";
22882
- const root = resolve11(process.cwd());
24964
+ const root = resolve12(process.cwd());
22883
24965
  const src = safeJoin(root, from);
22884
24966
  const dst = safeJoin(root, to);
22885
24967
  if (!src || !dst) {
22886
24968
  res.json({ error: "Invalid path" }, 400);
22887
24969
  return;
22888
24970
  }
22889
- if (!existsSync19(src)) {
24971
+ if (!existsSync20(src)) {
22890
24972
  res.json({ error: `Source not found: ${from}` }, 404);
22891
24973
  return;
22892
24974
  }
22893
24975
  try {
22894
24976
  const { renameSync: renameSync3 } = await import("node:fs");
22895
- mkdirSync13(dirname10(dst), { recursive: true });
24977
+ mkdirSync14(dirname10(dst), { recursive: true });
22896
24978
  renameSync3(src, dst);
22897
- res.json({ ok: true, from: relative7(root, src), to: relative7(root, dst) });
24979
+ res.json({ ok: true, from: relative8(root, src), to: relative8(root, dst) });
22898
24980
  } catch (e) {
22899
24981
  res.json({ error: e.message }, 500);
22900
24982
  }
@@ -22902,20 +24984,20 @@ var init_devAdmin = __esm({
22902
24984
  handleFileDelete = async (req2, res) => {
22903
24985
  const body = req2.body || {};
22904
24986
  const rel = body.path || "";
22905
- const root = resolve11(process.cwd());
24987
+ const root = resolve12(process.cwd());
22906
24988
  const target = safeJoin(root, rel);
22907
24989
  if (!target) {
22908
24990
  res.json({ error: "Invalid path" }, 400);
22909
24991
  return;
22910
24992
  }
22911
- if (!existsSync19(target)) {
24993
+ if (!existsSync20(target)) {
22912
24994
  res.json({ error: `Not found: ${rel}` }, 404);
22913
24995
  return;
22914
24996
  }
22915
24997
  try {
22916
24998
  const { rmSync } = await import("node:fs");
22917
24999
  rmSync(target, { recursive: true, force: true });
22918
- res.json({ ok: true, deleted: relative7(root, target) });
25000
+ res.json({ ok: true, deleted: relative8(root, target) });
22919
25001
  } catch (e) {
22920
25002
  res.json({ error: e.message }, 500);
22921
25003
  }
@@ -22953,7 +25035,7 @@ var init_devAdmin = __esm({
22953
25035
  const { execFileSync: execFileSync7 } = await import("node:child_process");
22954
25036
  const args = ["install", dev ? "--save-dev" : "--save", pkg];
22955
25037
  const output = execFileSync7("npm", args, {
22956
- cwd: resolve11(process.cwd()),
25038
+ cwd: resolve12(process.cwd()),
22957
25039
  timeout: 12e4,
22958
25040
  encoding: "utf-8"
22959
25041
  }).toString();
@@ -22965,7 +25047,7 @@ var init_devAdmin = __esm({
22965
25047
  handleGitStatus = async (_req, res) => {
22966
25048
  try {
22967
25049
  const { execFileSync: execFileSync7 } = await import("node:child_process");
22968
- const cwd = resolve11(process.cwd());
25050
+ const cwd = resolve12(process.cwd());
22969
25051
  try {
22970
25052
  execFileSync7("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 3e3 });
22971
25053
  } catch {
@@ -23104,7 +25186,7 @@ var init_devAdmin = __esm({
23104
25186
  try {
23105
25187
  const { execFileSync: execFileSync7 } = await import("node:child_process");
23106
25188
  const output = execFileSync7("npx", ["tina4nodejs", "generate", kind, name], {
23107
- cwd: resolve11(process.cwd()),
25189
+ cwd: resolve12(process.cwd()),
23108
25190
  timeout: 3e4,
23109
25191
  encoding: "utf-8"
23110
25192
  }).toString();
@@ -23249,21 +25331,21 @@ var init_devAdmin = __esm({
23249
25331
  });
23250
25332
  };
23251
25333
  handleDevAdminJs = async (_req, res) => {
23252
- const { readFileSync: readFileSync27, existsSync: existsSync27 } = await import("node:fs");
23253
- const { dirname: dirname15, join: join32, resolve: resolve20 } = await import("node:path");
25334
+ const { readFileSync: readFileSync27, existsSync: existsSync28 } = await import("node:fs");
25335
+ const { dirname: dirname15, join: join33, resolve: resolve21 } = await import("node:path");
23254
25336
  const { fileURLToPath: fileURLToPath8 } = await import("node:url");
23255
25337
  const dir = dirname15(fileURLToPath8(import.meta.url));
23256
25338
  const candidates = [
23257
- join32(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
25339
+ join33(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
23258
25340
  // src/../public/js/
23259
- join32(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
25341
+ join33(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
23260
25342
  // deeper nesting
23261
- resolve20(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
23262
- resolve20(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
25343
+ resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
25344
+ resolve21(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
23263
25345
  // project public/
23264
25346
  ];
23265
25347
  for (const jsPath of candidates) {
23266
- if (existsSync27(jsPath)) {
25348
+ if (existsSync28(jsPath)) {
23267
25349
  try {
23268
25350
  const content = readFileSync27(jsPath, "utf-8");
23269
25351
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
@@ -23288,8 +25370,8 @@ var init_devAdmin = __esm({
23288
25370
  });
23289
25371
 
23290
25372
  // ../core/src/i18n.ts
23291
- import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync20 } from "node:fs";
23292
- import { join as join22, resolve as resolve12 } from "node:path";
25373
+ import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync21 } from "node:fs";
25374
+ import { join as join23, resolve as resolve13 } from "node:path";
23293
25375
  var I18n;
23294
25376
  var init_i18n = __esm({
23295
25377
  "../core/src/i18n.ts"() {
@@ -23309,7 +25391,7 @@ var init_i18n = __esm({
23309
25391
  * (BUG-7, BREAKING in 3.13.x — was previously (localeDir, defaultLocale)).
23310
25392
  */
23311
25393
  constructor(locale, path8) {
23312
- this._localeDir = resolve12(
25394
+ this._localeDir = resolve13(
23313
25395
  path8 ?? process.env.TINA4_LOCALE_DIR ?? "src/locales"
23314
25396
  );
23315
25397
  this._defaultLocale = locale ?? process.env.TINA4_LOCALE ?? "en";
@@ -23366,7 +25448,7 @@ var init_i18n = __esm({
23366
25448
  }
23367
25449
  /** List available locale codes based on JSON files in the locale directory. */
23368
25450
  availableLocales() {
23369
- if (!existsSync20(this._localeDir)) {
25451
+ if (!existsSync21(this._localeDir)) {
23370
25452
  return [this._defaultLocale];
23371
25453
  }
23372
25454
  try {
@@ -23382,8 +25464,8 @@ var init_i18n = __esm({
23382
25464
  if (this._translations.has(locale)) {
23383
25465
  return;
23384
25466
  }
23385
- const filePath = join22(this._localeDir, `${locale}.json`);
23386
- if (existsSync20(filePath)) {
25467
+ const filePath = join23(this._localeDir, `${locale}.json`);
25468
+ if (existsSync21(filePath)) {
23387
25469
  try {
23388
25470
  const raw = readFileSync19(filePath, "utf-8");
23389
25471
  const data = JSON.parse(raw);
@@ -23395,8 +25477,8 @@ var init_i18n = __esm({
23395
25477
  }
23396
25478
  }
23397
25479
  for (const ext of [".yml", ".yaml"]) {
23398
- const yamlPath = join22(this._localeDir, `${locale}${ext}`);
23399
- if (existsSync20(yamlPath)) {
25480
+ const yamlPath = join23(this._localeDir, `${locale}${ext}`);
25481
+ if (existsSync21(yamlPath)) {
23400
25482
  try {
23401
25483
  const raw = readFileSync19(yamlPath, "utf-8");
23402
25484
  const data = _I18n._parseSimpleYaml(raw);
@@ -23741,7 +25823,7 @@ function sanitizeSecurity(reqs, schemes) {
23741
25823
  return clean;
23742
25824
  });
23743
25825
  }
23744
- function generate(routes, models = []) {
25826
+ function generate2(routes, models = []) {
23745
25827
  const info = {
23746
25828
  title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
23747
25829
  // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
@@ -24189,7 +26271,7 @@ __export(src_exports, {
24189
26271
  addSchema: () => addSchema,
24190
26272
  addSecurityScheme: () => addSecurityScheme,
24191
26273
  createSwaggerRoutes: () => createSwaggerRoutes,
24192
- generate: () => generate,
26274
+ generate: () => generate2,
24193
26275
  resetRegistry: () => resetRegistry,
24194
26276
  swaggerEnabled: () => swaggerEnabled
24195
26277
  });
@@ -24543,8 +26625,8 @@ function writeMcpDiscovery(projectRoot3, port) {
24543
26625
  const lines = contents.split(/\r?\n/);
24544
26626
  const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
24545
26627
  if (!already) {
24546
- const sep6 = contents.endsWith("\n") || contents === "" ? "" : "\n";
24547
- fs8.writeFileSync(gitignorePath, `${contents}${sep6}${GITIGNORE_LINE}
26628
+ const sep7 = contents.endsWith("\n") || contents === "" ? "" : "\n";
26629
+ fs8.writeFileSync(gitignorePath, `${contents}${sep7}${GITIGNORE_LINE}
24548
26630
  `, "utf-8");
24549
26631
  }
24550
26632
  }
@@ -24563,8 +26645,8 @@ var init_docsAutoDiscovery = __esm({
24563
26645
  // ../core/src/server.ts
24564
26646
  import { createServer as createServer2 } from "node:http";
24565
26647
  import { randomBytes as randomBytes6 } from "node:crypto";
24566
- import { resolve as resolve14, dirname as dirname11, join as join24, relative as relative8 } from "node:path";
24567
- import { existsSync as existsSync22, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
26648
+ import { resolve as resolve15, dirname as dirname11, join as join25, relative as relative9 } from "node:path";
26649
+ import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
24568
26650
  import { isatty } from "node:tty";
24569
26651
  import { fileURLToPath as fileURLToPath6 } from "node:url";
24570
26652
  import { execFileSync as execFileSync3, exec } from "node:child_process";
@@ -24602,8 +26684,8 @@ function swaggerAdvertised() {
24602
26684
  return TRUTHY2.includes(raw);
24603
26685
  }
24604
26686
  async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
24605
- const dir = resolve14(base, migrationDir);
24606
- if (!existsSync22(dir)) return;
26687
+ const dir = resolve15(base, migrationDir);
26688
+ if (!existsSync23(dir)) return;
24607
26689
  let hasSql = false;
24608
26690
  try {
24609
26691
  hasSql = readdirSync14(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
@@ -24744,12 +26826,12 @@ async function renderErrorPage(code, data, templatesDir) {
24744
26826
  }
24745
26827
  return instance;
24746
26828
  };
24747
- const userTemplatePath = join24(templatesDir, templateFile);
24748
- if (existsSync22(userTemplatePath)) {
26829
+ const userTemplatePath = join25(templatesDir, templateFile);
26830
+ if (existsSync23(userTemplatePath)) {
24749
26831
  return getCachedFrond(templatesDir).render(templateFile, data);
24750
26832
  }
24751
- const builtinTemplatePath = join24(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
24752
- if (existsSync22(builtinTemplatePath)) {
26833
+ const builtinTemplatePath = join25(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
26834
+ if (existsSync23(builtinTemplatePath)) {
24753
26835
  return getCachedFrond(BUILTIN_ERROR_TEMPLATES_DIR).render(templateFile, data);
24754
26836
  }
24755
26837
  return null;
@@ -24766,29 +26848,29 @@ function injectDevToolbar(html, ctx) {
24766
26848
  }
24767
26849
  function walkGalleryFiles(dir) {
24768
26850
  const results = [];
24769
- if (!existsSync22(dir)) return results;
26851
+ if (!existsSync23(dir)) return results;
24770
26852
  for (const f of readdirSync14(dir)) {
24771
- const full = join24(dir, f);
26853
+ const full = join25(dir, f);
24772
26854
  if (statSync15(full).isDirectory()) results.push(...walkGalleryFiles(full));
24773
26855
  else results.push(full);
24774
26856
  }
24775
26857
  return results;
24776
26858
  }
24777
26859
  function getGalleryDeployedState() {
24778
- const galleryDir = resolve14(__dirname, "..", "gallery");
26860
+ const galleryDir = resolve15(__dirname, "..", "gallery");
24779
26861
  const state = {};
24780
- if (!existsSync22(galleryDir)) return state;
26862
+ if (!existsSync23(galleryDir)) return state;
24781
26863
  try {
24782
26864
  const entries = readdirSync14(galleryDir).sort();
24783
26865
  for (const entry of entries) {
24784
- const entryPath = join24(galleryDir, entry);
24785
- const metaFile = join24(entryPath, "meta.json");
24786
- if (statSync15(entryPath).isDirectory() && existsSync22(metaFile)) {
24787
- const srcDir = join24(entryPath, "src");
24788
- if (existsSync22(srcDir)) {
26866
+ const entryPath = join25(galleryDir, entry);
26867
+ const metaFile = join25(entryPath, "meta.json");
26868
+ if (statSync15(entryPath).isDirectory() && existsSync23(metaFile)) {
26869
+ const srcDir = join25(entryPath, "src");
26870
+ if (existsSync23(srcDir)) {
24789
26871
  const files = walkGalleryFiles(srcDir);
24790
- const projectSrc = resolve14(process.cwd(), "src");
24791
- state[entry] = files.every((f) => existsSync22(join24(projectSrc, relative8(srcDir, f))));
26872
+ const projectSrc = resolve15(process.cwd(), "src");
26873
+ state[entry] = files.every((f) => existsSync23(join25(projectSrc, relative9(srcDir, f))));
24792
26874
  } else {
24793
26875
  state[entry] = false;
24794
26876
  }
@@ -24816,9 +26898,9 @@ function resolveTemplate(pathname, templatesDir) {
24816
26898
  const isDev2 = (process.env.TINA4_DEBUG ?? "false").toLowerCase() === "true";
24817
26899
  if (isDev2) {
24818
26900
  if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
24819
- const pagesDir = resolve14(templatesDir, TEMPLATE_PAGES_DIR);
26901
+ const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
24820
26902
  for (const ext of [".twig", ".html"]) {
24821
- if (existsSync22(resolve14(pagesDir, cleanPath + ext))) {
26903
+ if (existsSync23(resolve15(pagesDir, cleanPath + ext))) {
24822
26904
  return `${TEMPLATE_PAGES_DIR}/${cleanPath}${ext}`;
24823
26905
  }
24824
26906
  }
@@ -24826,14 +26908,14 @@ function resolveTemplate(pathname, templatesDir) {
24826
26908
  }
24827
26909
  if (!templateCache) {
24828
26910
  templateCache = /* @__PURE__ */ new Map();
24829
- const pagesDir = resolve14(templatesDir, TEMPLATE_PAGES_DIR);
24830
- if (existsSync22(pagesDir)) {
26911
+ const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
26912
+ if (existsSync23(pagesDir)) {
24831
26913
  const scan = (dir, prefix) => {
24832
26914
  for (const entry of readdirSync14(dir, { withFileTypes: true })) {
24833
26915
  if (entry.name.startsWith("_")) continue;
24834
26916
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
24835
26917
  if (entry.isDirectory()) {
24836
- scan(resolve14(dir, entry.name), rel);
26918
+ scan(resolve15(dir, entry.name), rel);
24837
26919
  } else if (entry.name.endsWith(".twig") || entry.name.endsWith(".html")) {
24838
26920
  const urlPath = rel.replace(/\.(twig|html)$/, "");
24839
26921
  if (!templateCache.has(urlPath)) {
@@ -25238,7 +27320,7 @@ function serveTemplateFallback(ctx) {
25238
27320
  if ((ctx.req.method ?? "GET") !== "GET") return false;
25239
27321
  const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
25240
27322
  if (!tplFile) return false;
25241
- const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(resolve14(ctx.templatesDir, tplFile), "utf-8");
27323
+ const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(resolve15(ctx.templatesDir, tplFile), "utf-8");
25242
27324
  ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
25243
27325
  ctx.res.raw.end(html);
25244
27326
  return true;
@@ -25277,9 +27359,9 @@ function serveMethodNotAllowed(ctx) {
25277
27359
  }
25278
27360
  function serveStaticAsset(ctx) {
25279
27361
  const custom = process.env.TINA4_PUBLIC_DIR;
25280
- if (custom && existsSync22(custom) && tryServeStatic(custom, ctx.req, ctx.res)) return true;
25281
- if (existsSync22(ctx.staticDir) && tryServeStatic(ctx.staticDir, ctx.req, ctx.res)) return true;
25282
- if (existsSync22(ctx.srcPublicDir) && tryServeStatic(ctx.srcPublicDir, ctx.req, ctx.res)) return true;
27362
+ if (custom && existsSync23(custom) && tryServeStatic(custom, ctx.req, ctx.res)) return true;
27363
+ if (existsSync23(ctx.staticDir) && tryServeStatic(ctx.staticDir, ctx.req, ctx.res)) return true;
27364
+ if (existsSync23(ctx.srcPublicDir) && tryServeStatic(ctx.srcPublicDir, ctx.req, ctx.res)) return true;
25283
27365
  if (ctx.swaggerAssetsEnabled || !isSwaggerAssetPath(ctx.pathname)) {
25284
27366
  if (tryServeStatic(BUILTIN_PUBLIC_DIR, ctx.req, ctx.res)) return true;
25285
27367
  }
@@ -25303,10 +27385,10 @@ async function serveNotFound(ctx) {
25303
27385
  return true;
25304
27386
  }
25305
27387
  async function buildDispatchContext(router, base) {
25306
- const root = base ? resolve14(base) : process.cwd();
25307
- const staticDir = resolve14(root, "public");
25308
- const srcPublicDir = resolve14(root, "src/public");
25309
- const templatesDir = resolve14(root, "src/templates");
27388
+ const root = base ? resolve15(base) : process.cwd();
27389
+ const staticDir = resolve15(root, "public");
27390
+ const srcPublicDir = resolve15(root, "src/public");
27391
+ const templatesDir = resolve15(root, "src/templates");
25310
27392
  let frondEngine = null;
25311
27393
  try {
25312
27394
  const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
@@ -25470,13 +27552,13 @@ ${reset2}
25470
27552
  };
25471
27553
  }
25472
27554
  }
25473
- const base = config?.basePath ? resolve14(config.basePath) : process.cwd();
25474
- const routesDir = resolve14(base, config?.routesDir ?? "src/routes");
25475
- const modelsDir = resolve14(base, config?.modelsDir ?? "src/models");
25476
- const ormDir = resolve14(base, "src/orm");
25477
- const staticDir = resolve14(base, config?.staticDir ?? "public");
25478
- const srcPublicDir = resolve14(base, "src/public");
25479
- const templatesDir = resolve14(base, config?.templatesDir ?? "src/templates");
27555
+ const base = config?.basePath ? resolve15(config.basePath) : process.cwd();
27556
+ const routesDir = resolve15(base, config?.routesDir ?? "src/routes");
27557
+ const modelsDir = resolve15(base, config?.modelsDir ?? "src/models");
27558
+ const ormDir = resolve15(base, "src/orm");
27559
+ const staticDir = resolve15(base, config?.staticDir ?? "public");
27560
+ const srcPublicDir = resolve15(base, "src/public");
27561
+ const templatesDir = resolve15(base, config?.templatesDir ?? "src/templates");
25480
27562
  const router = new Router();
25481
27563
  const middleware = new MiddlewareChain();
25482
27564
  globalThis.__tina4_router = router;
@@ -25510,8 +27592,8 @@ ${reset2}
25510
27592
  } catch {
25511
27593
  }
25512
27594
  if (frondEngine) {
25513
- const localeDir = resolve14(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
25514
- if (existsSync22(localeDir)) {
27595
+ const localeDir = resolve15(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
27596
+ if (existsSync23(localeDir)) {
25515
27597
  try {
25516
27598
  const localeFiles = readdirSync14(localeDir).filter((f) => f.endsWith(".json"));
25517
27599
  if (localeFiles.length > 0 && !frondEngine.globals?.t) {
@@ -25526,7 +27608,7 @@ ${reset2}
25526
27608
  middleware.use(requestLogger());
25527
27609
  middleware.use(rateLimiter());
25528
27610
  MiddlewareRunner.use(SecurityHeadersMiddleware);
25529
- if (existsSync22(routesDir)) {
27611
+ if (existsSync23(routesDir)) {
25530
27612
  const routes = await discoverRoutes(routesDir);
25531
27613
  for (const route of routes) {
25532
27614
  router.addRoute(route);
@@ -25546,8 +27628,8 @@ ${reset2}
25546
27628
  console.log(`
25547
27629
  \x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
25548
27630
  }
25549
- const hasOrmDir = existsSync22(ormDir);
25550
- const hasModelsDir = existsSync22(modelsDir);
27631
+ const hasOrmDir = existsSync23(ormDir);
27632
+ const hasModelsDir = existsSync23(modelsDir);
25551
27633
  if (hasOrmDir || hasModelsDir) {
25552
27634
  try {
25553
27635
  const orm = await Promise.resolve().then(() => (init_index(), index_exports));
@@ -25604,7 +27686,7 @@ ${reset2}
25604
27686
  let modelDefs = [];
25605
27687
  try {
25606
27688
  const orm = await Promise.resolve().then(() => (init_index(), index_exports));
25607
- const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync22(d));
27689
+ const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync23(d));
25608
27690
  const seenTables = /* @__PURE__ */ new Set();
25609
27691
  for (const dir of allModelDirs) {
25610
27692
  const discovered = await orm.discoverModels(dir);
@@ -25839,8 +27921,8 @@ var init_server = __esm({
25839
27921
  init_version();
25840
27922
  __filename = fileURLToPath6(import.meta.url);
25841
27923
  __dirname = dirname11(__filename);
25842
- BUILTIN_ERROR_TEMPLATES_DIR = resolve14(__dirname, "..", "templates");
25843
- BUILTIN_PUBLIC_DIR = resolve14(__dirname, "..", "public");
27924
+ BUILTIN_ERROR_TEMPLATES_DIR = resolve15(__dirname, "..", "templates");
27925
+ BUILTIN_PUBLIC_DIR = resolve15(__dirname, "..", "public");
25844
27926
  swaggerAssetsEnabled = false;
25845
27927
  DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 30;
25846
27928
  frondCache = /* @__PURE__ */ new Map();
@@ -25996,8 +28078,8 @@ var init_env = __esm({
25996
28078
 
25997
28079
  // ../core/src/fakeData.ts
25998
28080
  import { randomInt, randomUUID as randomUUID6 } from "node:crypto";
25999
- import { existsSync as existsSync23, readdirSync as readdirSync15 } from "node:fs";
26000
- import { resolve as resolve15, join as join25 } from "node:path";
28081
+ import { existsSync as existsSync24, readdirSync as readdirSync15 } from "node:fs";
28082
+ import { resolve as resolve16, join as join26 } from "node:path";
26001
28083
  function mulberry32(seed) {
26002
28084
  let s = seed | 0;
26003
28085
  return () => {
@@ -26431,12 +28513,12 @@ var init_fakeData = __esm({
26431
28513
  * Returns an array of executed file paths.
26432
28514
  */
26433
28515
  async seedDir(seedDir) {
26434
- const dir = resolve15(seedDir ?? "src/seeds");
26435
- if (!existsSync23(dir)) return [];
28516
+ const dir = resolve16(seedDir ?? "src/seeds");
28517
+ if (!existsSync24(dir)) return [];
26436
28518
  const files = readdirSync15(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
26437
28519
  const executed = [];
26438
28520
  for (const file of files) {
26439
- const fullPath = join25(dir, file);
28521
+ const fullPath = join26(dir, file);
26440
28522
  try {
26441
28523
  const mod = await import(fullPath);
26442
28524
  if (typeof mod.default === "function") {
@@ -26535,7 +28617,7 @@ var init_mqttMessage = __esm({
26535
28617
  import net2 from "node:net";
26536
28618
  import tls from "node:tls";
26537
28619
  import { randomBytes as randomBytes7 } from "node:crypto";
26538
- import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
28620
+ import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
26539
28621
  var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
26540
28622
  var init_mqtt = __esm({
26541
28623
  "../core/src/mqtt.ts"() {
@@ -26738,7 +28820,7 @@ var init_mqtt = __esm({
26738
28820
  */
26739
28821
  async connect() {
26740
28822
  this.closeSocket();
26741
- if (this.secure && this.tlsVerify && this.caFile && !existsSync24(this.caFile)) {
28823
+ if (this.secure && this.tlsVerify && this.caFile && !existsSync25(this.caFile)) {
26742
28824
  throw new MqttError(
26743
28825
  `MQTT CA file not found: ${this.caFile} -- TINA4_MQTT_CA_FILE (or caFile) must point at the broker's CA certificate in PEM form`
26744
28826
  );
@@ -26975,7 +29057,7 @@ var init_mqtt = __esm({
26975
29057
  * a later client.
26976
29058
  */
26977
29059
  openSocket() {
26978
- return new Promise((resolve20, reject) => {
29060
+ return new Promise((resolve21, reject) => {
26979
29061
  let settled = false;
26980
29062
  const settle = (fn) => {
26981
29063
  if (settled) return;
@@ -27001,9 +29083,9 @@ var init_mqtt = __esm({
27001
29083
  rejectUnauthorized: this.tlsVerify
27002
29084
  };
27003
29085
  if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
27004
- sock = tls.connect(opts, () => settle(() => resolve20(sock)));
29086
+ sock = tls.connect(opts, () => settle(() => resolve21(sock)));
27005
29087
  } else {
27006
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
29088
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
27007
29089
  }
27008
29090
  sock.once("error", (err) => {
27009
29091
  settle(() => {
@@ -27042,13 +29124,13 @@ var init_mqtt = __esm({
27042
29124
  writePacket(header, body) {
27043
29125
  if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
27044
29126
  const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
27045
- return new Promise((resolve20, reject) => {
29127
+ return new Promise((resolve21, reject) => {
27046
29128
  this.socket.write(packet, (err) => {
27047
29129
  if (err) {
27048
29130
  reject(new MqttError(`MQTT write failed: ${err.message}`));
27049
29131
  } else {
27050
29132
  this.lastWriteAt = Date.now();
27051
- resolve20();
29133
+ resolve21();
27052
29134
  }
27053
29135
  });
27054
29136
  });
@@ -27081,7 +29163,7 @@ var init_mqtt = __esm({
27081
29163
  if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
27082
29164
  if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
27083
29165
  if (this.socketError !== null) return Promise.reject(this.socketError);
27084
- return new Promise((resolve20, reject) => {
29166
+ return new Promise((resolve21, reject) => {
27085
29167
  let timer = null;
27086
29168
  if (deadline !== null) {
27087
29169
  const remaining = deadline - Date.now();
@@ -27096,7 +29178,7 @@ var init_mqtt = __esm({
27096
29178
  }
27097
29179
  }, remaining);
27098
29180
  }
27099
- this.waiter = { need, resolve: resolve20, reject, timer };
29181
+ this.waiter = { need, resolve: resolve21, reject, timer };
27100
29182
  this.serviceWaiter();
27101
29183
  });
27102
29184
  }
@@ -27221,7 +29303,7 @@ var init_mqtt = __esm({
27221
29303
 
27222
29304
  // ../core/src/service.ts
27223
29305
  import { readdirSync as readdirSync16, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
27224
- import { join as join26, extname as extname6 } from "node:path";
29306
+ import { join as join27, extname as extname6 } from "node:path";
27225
29307
  import { pathToFileURL } from "node:url";
27226
29308
  function matchCronField(field, value) {
27227
29309
  if (field === "*") return true;
@@ -27395,7 +29477,7 @@ var init_service = __esm({
27395
29477
  for (const entry of entries) {
27396
29478
  const ext = extname6(entry);
27397
29479
  if (ext !== ".ts" && ext !== ".js") continue;
27398
- const fullPath = join26(dir, entry);
29480
+ const fullPath = join27(dir, entry);
27399
29481
  const stat = statSync16(fullPath);
27400
29482
  if (!stat.isFile()) continue;
27401
29483
  try {
@@ -27522,7 +29604,7 @@ var init_service = __esm({
27522
29604
  for (const entry of entries) {
27523
29605
  const ext = extname6(entry);
27524
29606
  if (ext !== ".ts" && ext !== ".js") continue;
27525
- const fullPath = join26(dir, entry);
29607
+ const fullPath = join27(dir, entry);
27526
29608
  if (watchedFiles.has(fullPath)) continue;
27527
29609
  watchedFiles.add(fullPath);
27528
29610
  watchFile(fullPath, { interval: 1e3 }, async () => {
@@ -28131,7 +30213,7 @@ var init_api = __esm({
28131
30213
  * `res.destroy()`.
28132
30214
  */
28133
30215
  openStreamRequest(method, url, headers, data, connectSec) {
28134
- return new Promise((resolve20, reject) => {
30216
+ return new Promise((resolve21, reject) => {
28135
30217
  let parsed;
28136
30218
  try {
28137
30219
  parsed = new URL2(url);
@@ -28153,7 +30235,7 @@ var init_api = __esm({
28153
30235
  options.rejectUnauthorized = false;
28154
30236
  }
28155
30237
  const req2 = protocolModule.request(options, (res) => {
28156
- resolve20({ res });
30238
+ resolve21({ res });
28157
30239
  });
28158
30240
  req2.on("timeout", () => {
28159
30241
  req2.destroy(new ApiStreamError(`stream connect timeout after ${connectSec}s`, null));
@@ -28291,12 +30373,12 @@ var init_api = __esm({
28291
30373
  * authenticate to.
28292
30374
  */
28293
30375
  performRequest(method, url, headers, data, redirectsLeft) {
28294
- return new Promise((resolve20) => {
30376
+ return new Promise((resolve21) => {
28295
30377
  let parsed;
28296
30378
  try {
28297
30379
  parsed = new URL2(url);
28298
30380
  } catch (err) {
28299
- resolve20({ kind: "error", error: err instanceof Error ? err.message : String(err) });
30381
+ resolve21({ kind: "error", error: err instanceof Error ? err.message : String(err) });
28300
30382
  return;
28301
30383
  }
28302
30384
  const isHttps = parsed.protocol === "https:";
@@ -28321,7 +30403,7 @@ var init_api = __esm({
28321
30403
  try {
28322
30404
  nextUrl = new URL2(location, url).toString();
28323
30405
  } catch {
28324
- resolve20({ kind: "response", res });
30406
+ resolve21({ kind: "response", res });
28325
30407
  return;
28326
30408
  }
28327
30409
  const crossOrigin = !sameOrigin(url, nextUrl);
@@ -28339,17 +30421,17 @@ var init_api = __esm({
28339
30421
  deleteHeaderCaseInsensitive(nextHeaders, name);
28340
30422
  }
28341
30423
  }
28342
- this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve20);
30424
+ this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve21);
28343
30425
  return;
28344
30426
  }
28345
- resolve20({ kind: "response", res });
30427
+ resolve21({ kind: "response", res });
28346
30428
  });
28347
30429
  req2.on("timeout", () => {
28348
30430
  req2.destroy();
28349
- resolve20({ kind: "error", error: `Request timed out after ${this.timeout}s` });
30431
+ resolve21({ kind: "error", error: `Request timed out after ${this.timeout}s` });
28350
30432
  });
28351
30433
  req2.on("error", (err) => {
28352
- resolve20({ kind: "error", error: err.message });
30434
+ resolve21({ kind: "error", error: err.message });
28353
30435
  });
28354
30436
  if (data) {
28355
30437
  req2.write(data);
@@ -28359,7 +30441,7 @@ var init_api = __esm({
28359
30441
  }
28360
30442
  /** Buffer a response body, parse JSON if possible, and store cookies. */
28361
30443
  readResponse(res) {
28362
- return new Promise((resolve20) => {
30444
+ return new Promise((resolve21) => {
28363
30445
  const chunks = [];
28364
30446
  res.on("data", (chunk) => {
28365
30447
  chunks.push(chunk);
@@ -28374,7 +30456,7 @@ var init_api = __esm({
28374
30456
  } catch {
28375
30457
  parsed = raw;
28376
30458
  }
28377
- resolve20({
30459
+ resolve21({
28378
30460
  http_code: res.statusCode ?? null,
28379
30461
  body: parsed,
28380
30462
  headers: respHeaders,
@@ -28382,7 +30464,7 @@ var init_api = __esm({
28382
30464
  });
28383
30465
  });
28384
30466
  res.on("error", (err) => {
28385
- resolve20({ http_code: null, body: null, headers: {}, error: err.message });
30467
+ resolve21({ http_code: null, body: null, headers: {}, error: err.message });
28386
30468
  });
28387
30469
  });
28388
30470
  }
@@ -28446,7 +30528,7 @@ function parseMailRedirectList(raw) {
28446
30528
  return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
28447
30529
  }
28448
30530
  function readResponse(socket) {
28449
- return new Promise((resolve20, reject) => {
30531
+ return new Promise((resolve21, reject) => {
28450
30532
  let buffer = "";
28451
30533
  const onData = (chunk) => {
28452
30534
  buffer += chunk.toString("utf-8");
@@ -28458,7 +30540,7 @@ function readResponse(socket) {
28458
30540
  if (line.length >= 4 && line[3] === " ") {
28459
30541
  socket.removeListener("data", onData);
28460
30542
  socket.removeListener("error", onError);
28461
- resolve20({ code, text: buffer.trim() });
30543
+ resolve21({ code, text: buffer.trim() });
28462
30544
  return;
28463
30545
  }
28464
30546
  }
@@ -28472,10 +30554,10 @@ function readResponse(socket) {
28472
30554
  });
28473
30555
  }
28474
30556
  function sendCommand(socket, command) {
28475
- return new Promise((resolve20, reject) => {
30557
+ return new Promise((resolve21, reject) => {
28476
30558
  socket.write(command + "\r\n", "utf-8", (err) => {
28477
30559
  if (err) return reject(err);
28478
- readResponse(socket).then(resolve20, reject);
30560
+ readResponse(socket).then(resolve21, reject);
28479
30561
  });
28480
30562
  });
28481
30563
  }
@@ -28575,7 +30657,7 @@ function imapQuote(s) {
28575
30657
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
28576
30658
  }
28577
30659
  function imapReadLine(socket) {
28578
- return new Promise((resolve20, reject) => {
30660
+ return new Promise((resolve21, reject) => {
28579
30661
  let buffer = "";
28580
30662
  const onData = (chunk) => {
28581
30663
  buffer += chunk.toString("utf-8");
@@ -28583,7 +30665,7 @@ function imapReadLine(socket) {
28583
30665
  if (nlIndex !== -1) {
28584
30666
  socket.removeListener("data", onData);
28585
30667
  socket.removeListener("error", onError);
28586
- resolve20(buffer);
30668
+ resolve21(buffer);
28587
30669
  }
28588
30670
  };
28589
30671
  const onError = (err) => {
@@ -28595,7 +30677,7 @@ function imapReadLine(socket) {
28595
30677
  });
28596
30678
  }
28597
30679
  function imapCommand(socket, command) {
28598
- return new Promise((resolve20, reject) => {
30680
+ return new Promise((resolve21, reject) => {
28599
30681
  imapTagCounter++;
28600
30682
  const tag = `T${imapTagCounter}`;
28601
30683
  const fullCommand = `${tag} ${command}\r
@@ -28606,7 +30688,7 @@ function imapCommand(socket, command) {
28606
30688
  if (buffer.includes(`${tag} OK`)) {
28607
30689
  socket.removeListener("data", onData);
28608
30690
  socket.removeListener("error", onError);
28609
- resolve20(buffer);
30691
+ resolve21(buffer);
28610
30692
  return;
28611
30693
  }
28612
30694
  if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
@@ -28911,14 +30993,14 @@ var init_messenger = __esm({
28911
30993
  let socket;
28912
30994
  if (this.port === 465) {
28913
30995
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
28914
- await new Promise((resolve20, reject) => {
28915
- socket.once("secureConnect", resolve20);
30996
+ await new Promise((resolve21, reject) => {
30997
+ socket.once("secureConnect", resolve21);
28916
30998
  socket.once("error", reject);
28917
30999
  });
28918
31000
  } else {
28919
31001
  socket = net3.createConnection({ host: this.host, port: this.port });
28920
- await new Promise((resolve20, reject) => {
28921
- socket.once("connect", resolve20);
31002
+ await new Promise((resolve21, reject) => {
31003
+ socket.once("connect", resolve21);
28922
31004
  socket.once("error", reject);
28923
31005
  });
28924
31006
  }
@@ -28942,8 +31024,8 @@ var init_messenger = __esm({
28942
31024
  socket = tls2.connect(
28943
31025
  { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
28944
31026
  );
28945
- await new Promise((resolve20, reject) => {
28946
- socket.once("secureConnect", resolve20);
31027
+ await new Promise((resolve21, reject) => {
31028
+ socket.once("secureConnect", resolve21);
28947
31029
  socket.once("error", reject);
28948
31030
  });
28949
31031
  const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
@@ -29036,14 +31118,14 @@ var init_messenger = __esm({
29036
31118
  let socket;
29037
31119
  if (this.port === 465) {
29038
31120
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
29039
- await new Promise((resolve20, reject) => {
29040
- socket.once("secureConnect", resolve20);
31121
+ await new Promise((resolve21, reject) => {
31122
+ socket.once("secureConnect", resolve21);
29041
31123
  socket.once("error", reject);
29042
31124
  });
29043
31125
  } else {
29044
31126
  socket = net3.createConnection({ host: this.host, port: this.port });
29045
- await new Promise((resolve20, reject) => {
29046
- socket.once("connect", resolve20);
31127
+ await new Promise((resolve21, reject) => {
31128
+ socket.once("connect", resolve21);
29047
31129
  socket.once("error", reject);
29048
31130
  });
29049
31131
  }
@@ -29078,14 +31160,14 @@ var init_messenger = __esm({
29078
31160
  const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
29079
31161
  if (useTls) {
29080
31162
  socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
29081
- await new Promise((resolve20, reject) => {
29082
- socket.once("secureConnect", resolve20);
31163
+ await new Promise((resolve21, reject) => {
31164
+ socket.once("secureConnect", resolve21);
29083
31165
  socket.once("error", reject);
29084
31166
  });
29085
31167
  } else {
29086
31168
  socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
29087
- await new Promise((resolve20, reject) => {
29088
- socket.once("connect", resolve20);
31169
+ await new Promise((resolve21, reject) => {
31170
+ socket.once("connect", resolve21);
29089
31171
  socket.once("error", reject);
29090
31172
  });
29091
31173
  }
@@ -29995,16 +32077,16 @@ var init_htmlElement = __esm({
29995
32077
  });
29996
32078
 
29997
32079
  // ../core/src/ai.ts
29998
- import { existsSync as existsSync25, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15, readFileSync as readFileSync24 } from "node:fs";
32080
+ import { existsSync as existsSync26, mkdirSync as mkdirSync17, writeFileSync as writeFileSync16, readFileSync as readFileSync24 } from "node:fs";
29999
32081
  import { homedir } from "node:os";
30000
- import { join as join27, resolve as resolve16, relative as relative9, dirname as dirname12 } from "node:path";
32082
+ import { join as join28, resolve as resolve17, relative as relative10, dirname as dirname12 } from "node:path";
30001
32083
  import { fileURLToPath as fileURLToPath7 } from "node:url";
30002
32084
  import { execSync, execFileSync as execFileSync4 } from "node:child_process";
30003
32085
  import { createInterface } from "node:readline";
30004
32086
  function readVersion() {
30005
32087
  try {
30006
32088
  const thisDir = dirname12(fileURLToPath7(import.meta.url));
30007
- const rootPkg = resolve16(thisDir, "..", "..", "..", "package.json");
32089
+ const rootPkg = resolve17(thisDir, "..", "..", "..", "package.json");
30008
32090
  const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
30009
32091
  return pkg.version ?? "0.0.0";
30010
32092
  } catch {
@@ -30068,8 +32150,8 @@ function downloadSkillsSync(jobs) {
30068
32150
  function installSkills(root = ".", targets) {
30069
32151
  const ref = skillsRef();
30070
32152
  const dests = targets ?? [
30071
- join27(resolve16(root), ".claude", "skills"),
30072
- join27(homedir(), ".claude", "skills")
32153
+ join28(resolve17(root), ".claude", "skills"),
32154
+ join28(homedir(), ".claude", "skills")
30073
32155
  ];
30074
32156
  const jobs = [];
30075
32157
  const index = /* @__PURE__ */ new Map();
@@ -30087,9 +32169,9 @@ function installSkills(root = ".", targets) {
30087
32169
  const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
30088
32170
  skillMdUrl[skill] = `${base}/SKILL.md`;
30089
32171
  for (const dest of dests) {
30090
- add(`${base}/SKILL.md`, join27(dest, skill, "SKILL.md"));
32172
+ add(`${base}/SKILL.md`, join28(dest, skill, "SKILL.md"));
30091
32173
  for (const r of spec.references) {
30092
- add(`${base}/references/${r}`, join27(dest, skill, "references", r));
32174
+ add(`${base}/references/${r}`, join28(dest, skill, "references", r));
30093
32175
  }
30094
32176
  }
30095
32177
  }
@@ -30101,10 +32183,10 @@ function installSkills(root = ".", targets) {
30101
32183
  return installed;
30102
32184
  }
30103
32185
  function isInstalled(root, tool) {
30104
- return existsSync25(join27(resolve16(root), tool.contextFile));
32186
+ return existsSync26(join28(resolve17(root), tool.contextFile));
30105
32187
  }
30106
32188
  function showMenu(root = ".") {
30107
- const r = resolve16(root);
32189
+ const r = resolve17(root);
30108
32190
  console.log("\n Tina4 AI Context Installer\n");
30109
32191
  for (let i = 0; i < AI_TOOLS.length; i++) {
30110
32192
  const tool = AI_TOOLS[i];
@@ -30122,16 +32204,16 @@ function showMenu(root = ".") {
30122
32204
  const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
30123
32205
  console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
30124
32206
  console.log();
30125
- return new Promise((resolve20) => {
32207
+ return new Promise((resolve21) => {
30126
32208
  const rl = createInterface({ input: process.stdin, output: process.stdout });
30127
32209
  rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
30128
32210
  rl.close();
30129
- resolve20(answer.trim());
32211
+ resolve21(answer.trim());
30130
32212
  });
30131
32213
  });
30132
32214
  }
30133
32215
  function installSelected(root, selection) {
30134
- const rootPath = resolve16(root);
32216
+ const rootPath = resolve17(root);
30135
32217
  const created = [];
30136
32218
  let indices;
30137
32219
  let doInstallTina4Ai = false;
@@ -30224,35 +32306,35 @@ function looksLikeOldFrameworkInstall(existing) {
30224
32306
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
30225
32307
  const block = skillBlock(contextFile);
30226
32308
  const [start2, end] = markersFor(contextFile);
30227
- if (!existsSync25(contextPath)) {
30228
- writeFileSync15(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
32309
+ if (!existsSync26(contextPath)) {
32310
+ writeFileSync16(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
30229
32311
  return "Installed";
30230
32312
  }
30231
32313
  const existing = readFileSync24(contextPath, "utf-8");
30232
32314
  if (hasMarkers(existing, start2, end)) {
30233
- writeFileSync15(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
32315
+ writeFileSync16(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
30234
32316
  return "Refreshed skill block in";
30235
32317
  }
30236
32318
  if (looksLikeOldFrameworkInstall(existing)) {
30237
32319
  const head = existing.replace(/^\s+/, "");
30238
32320
  const preamble = existing.slice(0, existing.length - head.length);
30239
32321
  const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
30240
- writeFileSync15(contextPath, newContent, "utf-8");
32322
+ writeFileSync16(contextPath, newContent, "utf-8");
30241
32323
  return "Migrated (replaced old framework dump in)";
30242
32324
  }
30243
- writeFileSync15(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
32325
+ writeFileSync16(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
30244
32326
  return "Appended skill block to";
30245
32327
  }
30246
32328
  function installForTool(root, tool, context) {
30247
32329
  const created = [];
30248
- const contextPath = join27(root, tool.contextFile);
32330
+ const contextPath = join28(root, tool.contextFile);
30249
32331
  if (tool.configDir) {
30250
- mkdirSync16(join27(root, tool.configDir), { recursive: true });
32332
+ mkdirSync17(join28(root, tool.configDir), { recursive: true });
30251
32333
  }
30252
32334
  const parentDir = dirname12(contextPath);
30253
- mkdirSync16(parentDir, { recursive: true });
32335
+ mkdirSync17(parentDir, { recursive: true });
30254
32336
  const action = writeOrMerge(contextPath, tool.contextFile, context);
30255
- const rel = relative9(root, contextPath);
32337
+ const rel = relative10(root, contextPath);
30256
32338
  created.push(rel);
30257
32339
  console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
30258
32340
  if (tool.name === "claude-code") {
@@ -30285,7 +32367,7 @@ function installTina4Ai() {
30285
32367
  function installClaudeSkills(root) {
30286
32368
  const created = [];
30287
32369
  for (const skill of installSkills(root)) {
30288
- created.push(join27(".claude", "skills", skill));
32370
+ created.push(join28(".claude", "skills", skill));
30289
32371
  console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
30290
32372
  }
30291
32373
  return created;
@@ -30627,9 +32709,9 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
30627
32709
  function generateClaudeCodeContext() {
30628
32710
  try {
30629
32711
  const thisDir = dirname12(fileURLToPath7(import.meta.url));
30630
- const repoRoot = resolve16(thisDir, "..", "..", "..");
30631
- const claudeMdPath = join27(repoRoot, "CLAUDE.md");
30632
- if (existsSync25(claudeMdPath)) {
32712
+ const repoRoot = resolve17(thisDir, "..", "..", "..");
32713
+ const claudeMdPath = join28(repoRoot, "CLAUDE.md");
32714
+ if (existsSync26(claudeMdPath)) {
30633
32715
  return readFileSync24(claudeMdPath, "utf-8");
30634
32716
  }
30635
32717
  } catch {
@@ -31176,11 +33258,11 @@ var init_aiClient = __esm({
31176
33258
  const payload = JSON.stringify(body);
31177
33259
  const controller = new AbortController();
31178
33260
  const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
31179
- return new Promise((resolve20, reject) => {
33261
+ return new Promise((resolve21, reject) => {
31180
33262
  const client = url.protocol === "https:" ? https2 : http2;
31181
33263
  const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
31182
33264
  clearTimeout(connectTimer);
31183
- resolve20({ response, cleanup: () => {
33265
+ resolve21({ response, cleanup: () => {
31184
33266
  clearTimeout(totalTimer);
31185
33267
  clearTimeout(connectTimer);
31186
33268
  } });
@@ -31209,7 +33291,7 @@ var init_aiClient = __esm({
31209
33291
  const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
31210
33292
  const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
31211
33293
  const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
31212
- return new Promise((resolve20) => setTimeout(resolve20, delay));
33294
+ return new Promise((resolve21) => setTimeout(resolve21, delay));
31213
33295
  }
31214
33296
  static async requestJson(config, headers, body) {
31215
33297
  const deadline = performance.now() + config.totalTimeout * 1e3;
@@ -34153,8 +36235,8 @@ __export(sqlite_exports, {
34153
36235
  SQLiteAdapter: () => SQLiteAdapter
34154
36236
  });
34155
36237
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
34156
- import { mkdirSync as mkdirSync17 } from "node:fs";
34157
- import { dirname as dirname13, isAbsolute as isAbsolute5, join as join28, resolve as resolve17 } from "node:path";
36238
+ import { mkdirSync as mkdirSync18 } from "node:fs";
36239
+ import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29, resolve as resolve18 } from "node:path";
34158
36240
  function isIdentifier(str) {
34159
36241
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
34160
36242
  }
@@ -34187,13 +36269,13 @@ function resolveSqlitePath(dbPath) {
34187
36269
  if (dbPath === ":memory:") return dbPath;
34188
36270
  let path8 = dbPath;
34189
36271
  if (!isAbsolute5(path8)) {
34190
- path8 = join28(process.cwd(), path8);
34191
- mkdirSync17(dirname13(path8), { recursive: true });
36272
+ path8 = join29(process.cwd(), path8);
36273
+ mkdirSync18(dirname13(path8), { recursive: true });
34192
36274
  } else {
34193
- const cwd = resolve17(process.cwd());
34194
- const abs = resolve17(path8);
36275
+ const cwd = resolve18(process.cwd());
36276
+ const abs = resolve18(path8);
34195
36277
  if (abs.startsWith(cwd + "/") || abs === cwd) {
34196
- mkdirSync17(dirname13(abs), { recursive: true });
36278
+ mkdirSync18(dirname13(abs), { recursive: true });
34197
36279
  }
34198
36280
  }
34199
36281
  return path8;
@@ -34591,7 +36673,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
34591
36673
  const elapsedMs = () => performance.now() - startedAt;
34592
36674
  if (budgetMs === null) return attempt();
34593
36675
  const started = attempt();
34594
- return new Promise((resolve20, reject) => {
36676
+ return new Promise((resolve21, reject) => {
34595
36677
  let expired = false;
34596
36678
  const timer = setTimeout(() => {
34597
36679
  expired = true;
@@ -34601,7 +36683,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
34601
36683
  (arrived) => {
34602
36684
  clearTimeout(timer);
34603
36685
  if (expired) abandon?.(arrived);
34604
- else resolve20(arrived);
36686
+ else resolve21(arrived);
34605
36687
  },
34606
36688
  (failure) => {
34607
36689
  clearTimeout(timer);
@@ -35190,10 +37272,10 @@ var init_mysql = __esm({
35190
37272
  ...timeoutOption
35191
37273
  });
35192
37274
  }
35193
- return new Promise((resolve20, reject) => {
37275
+ return new Promise((resolve21, reject) => {
35194
37276
  this.connection.connect((err) => {
35195
37277
  if (err) reject(err);
35196
- else resolve20();
37278
+ else resolve21();
35197
37279
  });
35198
37280
  });
35199
37281
  },
@@ -35215,10 +37297,10 @@ var init_mysql = __esm({
35215
37297
  }
35216
37298
  }
35217
37299
  queryPromise(sql, params) {
35218
- return new Promise((resolve20, reject) => {
37300
+ return new Promise((resolve21, reject) => {
35219
37301
  this.connection.query(sql, params ?? [], (err, results) => {
35220
37302
  if (err) reject(err);
35221
- else resolve20(results);
37303
+ else resolve21(results);
35222
37304
  });
35223
37305
  });
35224
37306
  }
@@ -35604,11 +37686,11 @@ var init_mssql = __esm({
35604
37686
  };
35605
37687
  }
35606
37688
  await withConnectTimeout(
35607
- () => new Promise((resolve20, reject) => {
37689
+ () => new Promise((resolve21, reject) => {
35608
37690
  this.connection = new Connection(tediousConfig);
35609
37691
  this.connection.on("connect", (err) => {
35610
37692
  if (err) reject(err);
35611
- else resolve20();
37693
+ else resolve21();
35612
37694
  });
35613
37695
  this.connection.connect();
35614
37696
  }),
@@ -35653,11 +37735,11 @@ var init_mssql = __esm({
35653
37735
  const tediousModule = requireTedious();
35654
37736
  const Request = tediousModule.Request;
35655
37737
  const TYPES = tediousModule.TYPES;
35656
- return new Promise((resolve20, reject) => {
37738
+ return new Promise((resolve21, reject) => {
35657
37739
  const rows = [];
35658
37740
  const request = new Request(sql, (err, rowCount) => {
35659
37741
  if (err) reject(err);
35660
- else resolve20({ rows, rowCount });
37742
+ else resolve21({ rows, rowCount });
35661
37743
  });
35662
37744
  if (params) {
35663
37745
  params.forEach((p, i) => {
@@ -35859,8 +37941,8 @@ var init_mssql = __esm({
35859
37941
  throw new Error("Use startTransactionAsync() for MSSQL.");
35860
37942
  }
35861
37943
  async startTransactionAsync() {
35862
- await new Promise((resolve20, reject) => {
35863
- this.connection.beginTransaction((err) => err ? reject(err) : resolve20());
37944
+ await new Promise((resolve21, reject) => {
37945
+ this.connection.beginTransaction((err) => err ? reject(err) : resolve21());
35864
37946
  });
35865
37947
  this._inTransaction = true;
35866
37948
  }
@@ -35868,8 +37950,8 @@ var init_mssql = __esm({
35868
37950
  throw new Error("Use commitAsync() for MSSQL.");
35869
37951
  }
35870
37952
  async commitAsync() {
35871
- await new Promise((resolve20, reject) => {
35872
- this.connection.commitTransaction((err) => err ? reject(err) : resolve20());
37953
+ await new Promise((resolve21, reject) => {
37954
+ this.connection.commitTransaction((err) => err ? reject(err) : resolve21());
35873
37955
  });
35874
37956
  this._inTransaction = false;
35875
37957
  }
@@ -35877,8 +37959,8 @@ var init_mssql = __esm({
35877
37959
  throw new Error("Use rollbackAsync() for MSSQL.");
35878
37960
  }
35879
37961
  async rollbackAsync() {
35880
- await new Promise((resolve20, reject) => {
35881
- this.connection.rollbackTransaction((err) => err ? reject(err) : resolve20());
37962
+ await new Promise((resolve21, reject) => {
37963
+ this.connection.rollbackTransaction((err) => err ? reject(err) : resolve21());
35882
37964
  });
35883
37965
  this._inTransaction = false;
35884
37966
  }
@@ -36203,8 +38285,8 @@ var init_firebird = __esm({
36203
38285
  }
36204
38286
  attachOnce(config) {
36205
38287
  const fb = requireFirebird();
36206
- return new Promise((resolve20, reject) => {
36207
- fb.attach(config, (err, db) => err ? reject(err) : resolve20(db));
38288
+ return new Promise((resolve21, reject) => {
38289
+ fb.attach(config, (err, db) => err ? reject(err) : resolve21(db));
36208
38290
  });
36209
38291
  }
36210
38292
  /**
@@ -36223,7 +38305,7 @@ var init_firebird = __esm({
36223
38305
  } catch (err) {
36224
38306
  lastError = err;
36225
38307
  if (attempt < attempts - 1) {
36226
- await new Promise((resolve20) => setTimeout(resolve20, 100 * (attempt + 1)));
38308
+ await new Promise((resolve21) => setTimeout(resolve21, 100 * (attempt + 1)));
36227
38309
  }
36228
38310
  }
36229
38311
  }
@@ -36306,19 +38388,19 @@ var init_firebird = __esm({
36306
38388
  }
36307
38389
  queryPromise(sql, params) {
36308
38390
  const translated = this.translateSql(sql);
36309
- return this.withReconnect(() => new Promise((resolve20, reject) => {
38391
+ return this.withReconnect(() => new Promise((resolve21, reject) => {
36310
38392
  this.statementHandle().query(translated, params ?? [], (err, result) => {
36311
38393
  if (err) reject(err);
36312
- else resolve20(result ?? []);
38394
+ else resolve21(result ?? []);
36313
38395
  });
36314
38396
  }));
36315
38397
  }
36316
38398
  executePromise(sql, params) {
36317
38399
  const translated = this.translateSql(sql);
36318
- return this.withReconnect(() => new Promise((resolve20, reject) => {
38400
+ return this.withReconnect(() => new Promise((resolve21, reject) => {
36319
38401
  this.statementHandle().execute(translated, params ?? [], (err) => {
36320
38402
  if (err) reject(err);
36321
- else resolve20();
38403
+ else resolve21();
36322
38404
  });
36323
38405
  }));
36324
38406
  }
@@ -36357,13 +38439,13 @@ var init_firebird = __esm({
36357
38439
  * and no bytes round-tripped (FB-BLOB-SRP-UNVERIFIED).
36358
38440
  */
36359
38441
  readBlob(blobFn) {
36360
- return new Promise((resolve20, reject) => {
38442
+ return new Promise((resolve21, reject) => {
36361
38443
  blobFn((err, _name, emitter) => {
36362
38444
  if (err) return reject(err);
36363
- if (!emitter) return resolve20(null);
38445
+ if (!emitter) return resolve21(null);
36364
38446
  const chunks = [];
36365
38447
  emitter.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
36366
- emitter.on("end", () => resolve20(Buffer.concat(chunks)));
38448
+ emitter.on("end", () => resolve21(Buffer.concat(chunks)));
36367
38449
  emitter.on("error", (streamErr) => reject(streamErr));
36368
38450
  });
36369
38451
  });
@@ -36499,12 +38581,12 @@ var init_firebird = __esm({
36499
38581
  }
36500
38582
  async startTransactionAsync() {
36501
38583
  this.ensureConnected();
36502
- await new Promise((resolve20, reject) => {
38584
+ await new Promise((resolve21, reject) => {
36503
38585
  this.db.transaction(0, (err, transaction) => {
36504
38586
  if (err) reject(err);
36505
38587
  else {
36506
38588
  this.transaction = transaction;
36507
- resolve20();
38589
+ resolve21();
36508
38590
  }
36509
38591
  });
36510
38592
  });
@@ -36514,12 +38596,12 @@ var init_firebird = __esm({
36514
38596
  }
36515
38597
  async commitAsync() {
36516
38598
  if (!this.transaction) throw new Error("No active transaction to commit.");
36517
- await new Promise((resolve20, reject) => {
38599
+ await new Promise((resolve21, reject) => {
36518
38600
  this.transaction.commit((err) => {
36519
38601
  if (err) reject(err);
36520
38602
  else {
36521
38603
  this.transaction = null;
36522
- resolve20();
38604
+ resolve21();
36523
38605
  }
36524
38606
  });
36525
38607
  });
@@ -36529,12 +38611,12 @@ var init_firebird = __esm({
36529
38611
  }
36530
38612
  async rollbackAsync() {
36531
38613
  if (!this.transaction) throw new Error("No active transaction to rollback.");
36532
- await new Promise((resolve20, reject) => {
38614
+ await new Promise((resolve21, reject) => {
36533
38615
  this.transaction.rollback((err) => {
36534
38616
  if (err) reject(err);
36535
38617
  else {
36536
38618
  this.transaction = null;
36537
- resolve20();
38619
+ resolve21();
36538
38620
  }
36539
38621
  });
36540
38622
  });
@@ -39014,7 +41096,7 @@ var init_database = __esm({
39014
41096
 
39015
41097
  // src/model.ts
39016
41098
  import { readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
39017
- import { join as join29, extname as extname7 } from "node:path";
41099
+ import { join as join30, extname as extname7 } from "node:path";
39018
41100
  async function discoverModels(modelsDir) {
39019
41101
  const models = [];
39020
41102
  let files;
@@ -39024,7 +41106,7 @@ async function discoverModels(modelsDir) {
39024
41106
  return models;
39025
41107
  }
39026
41108
  for (const file of files) {
39027
- const filePath = join29(modelsDir, file);
41109
+ const filePath = join30(modelsDir, file);
39028
41110
  const stat = statSync17(filePath);
39029
41111
  if (!stat.isFile()) continue;
39030
41112
  const ext = extname7(file);
@@ -39066,8 +41148,8 @@ var init_model = __esm({
39066
41148
  });
39067
41149
 
39068
41150
  // src/migration.ts
39069
- import { existsSync as existsSync26, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "node:fs";
39070
- import { join as join30, resolve as resolve18 } from "node:path";
41151
+ import { existsSync as existsSync27, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17 } from "node:fs";
41152
+ import { join as join31, resolve as resolve19 } from "node:path";
39071
41153
  function unwrapAdapter(db) {
39072
41154
  let cur = db;
39073
41155
  while (cur && cur.constructor?.name === "CachedDatabaseAdapter" && cur.adapter) {
@@ -39383,15 +41465,15 @@ async function rollback(migrationsDir, delimiter2) {
39383
41465
  }
39384
41466
  return rolledBack2;
39385
41467
  }
39386
- const dir = resolve18(migrationsDir ?? "migrations");
41468
+ const dir = resolve19(migrationsDir ?? "migrations");
39387
41469
  const delim = delimiter2 ?? ";";
39388
41470
  const db = getAdapter();
39389
41471
  const migrations = await getLastBatchMigrations();
39390
41472
  const rolledBack = [];
39391
41473
  for (const migration of migrations) {
39392
41474
  const downFile = `${migration.migration_name}.down.sql`;
39393
- const downPath = join30(dir, downFile);
39394
- if (!existsSync26(downPath)) {
41475
+ const downPath = join31(dir, downFile);
41476
+ if (!existsSync27(downPath)) {
39395
41477
  throw new Error(
39396
41478
  `Cannot rollback ${migration.migration_name}: no .down.sql file found`
39397
41479
  );
@@ -39555,10 +41637,10 @@ function warnUnprefixedMigrations(files) {
39555
41637
  }
39556
41638
  async function migrate(adapter, options) {
39557
41639
  const db = adapter ?? getAdapter();
39558
- const dir = resolve18(options?.migrationsDir ?? "migrations");
41640
+ const dir = resolve19(options?.migrationsDir ?? "migrations");
39559
41641
  const delimiter2 = options?.delimiter ?? ";";
39560
41642
  const result = { applied: [], skipped: [], failed: [] };
39561
- if (!existsSync26(dir)) {
41643
+ if (!existsSync27(dir)) {
39562
41644
  return result;
39563
41645
  }
39564
41646
  await ensureMigrationTableOn(db);
@@ -39594,7 +41676,7 @@ async function migrate(adapter, options) {
39594
41676
  result.skipped.push(file);
39595
41677
  continue;
39596
41678
  }
39597
- const sqlContent = readFileSync25(join30(dir, file), "utf-8").trim();
41679
+ const sqlContent = readFileSync25(join31(dir, file), "utf-8").trim();
39598
41680
  if (!sqlContent) {
39599
41681
  result.skipped.push(file);
39600
41682
  continue;
@@ -39629,9 +41711,9 @@ async function migrate(adapter, options) {
39629
41711
  }
39630
41712
  async function status(adapter, options) {
39631
41713
  const db = adapter ?? getAdapter();
39632
- const dir = resolve18(options?.migrationsDir ?? "migrations");
41714
+ const dir = resolve19(options?.migrationsDir ?? "migrations");
39633
41715
  const result = { completed: [], pending: [] };
39634
- if (!existsSync26(dir)) {
41716
+ if (!existsSync27(dir)) {
39635
41717
  return result;
39636
41718
  }
39637
41719
  if (!await adapterTableExists(db, MIGRATION_TABLE)) {
@@ -39677,13 +41759,13 @@ async function createMigration(description, options) {
39677
41759
  if (kind === "code" || kind === "class") {
39678
41760
  return createClassMigration(description, options);
39679
41761
  }
39680
- const dir = resolve18(options?.migrationsDir ?? "migrations");
39681
- if (!existsSync26(dir)) {
39682
- mkdirSync18(dir, { recursive: true });
41762
+ const dir = resolve19(options?.migrationsDir ?? "migrations");
41763
+ if (!existsSync27(dir)) {
41764
+ mkdirSync19(dir, { recursive: true });
39683
41765
  }
39684
41766
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
39685
41767
  const now = /* @__PURE__ */ new Date();
39686
- const timestamp = [
41768
+ const timestamp2 = [
39687
41769
  now.getFullYear(),
39688
41770
  String(now.getMonth() + 1).padStart(2, "0"),
39689
41771
  String(now.getDate()).padStart(2, "0"),
@@ -39691,10 +41773,10 @@ async function createMigration(description, options) {
39691
41773
  String(now.getMinutes()).padStart(2, "0"),
39692
41774
  String(now.getSeconds()).padStart(2, "0")
39693
41775
  ].join("");
39694
- const upFileName = `${timestamp}_${safeName}.sql`;
39695
- const downFileName = `${timestamp}_${safeName}.down.sql`;
39696
- const upPath = join30(dir, upFileName);
39697
- const downPath = join30(dir, downFileName);
41776
+ const upFileName = `${timestamp2}_${safeName}.sql`;
41777
+ const downFileName = `${timestamp2}_${safeName}.down.sql`;
41778
+ const upPath = join31(dir, upFileName);
41779
+ const downPath = join31(dir, downFileName);
39698
41780
  const upTemplate = `-- Migration: ${description}
39699
41781
  -- Created: ${now.toISOString()}
39700
41782
 
@@ -39703,19 +41785,19 @@ async function createMigration(description, options) {
39703
41785
  -- Created: ${now.toISOString()}
39704
41786
 
39705
41787
  `;
39706
- writeFileSync16(upPath, upTemplate, "utf-8");
39707
- writeFileSync16(downPath, downTemplate, "utf-8");
41788
+ writeFileSync17(upPath, upTemplate, "utf-8");
41789
+ writeFileSync17(downPath, downTemplate, "utf-8");
39708
41790
  return { upPath, downPath };
39709
41791
  }
39710
41792
  async function createClassMigration(description, options) {
39711
- const dir = resolve18(options?.migrationsDir ?? "migrations");
39712
- if (!existsSync26(dir)) {
39713
- mkdirSync18(dir, { recursive: true });
41793
+ const dir = resolve19(options?.migrationsDir ?? "migrations");
41794
+ if (!existsSync27(dir)) {
41795
+ mkdirSync19(dir, { recursive: true });
39714
41796
  }
39715
41797
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
39716
41798
  const className = description.replace(/[^a-zA-Z0-9 ]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
39717
41799
  const now = /* @__PURE__ */ new Date();
39718
- const timestamp = [
41800
+ const timestamp2 = [
39719
41801
  now.getFullYear(),
39720
41802
  String(now.getMonth() + 1).padStart(2, "0"),
39721
41803
  String(now.getDate()).padStart(2, "0"),
@@ -39723,8 +41805,8 @@ async function createClassMigration(description, options) {
39723
41805
  String(now.getMinutes()).padStart(2, "0"),
39724
41806
  String(now.getSeconds()).padStart(2, "0")
39725
41807
  ].join("");
39726
- const fileName = `${timestamp}_${safeName}.ts`;
39727
- const filePath = join30(dir, fileName);
41808
+ const fileName = `${timestamp2}_${safeName}.ts`;
41809
+ const filePath = join31(dir, fileName);
39728
41810
  const content = `// Migration: ${description}
39729
41811
  // Created: ${now.toISOString()}
39730
41812
 
@@ -39740,7 +41822,7 @@ export class ${className} {
39740
41822
  }
39741
41823
  }
39742
41824
  `;
39743
- writeFileSync16(filePath, content, "utf-8");
41825
+ writeFileSync17(filePath, content, "utf-8");
39744
41826
  return filePath;
39745
41827
  }
39746
41828
  var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, Migration;
@@ -39821,8 +41903,8 @@ var init_migration = __esm({
39821
41903
  }
39822
41904
  /** Return sorted list of all migration files on disk (excludes .down.sql). */
39823
41905
  getFiles() {
39824
- const dir = resolve18(this.dir);
39825
- if (!existsSync26(dir)) return [];
41906
+ const dir = resolve19(this.dir);
41907
+ if (!existsSync27(dir)) return [];
39826
41908
  return sortMigrationFiles(
39827
41909
  readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
39828
41910
  );
@@ -42606,8 +44688,8 @@ var init_seeder = __esm({
42606
44688
  // src/docstore.ts
42607
44689
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
42608
44690
  import { randomBytes as randomBytes9 } from "node:crypto";
42609
- import { mkdirSync as mkdirSync19 } from "node:fs";
42610
- import { dirname as dirname14, isAbsolute as isAbsolute6, join as join31 } from "node:path";
44691
+ import { mkdirSync as mkdirSync20 } from "node:fs";
44692
+ import { dirname as dirname14, isAbsolute as isAbsolute6, join as join32 } from "node:path";
42611
44693
  function iso(d) {
42612
44694
  return d.toISOString();
42613
44695
  }
@@ -42872,8 +44954,8 @@ function resolveStorePath(dbPath) {
42872
44954
  if (dbPath === ":memory:") return dbPath;
42873
44955
  let path8 = dbPath;
42874
44956
  if (!isAbsolute6(path8)) {
42875
- path8 = join31(process.cwd(), path8);
42876
- mkdirSync19(dirname14(path8), { recursive: true });
44957
+ path8 = join32(process.cwd(), path8);
44958
+ mkdirSync20(dirname14(path8), { recursive: true });
42877
44959
  }
42878
44960
  return path8;
42879
44961
  }
@@ -44156,8 +46238,8 @@ var init_attachment = __esm({
44156
46238
 
44157
46239
  // src/realtime/storage.ts
44158
46240
  import { randomBytes as randomBytes10 } from "node:crypto";
44159
- import { mkdirSync as mkdirSync20, readFileSync as readFileSync26, writeFileSync as writeFileSync17, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
44160
- import { resolve as resolve19, sep as sep5 } from "node:path";
46241
+ import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync18, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
46242
+ import { resolve as resolve20, sep as sep6 } from "node:path";
44161
46243
  import { createRequire as createRequire8 } from "node:module";
44162
46244
  function storageKey(filename = "") {
44163
46245
  let ext = "";
@@ -44193,19 +46275,19 @@ var init_storage = __esm({
44193
46275
  LocalStorage = class {
44194
46276
  directory;
44195
46277
  constructor(directory) {
44196
- this.directory = resolve19(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
44197
- mkdirSync20(this.directory, { recursive: true });
46278
+ this.directory = resolve20(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
46279
+ mkdirSync21(this.directory, { recursive: true });
44198
46280
  }
44199
46281
  // Resolve inside the root and reject any traversal attempt.
44200
46282
  pathFor(key) {
44201
- const target = resolve19(this.directory, key);
44202
- if (target !== this.directory && !target.startsWith(this.directory + sep5)) {
46283
+ const target = resolve20(this.directory, key);
46284
+ if (target !== this.directory && !target.startsWith(this.directory + sep6)) {
44203
46285
  throw new Error(`unsafe storage key: ${JSON.stringify(key)}`);
44204
46286
  }
44205
46287
  return target;
44206
46288
  }
44207
46289
  put(key, data) {
44208
- writeFileSync17(this.pathFor(key), data);
46290
+ writeFileSync18(this.pathFor(key), data);
44209
46291
  }
44210
46292
  get(key) {
44211
46293
  try {