tina4-nodejs 3.13.120 → 3.13.121

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)) {
@@ -7161,7 +7161,7 @@ ${s}\r
7161
7161
  connect() {
7162
7162
  if (this.connected) return Promise.resolve();
7163
7163
  if (this.connecting) return this.connecting;
7164
- this.connecting = new Promise((resolve20, reject) => {
7164
+ this.connecting = new Promise((resolve21, reject) => {
7165
7165
  const sock = net.createConnection({ host: this.host, port: this.port });
7166
7166
  sock.setNoDelay(true);
7167
7167
  const onError = (err) => {
@@ -7198,7 +7198,7 @@ ${s}\r
7198
7198
  sock.on("error", (e) => {
7199
7199
  this.brokenError = e;
7200
7200
  });
7201
- resolve20();
7201
+ resolve21();
7202
7202
  } catch (e) {
7203
7203
  onError(e);
7204
7204
  }
@@ -7261,12 +7261,12 @@ ${s}\r
7261
7261
  }
7262
7262
  /** Send one command and await its reply (assumes socket is up). */
7263
7263
  raw(args) {
7264
- return new Promise((resolve20, reject) => {
7264
+ return new Promise((resolve21, reject) => {
7265
7265
  if (!this.sock || this.sock.destroyed) {
7266
7266
  reject(this.brokenError ?? new Error("redis socket not connected"));
7267
7267
  return;
7268
7268
  }
7269
- this.waiters.push({ resolve: resolve20, reject });
7269
+ this.waiters.push({ resolve: resolve21, reject });
7270
7270
  this.sock.write(_RespClient.encode(args));
7271
7271
  });
7272
7272
  }
@@ -7608,7 +7608,7 @@ ${s}\r
7608
7608
  connect() {
7609
7609
  if (this.connected) return Promise.resolve();
7610
7610
  if (this.connecting) return this.connecting;
7611
- this.connecting = new Promise((resolve20, reject) => {
7611
+ this.connecting = new Promise((resolve21, reject) => {
7612
7612
  const sock = net.createConnection({ host: this.host, port: this.port });
7613
7613
  sock.setNoDelay(true);
7614
7614
  sock.once("error", (err) => {
@@ -7629,7 +7629,7 @@ ${s}\r
7629
7629
  p.resolve(this.buffer.toString("utf-8"));
7630
7630
  }
7631
7631
  });
7632
- resolve20();
7632
+ resolve21();
7633
7633
  });
7634
7634
  });
7635
7635
  return this.connecting;
@@ -7662,13 +7662,13 @@ ${s}\r
7662
7662
  async send(payload, terminator) {
7663
7663
  await this.connect();
7664
7664
  if (!this.sock || this.sock.destroyed) return "";
7665
- return new Promise((resolve20) => {
7665
+ return new Promise((resolve21) => {
7666
7666
  this.buffer = Buffer.alloc(0);
7667
- this.pending = { terminator, resolve: resolve20 };
7667
+ this.pending = { terminator, resolve: resolve21 };
7668
7668
  const timer = setTimeout(() => {
7669
- if (this.pending && this.pending.resolve === resolve20) {
7669
+ if (this.pending && this.pending.resolve === resolve21) {
7670
7670
  this.pending = null;
7671
- resolve20(this.buffer.toString("utf-8"));
7671
+ resolve21(this.buffer.toString("utf-8"));
7672
7672
  }
7673
7673
  }, 4e3);
7674
7674
  if (timer.unref) timer.unref();
@@ -9434,7 +9434,7 @@ async function parseBody(req2) {
9434
9434
  }
9435
9435
  const contentType = req2.headers["content-type"] ?? "";
9436
9436
  const chunks = [];
9437
- await new Promise((resolve20, reject) => {
9437
+ await new Promise((resolve21, reject) => {
9438
9438
  let received = 0;
9439
9439
  let refused = false;
9440
9440
  req2.on("data", (chunk) => {
@@ -9449,7 +9449,7 @@ async function parseBody(req2) {
9449
9449
  chunks.push(chunk);
9450
9450
  });
9451
9451
  req2.on("end", () => {
9452
- if (!refused) resolve20();
9452
+ if (!refused) resolve21();
9453
9453
  });
9454
9454
  req2.on("error", reject);
9455
9455
  });
@@ -12682,6 +12682,2038 @@ var init_version = __esm({
12682
12682
  }
12683
12683
  });
12684
12684
 
12685
+ // ../cli/src/commands/generate.ts
12686
+ var generate_exports = {};
12687
+ __export(generate_exports, {
12688
+ DEFAULT_FIELDS: () => DEFAULT_FIELDS,
12689
+ GENERATORS: () => GENERATORS,
12690
+ RESOLUTION_ENVELOPE_VERSION: () => RESOLUTION_ENVELOPE_VERSION,
12691
+ SQL_RESERVED_TABLE_NAMES: () => SQL_RESERVED_TABLE_NAMES,
12692
+ aiFill: () => aiFill,
12693
+ currentResolution: () => currentResolution,
12694
+ extend: () => extend,
12695
+ fieldsOrDefault: () => fieldsOrDefault,
12696
+ generate: () => generate,
12697
+ generateMigration: () => generateMigration,
12698
+ generateProgrammatic: () => generateProgrammatic,
12699
+ parseCliArgs: () => parseCliArgs,
12700
+ parseEvery: () => parseEvery,
12701
+ parseFields: () => parseFields,
12702
+ pluralizeReserved: () => pluralizeReserved,
12703
+ toPascal: () => toPascal,
12704
+ toSnake: () => toSnake,
12705
+ toTableName: () => toTableName
12706
+ });
12707
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "node:fs";
12708
+ import { join as join15, relative as relative2, resolve as resolve6, sep as sep3 } from "node:path";
12709
+ function ensureDir(dir) {
12710
+ if (__resolution.dryRun) return;
12711
+ if (!existsSync13(dir)) {
12712
+ mkdirSync8(dir, { recursive: true });
12713
+ }
12714
+ }
12715
+ function writeFileSafe(path8, content) {
12716
+ captureEditHints(path8, content);
12717
+ if (__resolution.dryRun) {
12718
+ return;
12719
+ }
12720
+ if (existsSync13(path8)) {
12721
+ if (!__resolution.jsonMode) console.log(` File already exists: ${path8}`);
12722
+ return;
12723
+ }
12724
+ writeFileSync7(path8, content, "utf-8");
12725
+ __resolution.actionsTaken.push(`wrote ${path8}`);
12726
+ if (!__resolution.jsonMode) console.log(` Created ${path8}`);
12727
+ }
12728
+ function toSnake(name) {
12729
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
12730
+ }
12731
+ function pluralizeReserved(name) {
12732
+ if (name.endsWith("y") && !/[aeiouy]y$/i.test(name)) return name.slice(0, -1) + "ies";
12733
+ if (/(s|x|z|ch|sh)$/.test(name)) return name + "es";
12734
+ return name + "s";
12735
+ }
12736
+ function toTableName(name) {
12737
+ const raw = toSnake(name);
12738
+ if (SQL_RESERVED_TABLE_NAMES.has(raw)) {
12739
+ const safe = pluralizeReserved(raw);
12740
+ recordTransformation({
12741
+ kind: "reserved_word_pluralize",
12742
+ from: raw,
12743
+ to: safe,
12744
+ reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
12745
+ override: `--table ${raw} --quote (requires quoted-identifier mode, not yet implemented)`
12746
+ });
12747
+ return safe;
12748
+ }
12749
+ return raw;
12750
+ }
12751
+ function resetResolution(target, input, opts) {
12752
+ __resolution.target = target;
12753
+ __resolution.input = input;
12754
+ __resolution.body = { transformations: [] };
12755
+ __resolution.actionsTaken = [];
12756
+ __resolution.dryRun = opts.dryRun;
12757
+ __resolution.jsonMode = opts.jsonMode;
12758
+ }
12759
+ function recordTransformation(t) {
12760
+ __resolution.body.transformations.push(t);
12761
+ }
12762
+ function currentResolution() {
12763
+ const body = {
12764
+ ...__resolution.body,
12765
+ transformations: [...__resolution.body.transformations]
12766
+ };
12767
+ if (__resolution.body.edit_hints) {
12768
+ body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
12769
+ }
12770
+ if (__resolution.body.next) {
12771
+ body.next = [...__resolution.body.next];
12772
+ }
12773
+ if (__resolution.body.test_paths) {
12774
+ body.test_paths = [...__resolution.body.test_paths];
12775
+ }
12776
+ if (__resolution.body.routes) {
12777
+ body.routes = [...__resolution.body.routes];
12778
+ }
12779
+ return {
12780
+ command: "generate",
12781
+ target: __resolution.target,
12782
+ input: { ...__resolution.input },
12783
+ resolution: body,
12784
+ actions_taken: [...__resolution.actionsTaken],
12785
+ dry_run: __resolution.dryRun
12786
+ };
12787
+ }
12788
+ function setResolutionField(key, value) {
12789
+ __resolution.body[key] = value;
12790
+ }
12791
+ function pushRoute(routePattern) {
12792
+ if (!__resolution.body.routes) __resolution.body.routes = [];
12793
+ __resolution.body.routes.push(routePattern);
12794
+ }
12795
+ function pushTestPath(path8) {
12796
+ if (!__resolution.body.test_paths) __resolution.body.test_paths = [];
12797
+ __resolution.body.test_paths.push(path8);
12798
+ }
12799
+ function pushEditHint(hint) {
12800
+ if (!__resolution.body.edit_hints) __resolution.body.edit_hints = [];
12801
+ __resolution.body.edit_hints.push(hint);
12802
+ }
12803
+ function setNextSteps(steps) {
12804
+ if (steps.length === 0) return;
12805
+ __resolution.body.next = [...steps];
12806
+ }
12807
+ function toRelPath(absPath) {
12808
+ const cwd = process.cwd();
12809
+ const rel = relative2(cwd, absPath);
12810
+ if (!rel) return absPath;
12811
+ return sep3 === "/" ? rel : rel.split(sep3).join("/");
12812
+ }
12813
+ function captureEditHints(absPath, content) {
12814
+ if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
12815
+ const relPath = toRelPath(absPath);
12816
+ const lines = content.split("\n");
12817
+ for (let i = 0; i < lines.length; i++) {
12818
+ const match = TINA4_EDIT_MARKER.exec(lines[i]);
12819
+ if (match) {
12820
+ pushEditHint({ file: relPath, line: i + 1, label: match[1].trim() });
12821
+ }
12822
+ }
12823
+ }
12824
+ function printResolution() {
12825
+ if (__resolution.jsonMode) {
12826
+ process.stdout.write(JSON.stringify(currentResolution(), null, 2) + "\n");
12827
+ return;
12828
+ }
12829
+ const b = __resolution.body;
12830
+ const lines = [];
12831
+ lines.push("");
12832
+ lines.push(`Generated ${__resolution.target} ${__resolution.input.name}`);
12833
+ if (b.class_name || b.file_path) {
12834
+ const where = b.file_path ? ` (in ${b.file_path})` : "";
12835
+ lines.push(` class ${b.class_name ?? __resolution.input.name}${where}`);
12836
+ }
12837
+ if (b.table_name) {
12838
+ const t = b.transformations.find((x) => x.kind === "reserved_word_pluralize");
12839
+ const note = t ? ` (auto-pluralized: '${t.from}' is a SQL reserved word)` : "";
12840
+ lines.push(` table ${b.table_name}${note}`);
12841
+ }
12842
+ if (b.routes && b.routes.length) {
12843
+ lines.push(` routes ${b.routes.join(", ")}`);
12844
+ }
12845
+ if (b.migration_path) {
12846
+ lines.push(` migration ${b.migration_path}`);
12847
+ }
12848
+ const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
12849
+ if (reserved && reserved.from && reserved.override) {
12850
+ lines.push("");
12851
+ lines.push(` To keep the raw name '${reserved.from}' as the table:`);
12852
+ lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
12853
+ }
12854
+ if (b.test_paths && b.test_paths.length > 0) {
12855
+ lines.push("");
12856
+ lines.push(" Tests:");
12857
+ for (const testPath of b.test_paths) lines.push(` ${testPath}`);
12858
+ }
12859
+ if (b.edit_hints && b.edit_hints.length > 0) {
12860
+ lines.push("");
12861
+ lines.push(" Edit these lines:");
12862
+ for (const hint of b.edit_hints) {
12863
+ lines.push(` ${hint.file}:${hint.line} ${hint.label}`);
12864
+ }
12865
+ }
12866
+ if (b.next && b.next.length > 0) {
12867
+ lines.push("");
12868
+ lines.push(" Next:");
12869
+ for (const step of b.next) lines.push(` ${step}`);
12870
+ }
12871
+ lines.push("");
12872
+ process.stderr.write(lines.join("\n"));
12873
+ }
12874
+ function toPlural(name) {
12875
+ const lower = name.toLowerCase();
12876
+ if (lower.endsWith("s")) return lower;
12877
+ if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
12878
+ return lower + "s";
12879
+ }
12880
+ function toCamel(name) {
12881
+ return name.charAt(0).toLowerCase() + name.slice(1);
12882
+ }
12883
+ function toPascal(name) {
12884
+ return name.split(/[^0-9a-zA-Z]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
12885
+ }
12886
+ function parseFields(fieldsStr) {
12887
+ if (!fieldsStr || !fieldsStr.trim()) return [];
12888
+ const result = [];
12889
+ for (const part of fieldsStr.split(",")) {
12890
+ const trimmed = part.trim();
12891
+ if (trimmed.includes(":")) {
12892
+ const [fname, ftype] = trimmed.split(":", 2);
12893
+ if (fname.trim()) result.push([fname.trim(), ftype.trim().toLowerCase()]);
12894
+ } else if (trimmed) {
12895
+ result.push([trimmed, "string"]);
12896
+ }
12897
+ }
12898
+ return result;
12899
+ }
12900
+ function fieldsOrDefault(fieldsStr) {
12901
+ const parsed = parseFields(fieldsStr);
12902
+ return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
12903
+ }
12904
+ function parseCliArgs(args) {
12905
+ const booleanFlags = /* @__PURE__ */ new Set([
12906
+ "no-browser",
12907
+ "no-reload",
12908
+ "production",
12909
+ "managed",
12910
+ "all",
12911
+ "clear",
12912
+ "public",
12913
+ "no-migration",
12914
+ // Suppress the co-emitted migration test (used by the migrate:create
12915
+ // delegation — a plain migrate:create is "just a migration, no test",
12916
+ // matching its pre-3.13.121 UX now that it routes through generate migration).
12917
+ "no-test",
12918
+ // Resolution transparency (Feature B, 3.13.117): both accept NO value.
12919
+ "json",
12920
+ "dry-run"
12921
+ ]);
12922
+ const flags = {};
12923
+ const positional = [];
12924
+ let i = 0;
12925
+ while (i < args.length) {
12926
+ if (args[i].startsWith("--")) {
12927
+ const key = args[i].slice(2);
12928
+ if (booleanFlags.has(key)) {
12929
+ flags[key] = true;
12930
+ i += 1;
12931
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
12932
+ flags[key] = args[i + 1];
12933
+ i += 2;
12934
+ } else {
12935
+ flags[key] = true;
12936
+ i += 1;
12937
+ }
12938
+ } else {
12939
+ positional.push(args[i]);
12940
+ i += 1;
12941
+ }
12942
+ }
12943
+ return { flags, positional };
12944
+ }
12945
+ function parseEvery(every) {
12946
+ if (!every || every === true) return 60;
12947
+ const s = String(every).trim().toLowerCase();
12948
+ const units = { s: 1, m: 60, h: 3600, d: 86400 };
12949
+ const unit = s.slice(-1);
12950
+ if (unit in units) {
12951
+ const n2 = parseFloat(s.slice(0, -1));
12952
+ return Number.isFinite(n2) ? Math.max(1, Math.round(n2 * units[unit])) : 60;
12953
+ }
12954
+ const n = parseFloat(s);
12955
+ return Number.isFinite(n) ? Math.max(1, Math.round(n)) : 60;
12956
+ }
12957
+ function aiFill(fn, spec, indent = " ") {
12958
+ const rule = (label) => "\u2500".repeat(Math.max(4, 46 - label.length));
12959
+ const lines = [`${indent}// \u2500\u2500\u2500 AI-FILL: ${fn} ${rule(fn)}`];
12960
+ lines.push(`${indent}// Intent: ${spec.intent}`);
12961
+ if (spec.given) lines.push(`${indent}// Given: ${spec.given}`);
12962
+ lines.push(`${indent}// Use: ${spec.use}`);
12963
+ if (spec.ret) lines.push(`${indent}// Return: ${spec.ret}`);
12964
+ lines.push(`${indent}// Ground: ${spec.ground}`);
12965
+ lines.push(`${indent}throw new Error(${JSON.stringify(spec.raise)}); // remove when implemented`);
12966
+ lines.push(`${indent}// ${"\u2500".repeat(52)}`);
12967
+ return lines.join("\n") + "\n";
12968
+ }
12969
+ function extend(note, hint = "", indent = " ") {
12970
+ let out = `${indent}// \u2500\u2500\u2500 EXTEND: ${note} ${"\u2500".repeat(Math.max(4, 46 - note.length))}
12971
+ `;
12972
+ if (hint) out += `${indent}// ${hint}
12973
+ `;
12974
+ return out;
12975
+ }
12976
+ function timestamp() {
12977
+ const now = /* @__PURE__ */ new Date();
12978
+ 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");
12979
+ }
12980
+ function isoNow() {
12981
+ return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
12982
+ }
12983
+ async function generate(what, name, extraArgs = []) {
12984
+ if (!what) {
12985
+ console.error(" Usage: tina4nodejs generate <what> <name> [options]");
12986
+ console.error(` Generators: ${GENERATOR_LIST}`);
12987
+ console.error(' Options: --fields "name:string,price:float" --model ModelName');
12988
+ console.error(" --public open a route's writes (default: secure)");
12989
+ console.error(' --every 5m | --cron "\u2026" service schedule');
12990
+ console.error(" --json emit machine-readable resolution envelope on stdout");
12991
+ console.error(" --dry-run report resolution without writing any files");
12992
+ process.exit(1);
12993
+ }
12994
+ const noNameGenerators = /* @__PURE__ */ new Set(["auth"]);
12995
+ if (noNameGenerators.has(what) && name.startsWith("--")) {
12996
+ extraArgs = [name, ...extraArgs];
12997
+ name = "";
12998
+ }
12999
+ if (!noNameGenerators.has(what) && !name) {
13000
+ console.error(` Usage: tina4nodejs generate ${what} <name> [options]`);
13001
+ process.exit(1);
13002
+ }
13003
+ const { flags } = parseCliArgs(extraArgs);
13004
+ const jsonMode = Boolean(flags.json);
13005
+ const dryRun = Boolean(flags["dry-run"]);
13006
+ resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode });
13007
+ const spec = GENERATORS[what];
13008
+ if (spec) {
13009
+ spec.handler(name, flags);
13010
+ } else {
13011
+ console.error(` Unknown generator: ${what}`);
13012
+ console.error(` Available: ${GENERATOR_LIST}`);
13013
+ process.exit(1);
13014
+ }
13015
+ const nextFn = NEXT_STEPS[what];
13016
+ if (nextFn) {
13017
+ const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
13018
+ setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
13019
+ }
13020
+ printResolution();
13021
+ }
13022
+ async function generateProgrammatic(what, name, extraArgs = []) {
13023
+ const spec = GENERATORS[what];
13024
+ if (!spec) throw new Error(`Unknown generator: ${what} (available: ${GENERATOR_LIST})`);
13025
+ const { flags } = parseCliArgs(extraArgs);
13026
+ const dryRun = Boolean(flags["dry-run"]);
13027
+ resetResolution(what, { name, fields: flags.fields ?? null }, { dryRun, jsonMode: true });
13028
+ spec.handler(name, flags);
13029
+ const nextFn = NEXT_STEPS[what];
13030
+ if (nextFn) {
13031
+ const resolvedTable = __resolution.body.table_name ?? (name ? SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name) : "");
13032
+ setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
13033
+ }
13034
+ return currentResolution();
13035
+ }
13036
+ function generateModel(name, flags, emitTest = true) {
13037
+ const fields = fieldsOrDefault(flags.fields || "");
13038
+ const table2 = toTableName(name);
13039
+ const dir = resolve6("src/models");
13040
+ ensureDir(dir);
13041
+ const path8 = join15(dir, `${name}.ts`);
13042
+ setResolutionField("class_name", name);
13043
+ setResolutionField("table_name", table2);
13044
+ setResolutionField("file_path", `src/models/${name}.ts`);
13045
+ pushTestPath(`tests/${table2}_model.test.ts`);
13046
+ const fieldLines = [
13047
+ ` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`,
13048
+ ` // tina4:edit add or change fields for this model (string,int,float,bool,text,datetime)`
13049
+ ];
13050
+ for (const [fname, ftype] of fields) {
13051
+ const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
13052
+ fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
13053
+ }
13054
+ fieldLines.push(` created_at: { type: "datetime" as const },`);
13055
+ const content = `import { BaseModel } from "tina4-nodejs/orm";
13056
+
13057
+ export default class ${name} extends BaseModel {
13058
+ static tableName = "${table2}";
13059
+ static fields = {
13060
+ ${fieldLines.join("\n")}
13061
+ };
13062
+ }
13063
+ `;
13064
+ writeFileSafe(path8, content);
13065
+ if (!flags["no-migration"]) {
13066
+ generateMigration(`create_${table2}`, flags, fields, table2, false);
13067
+ }
13068
+ if (emitTest) emitModelTest(name, table2, fields);
13069
+ }
13070
+ function secureOptOut(isPublic) {
13071
+ return isPublic ? `export const secure = false;
13072
+
13073
+ ` : "";
13074
+ }
13075
+ function generateRoute(name, flags, emitTest = true) {
13076
+ const routePath = name.replace(/^\//, "");
13077
+ const singular = routePath.endsWith("s") ? routePath.slice(0, -1) : routePath;
13078
+ const model = flags.model;
13079
+ const isPublic = Boolean(flags.public);
13080
+ const base = resolve6("src/routes/api", routePath);
13081
+ const idDir = join15(base, "[id]");
13082
+ ensureDir(base);
13083
+ ensureDir(idDir);
13084
+ pushRoute(`/api/${routePath}`);
13085
+ pushRoute(`/api/${routePath}/{id}`);
13086
+ if (__resolution.target === "route") {
13087
+ setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
13088
+ }
13089
+ const table2 = model ? toTableName(model) : "";
13090
+ const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
13091
+ ` : "";
13092
+ const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
13093
+ ` : "";
13094
+ const writeDoc = isPublic ? "Public (--public): no token required." : "Secure by default: requires a Bearer token (use --public to open).";
13095
+ if (model) {
13096
+ writeFileSafe(
13097
+ join15(base, "get.ts"),
13098
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13099
+ ${modelImportBase}
13100
+ export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
13101
+
13102
+ export default async function (req: Tina4Request, res: Tina4Response) {
13103
+ // tina4:edit tune pagination defaults or add filter/sort parsing here
13104
+ const page = parseInt(req.query.page as string) || 1;
13105
+ const limit = parseInt(req.query.limit as string) || 20;
13106
+ const offset = (page - 1) * limit;
13107
+ const rows = await ${model}.select("SELECT * FROM ${table2} LIMIT ? OFFSET ?", [limit, offset]);
13108
+ res.json({ data: rows.map((r) => r.toObject()), page, limit });
13109
+ }
13110
+ `
13111
+ );
13112
+ } else {
13113
+ writeFileSafe(
13114
+ join15(base, "get.ts"),
13115
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13116
+
13117
+ export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
13118
+
13119
+ export default async function (req: Tina4Request, res: Tina4Response) {
13120
+ ${aiFill(`list_${routePath}`, {
13121
+ intent: `return the ${routePath} collection (add pagination if it grows)`,
13122
+ given: "req.query -> filters/paging",
13123
+ use: `Model.select("SELECT \u2026 LIMIT ? OFFSET ?", [limit, offset]) then r.toObject()`,
13124
+ ret: "res.json({ data: rows })",
13125
+ ground: `tina4_context("list ORM records with pagination", "nodejs") \xB7 skill tina4-developer-nodejs`,
13126
+ raise: `${routePath} list not implemented`
13127
+ })}}
13128
+ `
13129
+ );
13130
+ }
13131
+ if (model) {
13132
+ writeFileSafe(
13133
+ join15(base, "post.ts"),
13134
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13135
+ ${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
13136
+
13137
+ // ${writeDoc}
13138
+ export default async function (req: Tina4Request, res: Tina4Response) {
13139
+ // tina4:edit validate the body before persist (Validator or hand-checks)
13140
+ ${extend(
13141
+ "validate / business rules before persist",
13142
+ `e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`
13143
+ )} const item = new ${model}(req.body as Record<string, unknown>);
13144
+ // save() returns false on failure rather than throwing - check it, or a failed
13145
+ // write is reported to the client as a 201 carrying unsaved data.
13146
+ if ((await item.save()) === false) {
13147
+ res.json({ error: "Could not create ${singular}" }, 400);
13148
+ return;
13149
+ }
13150
+ res.json({ data: item.toObject() }, 201);
13151
+ }
13152
+ `
13153
+ );
13154
+ } else {
13155
+ writeFileSafe(
13156
+ join15(base, "post.ts"),
13157
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13158
+
13159
+ ${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
13160
+
13161
+ // ${writeDoc}
13162
+ export default async function (req: Tina4Request, res: Tina4Response) {
13163
+ // tina4:edit fill the create handler (see AI-FILL fill-spec below)
13164
+ ${aiFill(`create_${singular}`, {
13165
+ intent: `validate the body and persist a new ${singular}`,
13166
+ given: "req.body -> the posted fields",
13167
+ use: "new Model(req.body).save() then item.toObject() (import your model)",
13168
+ ret: "res.json({ data: item }, 201)",
13169
+ ground: `tina4_context("create ORM record and return 201", "nodejs") \xB7 skill tina4-developer-nodejs`,
13170
+ raise: `create ${singular} not implemented`
13171
+ })}}
13172
+ `
13173
+ );
13174
+ }
13175
+ if (model) {
13176
+ writeFileSafe(
13177
+ join15(idDir, "get.ts"),
13178
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13179
+ ${modelImportId}
13180
+ export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
13181
+
13182
+ export default async function (req: Tina4Request, res: Tina4Response) {
13183
+ const { id } = req.params;
13184
+ const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
13185
+ if (!item) {
13186
+ res.json({ error: "Not found" }, 404);
13187
+ return;
13188
+ }
13189
+ res.json({ data: item.toObject() });
13190
+ }
13191
+ `
13192
+ );
13193
+ } else {
13194
+ writeFileSafe(
13195
+ join15(idDir, "get.ts"),
13196
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13197
+
13198
+ export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
13199
+
13200
+ export default async function (req: Tina4Request, res: Tina4Response) {
13201
+ ${aiFill(`get_${singular}`, {
13202
+ intent: `fetch one ${singular} by id`,
13203
+ given: "req.params.id -> the record id",
13204
+ use: `Model.selectOne("SELECT \u2026 WHERE id = ?", [req.params.id])`,
13205
+ ret: "res.json({ data: item }) or res.json({ error: 'Not found' }, 404)",
13206
+ ground: `tina4_context("find ORM record by id", "nodejs") \xB7 skill tina4-developer-nodejs`,
13207
+ raise: `get ${singular} not implemented`
13208
+ })}}
13209
+ `
13210
+ );
13211
+ }
13212
+ if (model) {
13213
+ writeFileSafe(
13214
+ join15(idDir, "put.ts"),
13215
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13216
+ ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
13217
+
13218
+ // ${writeDoc}
13219
+ export default async function (req: Tina4Request, res: Tina4Response) {
13220
+ const { id } = req.params;
13221
+ const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
13222
+ if (!item) {
13223
+ res.json({ error: "Not found" }, 404);
13224
+ return;
13225
+ }
13226
+ // tina4:edit guard which fields may be updated and who may update this row
13227
+ ${extend(
13228
+ "guard which fields / who may update",
13229
+ `e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`
13230
+ )} Object.assign(item, req.body as Record<string, unknown>);
13231
+ // save() returns false on failure rather than throwing - check it, or a failed
13232
+ // write is reported to the client as a 200 carrying unsaved data.
13233
+ if ((await item.save()) === false) {
13234
+ res.json({ error: "Could not update ${singular}" }, 400);
13235
+ return;
13236
+ }
13237
+ res.json({ data: item.toObject() });
13238
+ }
13239
+ `
13240
+ );
13241
+ } else {
13242
+ writeFileSafe(
13243
+ join15(idDir, "put.ts"),
13244
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13245
+
13246
+ ${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
13247
+
13248
+ // ${writeDoc}
13249
+ export default async function (req: Tina4Request, res: Tina4Response) {
13250
+ // tina4:edit fill the update handler (see AI-FILL fill-spec below)
13251
+ ${aiFill(`update_${singular}`, {
13252
+ intent: `load, mutate and save an existing ${singular}`,
13253
+ given: "req.params.id -> id; req.body -> changed fields",
13254
+ use: "Model.selectOne(\u2026) then Object.assign(item, req.body) then item.save()",
13255
+ ret: "res.json({ data: item }) or 404",
13256
+ ground: `tina4_context("update ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
13257
+ raise: `update ${singular} not implemented`
13258
+ })}}
13259
+ `
13260
+ );
13261
+ }
13262
+ if (model) {
13263
+ writeFileSafe(
13264
+ join15(idDir, "delete.ts"),
13265
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13266
+ ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
13267
+
13268
+ // ${writeDoc}
13269
+ export default async function (req: Tina4Request, res: Tina4Response) {
13270
+ const { id } = req.params;
13271
+ const item = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
13272
+ if (!item) {
13273
+ res.json({ error: "Not found" }, 404);
13274
+ return;
13275
+ }
13276
+ await item.delete();
13277
+ res.json({ message: "deleted", id });
13278
+ }
13279
+ `
13280
+ );
13281
+ } else {
13282
+ writeFileSafe(
13283
+ join15(idDir, "delete.ts"),
13284
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13285
+
13286
+ ${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
13287
+
13288
+ // ${writeDoc}
13289
+ export default async function (req: Tina4Request, res: Tina4Response) {
13290
+ ${aiFill(`delete_${singular}`, {
13291
+ intent: `delete a ${singular} by id`,
13292
+ given: "req.params.id -> id",
13293
+ use: "Model.selectOne(\u2026) then item.delete()",
13294
+ ret: "res.json({ message: 'deleted', id }) or 404",
13295
+ ground: `tina4_context("delete ORM record", "nodejs") \xB7 skill tina4-developer-nodejs`,
13296
+ raise: `delete ${singular} not implemented`
13297
+ })}}
13298
+ `
13299
+ );
13300
+ }
13301
+ if (emitTest) {
13302
+ if (model) {
13303
+ generateTest(routePath, { model, "secure-writes": true, public: isPublic });
13304
+ } else {
13305
+ emitRouteStubTest(routePath);
13306
+ }
13307
+ }
13308
+ }
13309
+ function generateCrud(name, flags) {
13310
+ const table2 = toTableName(name);
13311
+ const routeName = toPlural(table2);
13312
+ const isPublic = Boolean(flags.public);
13313
+ if (!__resolution.jsonMode) console.log(`
13314
+ Generating CRUD for ${name}...
13315
+ `);
13316
+ generateModel(name, flags, false);
13317
+ generateRoute(routeName, { ...flags, model: name }, false);
13318
+ generateForm(name, flags);
13319
+ generateView(name, flags);
13320
+ generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
13321
+ if (!__resolution.jsonMode) {
13322
+ console.log(`
13323
+ CRUD generation complete for ${name}.`);
13324
+ console.log(" Run: tina4nodejs migrate");
13325
+ console.log(" Visit: /swagger to see the API docs");
13326
+ }
13327
+ }
13328
+ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest = true) {
13329
+ const ts = timestamp();
13330
+ const dir = resolve6("migrations");
13331
+ ensureDir(dir);
13332
+ let table2;
13333
+ if (tableOverride) {
13334
+ table2 = tableOverride;
13335
+ } else {
13336
+ const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
13337
+ table2 = toTableName(raw);
13338
+ }
13339
+ if (__resolution.target === "migration") {
13340
+ setResolutionField("table_name", table2);
13341
+ }
13342
+ const fields = fieldsOverride || parseFields(flags.fields || "");
13343
+ const isCreate = name.startsWith("create_") || fieldsOverride !== void 0;
13344
+ const fileName = `${ts}_${name}.sql`;
13345
+ const path8 = join15(dir, fileName);
13346
+ setResolutionField("migration_path", `migrations/${fileName}`);
13347
+ if (__resolution.target === "migration") {
13348
+ setResolutionField("file_path", `migrations/${fileName}`);
13349
+ pushTestPath(`tests/${table2}_migration.test.ts`);
13350
+ }
13351
+ let upSql;
13352
+ let downSql;
13353
+ if (isCreate) {
13354
+ const colLines = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
13355
+ for (const [fname, ftype] of fields) {
13356
+ const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
13357
+ const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
13358
+ colLines.push(` ${fname} ${info.sql}${defaultClause}`);
13359
+ }
13360
+ colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
13361
+ upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
13362
+ -- tina4:edit add columns beyond id + created_at
13363
+ ${colLines.join(",\n")}
13364
+ );`;
13365
+ downSql = `-- tina4:edit mirror the CREATE's added columns in the rollback
13366
+ DROP TABLE IF EXISTS ${table2};`;
13367
+ } else {
13368
+ upSql = `-- tina4:edit write your UP migration SQL here
13369
+ -- Example: ALTER TABLE ${table2} ADD COLUMN new_col TEXT DEFAULT '';`;
13370
+ downSql = `-- tina4:edit write your DOWN rollback SQL here
13371
+ -- Example: ALTER TABLE ${table2} DROP COLUMN new_col;`;
13372
+ }
13373
+ const now = isoNow();
13374
+ const content = `-- Migration: ${name}
13375
+ -- Created: ${now}
13376
+
13377
+ -- UP
13378
+ ${upSql}
13379
+
13380
+ -- DOWN
13381
+ ${downSql}
13382
+ `;
13383
+ writeFileSafe(path8, content);
13384
+ const downPath = join15(dir, `${ts}_${name}.down.sql`);
13385
+ const downContent = `-- Rollback: ${name}
13386
+ -- Created: ${now}
13387
+
13388
+ ${downSql}
13389
+ `;
13390
+ writeFileSafe(downPath, downContent);
13391
+ if (emitTest && isCreate) emitMigrationTest(name, table2);
13392
+ }
13393
+ function generateMiddleware(name, _flags) {
13394
+ const snake = toSnake(name);
13395
+ const dir = resolve6("src/middleware");
13396
+ ensureDir(dir);
13397
+ const path8 = join15(dir, `${snake}.ts`);
13398
+ if (__resolution.target === "middleware") {
13399
+ setResolutionField("class_name", name);
13400
+ setResolutionField("file_path", `src/middleware/${snake}.ts`);
13401
+ pushTestPath(`tests/${snake}.test.ts`);
13402
+ }
13403
+ const content = `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13404
+
13405
+ /**
13406
+ * ${name} middleware \u2014 runs before and after route handlers.
13407
+ *
13408
+ * Usage:
13409
+ * import { before${name}, after${name} } from "../middleware/${snake}.js";
13410
+ */
13411
+
13412
+ export async function before${name}(
13413
+ req: Tina4Request,
13414
+ res: Tina4Response,
13415
+ next: () => Promise<void>,
13416
+ ): Promise<void> {
13417
+ // tina4:edit replace the Authorization check with the real pre-request rule
13418
+ const auth = req.headers["authorization"];
13419
+ if (!auth) {
13420
+ res.json({ error: "Unauthorized" }, 401);
13421
+ return;
13422
+ }
13423
+ await next();
13424
+ }
13425
+
13426
+ export async function after${name}(
13427
+ req: Tina4Request,
13428
+ res: Tina4Response,
13429
+ next: () => Promise<void>,
13430
+ ): Promise<void> {
13431
+ // tina4:edit add post-processing (logging, header injection, telemetry)
13432
+ await next();
13433
+ }
13434
+ `;
13435
+ writeFileSafe(path8, content);
13436
+ emitMiddlewareTest(name, snake);
13437
+ }
13438
+ function generateTest(name, flags) {
13439
+ const snake = toSnake(name);
13440
+ const singular = snake.endsWith("s") ? snake.slice(0, -1) : snake;
13441
+ const model = flags.model;
13442
+ const dir = resolve6("tests");
13443
+ ensureDir(dir);
13444
+ const path8 = join15(dir, `${snake}.test.ts`);
13445
+ if (model && flags["secure-writes"]) {
13446
+ const isPublic = Boolean(flags.public);
13447
+ const posture = isPublic ? "open (--public)" : "gated";
13448
+ const writeCase = isPublic ? ` // --public opened the write: an anonymous POST creates -> 201.
13449
+ assert("anonymous POST is public -> 201",
13450
+ (await client.post("/api/${snake}", { json: { name: "test" } })).status === 201);` : ` // Secure by default: a tokenless POST is rejected with 401.
13451
+ assert("anonymous POST is gated -> 401",
13452
+ (await client.post("/api/${snake}", { json: { name: "test" } })).status === 401);
13453
+ // A valid Bearer token passes the gate and creates -> 201.
13454
+ const token = getToken({ userId: 1 });
13455
+ assert("authenticated POST creates -> 201",
13456
+ (await client.post("/api/${snake}", { json: { name: "test" }, headers: { authorization: \`Bearer \${token}\` } })).status === 201);`;
13457
+ const content2 = `/**
13458
+ * ${name} CRUD \u2014 reads public, writes ${posture} (secure by default).
13459
+ *
13460
+ * Real end-to-end via TestClient: no mocks \u2014 real Router, real auth gate, real
13461
+ * JWT, real SQLite DB + table. Run with: npx tsx tests/${snake}.test.ts
13462
+ */
13463
+ import { dirname, resolve } from "node:path";
13464
+ import { fileURLToPath } from "node:url";
13465
+ import { Router, TestClient, getToken, discoverRoutes } from "tina4-nodejs";
13466
+ import { initDatabase } from "tina4-nodejs/orm";
13467
+ import ${model} from "../src/models/${model}.js";
13468
+
13469
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
13470
+ const here = dirname(fileURLToPath(import.meta.url));
13471
+
13472
+ let pass = 0;
13473
+ let fail = 0;
13474
+ function assert(label: string, ok: boolean): void {
13475
+ if (ok) { pass++; console.log(\` PASS \${label}\`); }
13476
+ else { fail++; console.log(\` FAIL \${label}\`); }
13477
+ }
13478
+
13479
+ await initDatabase({ url: "sqlite:///data/test_${snake}.db" });
13480
+ await ${model}.createTable();
13481
+
13482
+ const router = new Router();
13483
+ for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
13484
+ const client = new TestClient(router);
13485
+
13486
+ // Reads are public.
13487
+ assert("GET list is public -> 200", (await client.get("/api/${snake}")).status === 200);
13488
+ ${writeCase}
13489
+
13490
+ console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
13491
+ process.exit(fail > 0 ? 1 : 0);
13492
+ `;
13493
+ writeFileSafe(path8, content2);
13494
+ return;
13495
+ }
13496
+ let content;
13497
+ if (model) {
13498
+ content = `import { tests, assertTrue } from "tina4-nodejs";
13499
+
13500
+ /**
13501
+ * Tests for ${name} CRUD operations.
13502
+ */
13503
+
13504
+ const list${model}s = tests(
13505
+ assertTrue([]),
13506
+ )(function list${model}s() {
13507
+ // tina4:edit assert against a real GET /api/${toSnake(name)} response (rows, count)
13508
+ return true;
13509
+ });
13510
+
13511
+ const get${model} = tests(
13512
+ assertTrue([]),
13513
+ )(function get${model}() {
13514
+ // tina4:edit assert against GET /api/${toSnake(name)}/{id} for one seeded row
13515
+ return true;
13516
+ });
13517
+
13518
+ const create${model} = tests(
13519
+ assertTrue([]),
13520
+ )(function create${model}() {
13521
+ // tina4:edit POST a valid + an invalid body, assert 201 vs 400
13522
+ return true;
13523
+ });
13524
+
13525
+ const update${model} = tests(
13526
+ assertTrue([]),
13527
+ )(function update${model}() {
13528
+ // tina4:edit PUT changed fields, assert the row was persisted
13529
+ return true;
13530
+ });
13531
+
13532
+ const delete${model} = tests(
13533
+ assertTrue([]),
13534
+ )(function delete${model}() {
13535
+ // tina4:edit DELETE the id, assert 200 then GET returns 404
13536
+ return true;
13537
+ });
13538
+
13539
+ void [list${model}s, get${model}, create${model}, update${model}, delete${model}];
13540
+ `;
13541
+ } else {
13542
+ const titleName = name.charAt(0).toUpperCase() + name.slice(1);
13543
+ content = `import { tests, assertTrue } from "tina4-nodejs";
13544
+
13545
+ /**
13546
+ * Tests for ${name}.
13547
+ */
13548
+
13549
+ const test${titleName} = tests(
13550
+ assertTrue([]),
13551
+ )(function test${titleName}() {
13552
+ // tina4:edit assert against the real behaviour under test (no mocks)
13553
+ return true;
13554
+ });
13555
+
13556
+ void test${titleName};
13557
+ `;
13558
+ }
13559
+ writeFileSafe(path8, content);
13560
+ }
13561
+ function generateForm(name, flags) {
13562
+ const fields = fieldsOrDefault(flags.fields || "");
13563
+ const table2 = toTableName(name);
13564
+ const routeName = toPlural(table2);
13565
+ const inputTypes = {
13566
+ string: "text",
13567
+ str: "text",
13568
+ text: "textarea",
13569
+ int: "number",
13570
+ integer: "number",
13571
+ float: "number",
13572
+ numeric: "number",
13573
+ decimal: "number",
13574
+ bool: "checkbox",
13575
+ boolean: "checkbox",
13576
+ datetime: "datetime-local",
13577
+ blob: "file"
13578
+ };
13579
+ const dir = resolve6("src/templates/forms");
13580
+ ensureDir(dir);
13581
+ const path8 = join15(dir, `${table2}.twig`);
13582
+ let fieldHtml = "";
13583
+ for (const [fname, ftype] of fields) {
13584
+ const itype = inputTypes[ftype] || "text";
13585
+ const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
13586
+ const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
13587
+ if (itype === "textarea") {
13588
+ fieldHtml += ` <div class="form-group mb-3">
13589
+ <label for="${fname}">${label}</label>
13590
+ <textarea id="${fname}" name="${fname}" class="form-control" rows="4" placeholder="${label}">{{ item.${fname} }}</textarea>
13591
+ </div>
13592
+ `;
13593
+ } else if (itype === "checkbox") {
13594
+ fieldHtml += ` <div class="form-group mb-3">
13595
+ <label>
13596
+ <input type="checkbox" id="${fname}" name="${fname}" value="1" {% if item.${fname} %}checked{% endif %}>
13597
+ ${label}
13598
+ </label>
13599
+ </div>
13600
+ `;
13601
+ } else {
13602
+ fieldHtml += ` <div class="form-group mb-3">
13603
+ <label for="${fname}">${label}</label>
13604
+ <input type="${itype}" id="${fname}" name="${fname}" class="form-control"${step} value="{{ item.${fname} }}" placeholder="${label}">
13605
+ </div>
13606
+ `;
13607
+ }
13608
+ }
13609
+ const content = `{% extends "base.twig" %}
13610
+ {% block title %}${name} {% if item.id %}Edit{% else %}Create{% endif %}{% endblock %}
13611
+ {% block content %}
13612
+ <div class="container mt-4">
13613
+ <h1>{% if item.id %}Edit ${name}{% else %}Create ${name}{% endif %}</h1>
13614
+ {# tina4:edit restyle the form beyond the scaffolded defaults #}
13615
+ <form method="post" action="/api/${routeName}{% if item.id %}/{{ item.id }}{% endif %}">
13616
+ {{ form_token() }}
13617
+ ` + fieldHtml + ` <button type="submit" class="btn btn-primary">
13618
+ {% if item.id %}Update{% else %}Create{% endif %}
13619
+ </button>
13620
+ <a href="/api/${routeName}" class="btn btn-secondary">Cancel</a>
13621
+ </form>
13622
+ </div>
13623
+ {% endblock %}
13624
+ `;
13625
+ writeFileSafe(path8, content);
13626
+ }
13627
+ function generateView(name, flags) {
13628
+ const fields = fieldsOrDefault(flags.fields || "");
13629
+ const table2 = toTableName(name);
13630
+ const routeName = toPlural(table2);
13631
+ const cols = fields.map(([f]) => f);
13632
+ const dir = resolve6("src/templates/pages");
13633
+ ensureDir(dir);
13634
+ const listPath = join15(dir, `${routeName}.twig`);
13635
+ const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
13636
+ const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
13637
+ const listContent = `{% extends "base.twig" %}
13638
+ {% block title %}${name}s{% endblock %}
13639
+ {% block content %}
13640
+ <div class="container mt-4">
13641
+ {# tina4:edit add sort / filter / pagination controls to the list #}
13642
+ <div class="d-flex justify-content-between align-items-center mb-3">
13643
+ <h1>${name}s</h1>
13644
+ <a href="/${routeName}/create" class="btn btn-primary">Add ${name}</a>
13645
+ </div>
13646
+ <table class="table">
13647
+ <thead>
13648
+ <tr>
13649
+ <th>ID</th>
13650
+ ${th}
13651
+ <th>Actions</th>
13652
+ </tr>
13653
+ </thead>
13654
+ <tbody>
13655
+ {% for item in items %}
13656
+ <tr>
13657
+ <td>{{ item.id }}</td>
13658
+ ${td}
13659
+ <td>
13660
+ <a href="/${routeName}/{{ item.id }}" class="btn btn-sm btn-primary">View</a>
13661
+ <a href="/${routeName}/{{ item.id }}/edit" class="btn btn-sm btn-secondary">Edit</a>
13662
+ </td>
13663
+ </tr>
13664
+ {% endfor %}
13665
+ </tbody>
13666
+ </table>
13667
+ </div>
13668
+ {% endblock %}
13669
+ `;
13670
+ writeFileSafe(listPath, listContent);
13671
+ const detailPath = join15(dir, `${table2}.twig`);
13672
+ 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");
13673
+ const detailContent = `{% extends "base.twig" %}
13674
+ {% block title %}${name} Detail{% endblock %}
13675
+ {% block content %}
13676
+ <div class="container mt-4">
13677
+ {# tina4:edit extend the detail view with related records or actions #}
13678
+ <div class="d-flex justify-content-between align-items-center mb-3">
13679
+ <h1>${name} #{{ item.id }}</h1>
13680
+ <div>
13681
+ <a href="/${routeName}/{{ item.id }}/edit" class="btn btn-secondary">Edit</a>
13682
+ <a href="/${routeName}" class="btn btn-outline-secondary">Back</a>
13683
+ </div>
13684
+ </div>
13685
+ ${detailFields}
13686
+ </div>
13687
+ {% endblock %}
13688
+ `;
13689
+ writeFileSafe(detailPath, detailContent);
13690
+ }
13691
+ function generateAuth(_flags) {
13692
+ if (!__resolution.jsonMode) console.log("\n Generating authentication scaffolding...\n");
13693
+ generateModel("User", { fields: "email:string,password:string,role:string" }, false);
13694
+ const registerDir = resolve6("src/routes/api/auth/register");
13695
+ const loginDir = resolve6("src/routes/api/auth/login");
13696
+ const meDir = resolve6("src/routes/api/auth/me");
13697
+ ensureDir(registerDir);
13698
+ ensureDir(loginDir);
13699
+ ensureDir(meDir);
13700
+ writeFileSafe(
13701
+ join15(registerDir, "post.ts"),
13702
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13703
+ import { hashPassword } from "tina4-nodejs";
13704
+ import User from "../../../../models/User.js";
13705
+
13706
+ // Public: registration mints an account for a user who has no token yet.
13707
+ export const secure = false;
13708
+
13709
+ export const meta = { summary: "Register a new user", tags: ["auth"] };
13710
+
13711
+ export default async function (req: Tina4Request, res: Tina4Response) {
13712
+ // tina4:edit add password-strength / email-format / captcha rules before mint
13713
+ const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
13714
+
13715
+ if (!email || !password) {
13716
+ res.json({ error: "Email and password required" }, 400);
13717
+ return;
13718
+ }
13719
+
13720
+ const existing = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
13721
+ if (existing) {
13722
+ res.json({ error: "Email already registered" }, 409);
13723
+ return;
13724
+ }
13725
+
13726
+ const user = new User({ email, password: hashPassword(password), role: "user" });
13727
+ await user.save();
13728
+ res.json({ message: "Registered", id: user.toObject().id }, 201);
13729
+ }
13730
+ `
13731
+ );
13732
+ writeFileSafe(
13733
+ join15(loginDir, "post.ts"),
13734
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13735
+ import { checkPassword, getToken } from "tina4-nodejs";
13736
+ import User from "../../../../models/User.js";
13737
+
13738
+ // Public: login authenticates by password and mints the token.
13739
+ export const secure = false;
13740
+
13741
+ export const meta = { summary: "Login and receive JWT token", tags: ["auth"] };
13742
+
13743
+ export default async function (req: Tina4Request, res: Tina4Response) {
13744
+ // tina4:edit add rate-limit / lock-after-N-failures / 2FA before password check
13745
+ const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
13746
+
13747
+ if (!email || !password) {
13748
+ res.json({ error: "Email and password required" }, 400);
13749
+ return;
13750
+ }
13751
+
13752
+ const user = await User.selectOne("SELECT * FROM users WHERE email = ?", [email]);
13753
+ if (!user || !checkPassword(password, user.toObject().password as string)) {
13754
+ res.json({ error: "Invalid credentials" }, 401);
13755
+ return;
13756
+ }
13757
+
13758
+ const data = user.toObject();
13759
+ // tina4:edit set token TTL (getToken(payload, secret, expiresInMinutes)) and add scopes if needed
13760
+ const token = getToken({ userId: data.id, email: data.email, role: data.role });
13761
+ res.json({ token });
13762
+ }
13763
+ `
13764
+ );
13765
+ writeFileSafe(
13766
+ join15(meDir, "get.ts"),
13767
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
13768
+ import { authenticateRequest } from "tina4-nodejs";
13769
+
13770
+ export const meta = { summary: "Get current authenticated user", tags: ["auth"] };
13771
+
13772
+ export default async function (req: Tina4Request, res: Tina4Response) {
13773
+ const payload = authenticateRequest(req.headers as Record<string, string | string[] | undefined>);
13774
+ if (!payload) {
13775
+ res.json({ error: "Unauthorized" }, 401);
13776
+ return;
13777
+ }
13778
+ res.json({ user: payload });
13779
+ }
13780
+ `
13781
+ );
13782
+ const formsDir = resolve6("src/templates/forms");
13783
+ ensureDir(formsDir);
13784
+ writeFileSafe(
13785
+ join15(formsDir, "login.twig"),
13786
+ `{% extends "base.twig" %}
13787
+ {% block title %}Login{% endblock %}
13788
+ {% block content %}
13789
+ <div class="container mt-4" style="max-width:400px">
13790
+ <h1>Login</h1>
13791
+ <form method="post" action="/api/auth/login">
13792
+ {{ form_token() }}
13793
+ <div class="form-group mb-3">
13794
+ <label for="email">Email</label>
13795
+ <input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
13796
+ </div>
13797
+ <div class="form-group mb-3">
13798
+ <label for="password">Password</label>
13799
+ <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
13800
+ </div>
13801
+ <button type="submit" class="btn btn-primary w-100">Login</button>
13802
+ <p class="mt-3 text-center"><a href="/register">Create an account</a></p>
13803
+ </form>
13804
+ </div>
13805
+ {% endblock %}
13806
+ `
13807
+ );
13808
+ writeFileSafe(
13809
+ join15(formsDir, "register.twig"),
13810
+ `{% extends "base.twig" %}
13811
+ {% block title %}Register{% endblock %}
13812
+ {% block content %}
13813
+ <div class="container mt-4" style="max-width:400px">
13814
+ <h1>Register</h1>
13815
+ <form method="post" action="/api/auth/register">
13816
+ {{ form_token() }}
13817
+ <div class="form-group mb-3">
13818
+ <label for="email">Email</label>
13819
+ <input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
13820
+ </div>
13821
+ <div class="form-group mb-3">
13822
+ <label for="password">Password</label>
13823
+ <input type="password" id="password" name="password" class="form-control" placeholder="Password" minlength="8" required>
13824
+ </div>
13825
+ <button type="submit" class="btn btn-primary w-100">Register</button>
13826
+ <p class="mt-3 text-center"><a href="/login">Already have an account?</a></p>
13827
+ </form>
13828
+ </div>
13829
+ {% endblock %}
13830
+ `
13831
+ );
13832
+ emitAuthTest();
13833
+ if (!__resolution.jsonMode) {
13834
+ console.log("\n Authentication scaffolding complete.");
13835
+ console.log(" Run: tina4nodejs migrate");
13836
+ console.log(" POST /api/auth/register \u2014 create account (public)");
13837
+ console.log(" POST /api/auth/login \u2014 get JWT token (public)");
13838
+ console.log(" GET /api/auth/me \u2014 get profile (requires token)");
13839
+ }
13840
+ }
13841
+ function generateService(name, flags) {
13842
+ const snake = toSnake(name);
13843
+ const camel = toCamel(toPascal(name)) || snake;
13844
+ const cron = flags.cron;
13845
+ const dir = resolve6("src/services");
13846
+ ensureDir(dir);
13847
+ const path8 = join15(dir, `${snake}.ts`);
13848
+ let scheduleField;
13849
+ let note;
13850
+ if (cron && cron !== true) {
13851
+ scheduleField = ` timing: ${JSON.stringify(String(cron))},`;
13852
+ note = `cron '${cron}'`;
13853
+ } else {
13854
+ const seconds = parseEvery(flags.every);
13855
+ scheduleField = ` interval: ${seconds},`;
13856
+ note = `every ${seconds}s`;
13857
+ }
13858
+ const body = aiFill(`${camel}Task`, {
13859
+ intent: "do the scheduled work for this service",
13860
+ given: "context -> ServiceContext (.name, .running, .lastRun)",
13861
+ use: "your ORM / Api / Messenger code (re-run on schedule)",
13862
+ ground: `tina4_context("background service scheduled task", "nodejs") \xB7 skill tina4-developer-nodejs`,
13863
+ raise: `service ${snake} not implemented`
13864
+ });
13865
+ const content = `import type { ServiceContext } from "tina4-nodejs";
13866
+
13867
+ /**
13868
+ * ${name} background service \u2014 runs ${note} via ServiceRunner.
13869
+ *
13870
+ * Wire a runner once (e.g. in app.ts) to actually run it \u2014 \`tina4nodejs serve\`
13871
+ * does NOT auto-start services:
13872
+ *
13873
+ * import { ServiceRunner } from "tina4-nodejs";
13874
+ * await ServiceRunner.discover("src/services"); // registers this default export
13875
+ * ServiceRunner.start();
13876
+ */
13877
+
13878
+ export async function ${camel}Task(context: ServiceContext): Promise<void> {
13879
+ // tina4:edit replace the AI-FILL stub below with the scheduled work
13880
+ ${body}}
13881
+
13882
+ // Discovered by ServiceRunner.discover("src/services") \u2014 it reads name/handler
13883
+ // (+ interval or timing) off this default export.
13884
+ export default {
13885
+ name: "${snake}",
13886
+ handler: ${camel}Task,
13887
+ ${scheduleField}
13888
+ };
13889
+ `;
13890
+ writeFileSafe(path8, content);
13891
+ emitServiceTest(name, snake, camel);
13892
+ }
13893
+ function generateQueue(name, _flags) {
13894
+ const topic = name.replace(/^\//, "");
13895
+ const slug = toSnake(topic.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
13896
+ const pascal = toPascal(topic) || "Topic";
13897
+ const dir = resolve6("src/services");
13898
+ ensureDir(dir);
13899
+ const path8 = join15(dir, `${slug}_consumer.ts`);
13900
+ const body = aiFill(`handle${pascal}`, {
13901
+ intent: `process ONE ${topic} job payload`,
13902
+ given: "payload -> the produced job data (job.payload)",
13903
+ use: "your ORM / Messenger code; return to ack (job.complete), throw to nack (job.fail)",
13904
+ ground: `tina4_context("process a queue job", "nodejs") \xB7 skill tina4-developer-nodejs`,
13905
+ raise: `queue ${topic} handler not implemented`
13906
+ });
13907
+ const content = `import { Queue } from "tina4-nodejs";
13908
+ import type { ServiceContext } from "tina4-nodejs";
13909
+
13910
+ /**
13911
+ * ${topic} queue \u2014 producer + consumer worker.
13912
+ *
13913
+ * Produce from anywhere: publish${pascal}({ ... })
13914
+ * The consumer is a long-running worker wired as a ServiceRunner daemon:
13915
+ * await ServiceRunner.discover("src/services"); ServiceRunner.start();
13916
+ */
13917
+
13918
+ /** Enqueue a ${topic} job for the worker below to process. Returns the job id. */
13919
+ export function publish${pascal}(payload: Record<string, unknown>): string {
13920
+ return new Queue({ topic: "${topic}" }).produce("${topic}", payload);
13921
+ }
13922
+
13923
+ /** Process ONE ${topic} job payload. */
13924
+ export async function handle${pascal}(payload: unknown): Promise<void> {
13925
+ // tina4:edit implement the per-job handler; return to ack, throw to nack
13926
+ ${body}}
13927
+
13928
+ /** Long-running ${topic} worker \u2014 consume() yields jobs; ack/nack each. */
13929
+ export async function consume${pascal}(_context?: ServiceContext): Promise<void> {
13930
+ const queue = new Queue({ topic: "${topic}" });
13931
+ for await (const job of queue.consume("${topic}")) {
13932
+ const one = Array.isArray(job) ? job[0] : job;
13933
+ try {
13934
+ await handle${pascal}(one.payload);
13935
+ one.complete(); // ack \u2014 remove from the queue
13936
+ } catch (err) {
13937
+ one.fail(String(err)); // nack \u2014 retry / dead-letter
13938
+ }
13939
+ }
13940
+ }
13941
+
13942
+ // Discovered by ServiceRunner.discover("src/services"); daemon:true because
13943
+ // consume${pascal} owns its own loop. The topic + per-job handle keys let
13944
+ // \`tina4nodejs queue work ${topic}\` drive this consumer directly (own the poll
13945
+ // loop / bounded --once drain) without wiring a ServiceRunner.
13946
+ export default {
13947
+ name: "${topic}-consumer",
13948
+ topic: "${topic}",
13949
+ handler: consume${pascal},
13950
+ handle: handle${pascal},
13951
+ daemon: true,
13952
+ };
13953
+ `;
13954
+ writeFileSafe(path8, content);
13955
+ emitQueueTest(topic, slug, pascal);
13956
+ }
13957
+ function generateValidator(name, _flags) {
13958
+ const dir = resolve6("src/validators");
13959
+ ensureDir(dir);
13960
+ const path8 = join15(dir, `${toSnake(name)}.ts`);
13961
+ const rules = extend(
13962
+ "add / adjust the validation rules for this payload",
13963
+ `e.g. .email("email").minLength("name", 2).integer("age"); ground: tina4_context("validate request body with Validator", "nodejs")`
13964
+ );
13965
+ const content = `import { Validator } from "tina4-nodejs";
13966
+
13967
+ /**
13968
+ * Validate a ${name} payload. Returns a Validator (chainable rules).
13969
+ *
13970
+ * Usage in a route:
13971
+ * const v = validate${toPascal(name)}(req.body as Record<string, unknown>);
13972
+ * if (!v.isValid()) return res.json({ error: v.errors()[0]?.message }, 400);
13973
+ */
13974
+ export function validate${toPascal(name)}(data: Record<string, unknown>): Validator {
13975
+ const validator = new Validator(data);
13976
+ // tina4:edit add rules for this payload (.email/.minLength/.integer/.inList/.pattern)
13977
+ ${rules} validator.required("name"); // starter rule (matches the model's default field)
13978
+ return validator;
13979
+ }
13980
+ `;
13981
+ writeFileSafe(path8, content);
13982
+ emitValidatorTest(name, toSnake(name), toPascal(name));
13983
+ }
13984
+ function generateSeeder(name, _flags) {
13985
+ const table2 = toTableName(name);
13986
+ const dir = resolve6("src/seeds");
13987
+ ensureDir(dir);
13988
+ const path8 = join15(dir, `${table2}_seeder.ts`);
13989
+ const overrides = extend(
13990
+ "override fields that need a specific shape (seedOrm auto-fills the rest)",
13991
+ `e.g. return { email: (f) => f.email(), status: "active" }; ground: tina4_context("seed ORM model with FakeData", "nodejs")`
13992
+ );
13993
+ const content = `import { pathToFileURL } from "node:url";
13994
+ import { FakeData, seedOrm, initDatabase } from "tina4-nodejs/orm";
13995
+ import ${name} from "../models/${name}.js";
13996
+
13997
+ /**
13998
+ * Seeder for ${name} \u2014 run with: tina4nodejs seed
13999
+ *
14000
+ * seedOrm auto-fills every field by type/name; override the ones that need a
14001
+ * specific shape below. Each callable receives a FakeData instance.
14002
+ */
14003
+ export function fieldOverrides(fake: FakeData): Record<string, unknown> {
14004
+ // tina4:edit override any fields that need a specific shape (seedOrm auto-fills the rest)
14005
+ ${overrides} void fake; // available for overrides above
14006
+ return {};
14007
+ }
14008
+
14009
+ /** Seed rows. Invoked when this file is run directly by \`tina4nodejs seed\`. */
14010
+ export async function run(): Promise<void> {
14011
+ await initDatabase({ url: process.env.TINA4_DATABASE_URL ?? "sqlite:///data/app.db" });
14012
+ const summary = await seedOrm(${name} as never, 20, fieldOverrides(new FakeData()));
14013
+ console.log(\`Seeded \${summary.seeded} ${name} row(s), \${summary.failed} failed\`);
14014
+ }
14015
+
14016
+ // Only seed when executed as a script (\`tina4nodejs seed\` runs it via tsx) \u2014
14017
+ // importing this module (e.g. in a test) must NOT trigger seeding.
14018
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
14019
+ await run();
14020
+ }
14021
+ `;
14022
+ writeFileSafe(path8, content);
14023
+ emitSeederTest(name, table2);
14024
+ }
14025
+ function generateWebsocket(name, _flags) {
14026
+ const raw = name.trim();
14027
+ const wsPath = raw.startsWith("/") ? raw : "/ws/" + raw.replace(/^\/+/, "");
14028
+ let slug = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
14029
+ const base = slug.startsWith("ws_") ? slug.slice(3) : slug;
14030
+ const handlerName = `${toCamel(toPascal(base))}Ws`;
14031
+ const dir = resolve6("src/routes");
14032
+ ensureDir(dir);
14033
+ const path8 = join15(dir, `ws_${base}.ts`);
14034
+ const body = aiFill(handlerName, {
14035
+ intent: `handle an inbound "message" frame on ${wsPath}`,
14036
+ given: "data -> the message payload (string); connection -> WebSocketConnection",
14037
+ use: "connection.broadcast(data) or connection.sendJson({ ... })",
14038
+ ground: `tina4_context("websocket broadcast message", "nodejs") \xB7 skill tina4-developer-nodejs`,
14039
+ raise: `websocket ${wsPath} not implemented`
14040
+ });
14041
+ const content = `import { websocket } from "tina4-nodejs";
14042
+ import type { WebSocketConnection } from "tina4-nodejs";
14043
+
14044
+ /**
14045
+ * ${wsPath} WebSocket route.
14046
+ *
14047
+ * Registered on import by websocket(). Node has NO file-based WS
14048
+ * auto-discovery, so IMPORT this module once from app.ts to activate it (add
14049
+ * \`.secure()\` to require a JWT on the upgrade):
14050
+ *
14051
+ * import "./src/routes/ws_${base}.js";
14052
+ *
14053
+ * The server invokes the handler as (connection, event, data) for each event:
14054
+ * "open" (connect), "message" (inbound frame), "close" (disconnect).
14055
+ */
14056
+ export async function ${handlerName}(
14057
+ connection: WebSocketConnection,
14058
+ event: "open" | "message" | "close",
14059
+ data: string,
14060
+ ): Promise<void> {
14061
+ if (event === "open") {
14062
+ // tina4:edit customize the welcome frame (or drop it)
14063
+ connection.sendJson({ type: "welcome" });
14064
+ return;
14065
+ }
14066
+ if (event === "close") {
14067
+ return;
14068
+ }
14069
+ // event === "message"
14070
+ // tina4:edit handle the inbound "message" frame (broadcast, echo, route, etc.)
14071
+ ${body}}
14072
+
14073
+ websocket("${wsPath}", ${handlerName});
14074
+ `;
14075
+ writeFileSafe(path8, content);
14076
+ emitWebsocketTest(wsPath, base, handlerName);
14077
+ }
14078
+ function generateListener(name, _flags) {
14079
+ const event = name.trim();
14080
+ const slug = toSnake(event.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
14081
+ const handlerName = `on${toPascal(slug)}`;
14082
+ const dir = resolve6("src/listeners");
14083
+ ensureDir(dir);
14084
+ const path8 = join15(dir, `${slug}.ts`);
14085
+ const body = aiFill(handlerName, {
14086
+ intent: `react to the '${event}' event`,
14087
+ given: `args -> whatever Events.emit("${event}", ...args) passed`,
14088
+ use: "your app code \u2014 Messenger().send(...), an ORM write, or Events.emit(...) a follow-up",
14089
+ ground: `tina4_context("event listener reaction", "nodejs") \xB7 skill tina4-developer-nodejs`,
14090
+ raise: `listener ${event} not implemented`
14091
+ });
14092
+ const content = `import { Events } from "tina4-nodejs";
14093
+
14094
+ /**
14095
+ * Listener for the '${event}' event.
14096
+ *
14097
+ * Registered on import by Events.on(). Node has NO src/listeners/
14098
+ * auto-discovery, so IMPORT this module once from app.ts to activate it:
14099
+ *
14100
+ * import "./src/listeners/${slug}.js";
14101
+ *
14102
+ * Fires when something calls Events.emit("${event}", ...args).
14103
+ */
14104
+ export function ${handlerName}(...args: unknown[]): void {
14105
+ // tina4:edit implement the reaction to '${event}' (email, ORM write, follow-up emit)
14106
+ ${body}}
14107
+
14108
+ Events.on("${event}", ${handlerName});
14109
+ `;
14110
+ writeFileSafe(path8, content);
14111
+ emitListenerTest(event, slug);
14112
+ }
14113
+ function writeTest(testName, content) {
14114
+ const dir = resolve6("tests");
14115
+ ensureDir(dir);
14116
+ writeFileSafe(join15(dir, `${testName}.test.ts`), content);
14117
+ }
14118
+ function standaloneTest(doc, body) {
14119
+ return `${doc}
14120
+
14121
+ let pass = 0;
14122
+ let fail = 0;
14123
+ function assert(label: string, ok: boolean): void {
14124
+ if (ok) { pass++; console.log(\` PASS \${label}\`); }
14125
+ else { fail++; console.log(\` FAIL \${label}\`); }
14126
+ }
14127
+ async function assertThrows(label: string, fn: () => unknown | Promise<unknown>): Promise<void> {
14128
+ try { await fn(); assert(label, false); }
14129
+ catch { assert(label, true); }
14130
+ }
14131
+
14132
+ ${body}
14133
+
14134
+ console.log(\`\\nResults: \${pass} passed, \${fail} failed\`);
14135
+ process.exit(fail > 0 ? 1 : 0);
14136
+ `;
14137
+ }
14138
+ function sampleLiteral(fieldType) {
14139
+ switch ((fieldType || "string").toLowerCase()) {
14140
+ case "int":
14141
+ case "integer":
14142
+ return "1";
14143
+ case "float":
14144
+ case "number":
14145
+ case "numeric":
14146
+ case "decimal":
14147
+ return "1.5";
14148
+ case "bool":
14149
+ case "boolean":
14150
+ return "true";
14151
+ case "datetime":
14152
+ return '"2020-01-01 00:00:00"';
14153
+ case "blob":
14154
+ return '"x"';
14155
+ default:
14156
+ return '"sample"';
14157
+ }
14158
+ }
14159
+ function emitModelTest(model, table2, fields) {
14160
+ const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
14161
+ const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
14162
+ const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
14163
+ const valueAssert = stringField ? `
14164
+ assert("string field round-trips", fetched !== null && (fetched.toObject() as Record<string, unknown>).${stringField} === "sample");` : "";
14165
+ const doc = `/**
14166
+ * Real ORM roundtrip for ${model} \u2014 no mocks, real SQLite.
14167
+ *
14168
+ * Generated with src/models/${model}.ts by \`tina4nodejs generate model
14169
+ * ${model}\`. The model scaffold is working code, so this passes on generation:
14170
+ * binds a real in-memory SQLite DB, creates the table, saves a row, reads it
14171
+ * back. Run with: npx tsx tests/${table2}_model.test.ts
14172
+ */
14173
+ import ${model} from "../src/models/${model}.js";
14174
+ import { initDatabase } from "tina4-nodejs/orm";`;
14175
+ const body = `await initDatabase({ url: "sqlite:///:memory:" });
14176
+ await ${model}.createTable();
14177
+
14178
+ const row = new ${model}({ ${payload} });
14179
+ const saved = await row.save();
14180
+ assert("create() persists and returns the row", saved !== false && Boolean(row.toObject().id));
14181
+
14182
+ const id = row.toObject().id;
14183
+ const fetched = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [id]);
14184
+ assert("row reads back by id", fetched !== null);
14185
+ assert("read-back id matches", fetched !== null && (fetched.toObject() as Record<string, unknown>).id === id);${valueAssert}
14186
+
14187
+ const missing = await ${model}.selectOne("SELECT * FROM ${table2} WHERE id = ?", [999999]);
14188
+ assert("find missing returns null", missing === null);`;
14189
+ writeTest(`${table2}_model`, standaloneTest(doc, body));
14190
+ }
14191
+ function emitRouteStubTest(route) {
14192
+ const doc = `/**
14193
+ * Routing test for ${route} \u2014 no mocks, real Router + route discovery.
14194
+ *
14195
+ * Generated with src/routes/api/${route}/ by \`tina4nodejs generate route
14196
+ * ${route}\` (no --model). The handlers are AI-FILL stubs that throw until you
14197
+ * implement them, so this tests what IS live on generation: all five routes
14198
+ * register on the REAL Router, and the list handler fails loud until filled.
14199
+ * Run with: npx tsx tests/${route}.test.ts
14200
+ */
14201
+ import { dirname, resolve } from "node:path";
14202
+ import { fileURLToPath } from "node:url";
14203
+ import { Router, discoverRoutes } from "tina4-nodejs";
14204
+ import listHandler from "../src/routes/api/${route}/get.js";`;
14205
+ const body = `const here = dirname(fileURLToPath(import.meta.url));
14206
+ const router = new Router();
14207
+ const defs = await discoverRoutes(resolve(here, "../src/routes"));
14208
+ for (const def of defs) router.addRoute(def);
14209
+
14210
+ const sigs = defs.map((d) => \`\${d.method} \${d.pattern}\`);
14211
+ for (const sig of ["GET /api/${route}", "POST /api/${route}", "GET /api/${route}/{id}", "PUT /api/${route}/{id}", "DELETE /api/${route}/{id}"]) {
14212
+ assert(\`route registered: \${sig}\`, sigs.includes(sig));
14213
+ }
14214
+
14215
+ // The scaffolded list handler is a loud AI-FILL stub \u2014 it throws until filled.
14216
+ await assertThrows("list handler is a live stub (throws until filled)",
14217
+ () => listHandler({} as never, {} as never));`;
14218
+ writeTest(route, standaloneTest(doc, body));
14219
+ }
14220
+ function emitMiddlewareTest(name, snake) {
14221
+ const doc = `/**
14222
+ * Real dispatch test for the ${name} middleware \u2014 no mocks.
14223
+ *
14224
+ * Generated with src/middleware/${snake}.ts by \`tina4nodejs generate middleware
14225
+ * ${name}\`. Drives the scaffolded before/after functions through the REAL
14226
+ * MiddlewareChain with a real Tina4Request/Response (built from real node http
14227
+ * objects) \u2014 the same continuation dispatch the live server runs.
14228
+ * Run with: npx tsx tests/${snake}.test.ts
14229
+ */
14230
+ import { IncomingMessage, ServerResponse } from "node:http";
14231
+ import { Socket } from "node:net";
14232
+ import { MiddlewareChain, createRequest, createResponse } from "tina4-nodejs";
14233
+ import type { Tina4Request, Tina4Response } from "tina4-nodejs";
14234
+ import { before${name}, after${name} } from "../src/middleware/${snake}.js";`;
14235
+ const body = `function realPair(headers: Record<string, string>): { req: Tina4Request; res: Tina4Response; raw: ServerResponse } {
14236
+ const socket = new Socket();
14237
+ const rawReq = new IncomingMessage(socket);
14238
+ rawReq.method = "GET";
14239
+ rawReq.url = "/";
14240
+ rawReq.headers = { ...headers, host: "localhost" };
14241
+ rawReq.push(null);
14242
+ const rawRes = new ServerResponse(rawReq);
14243
+ rawRes.write = (() => true) as typeof rawRes.write;
14244
+ rawRes.end = (function (this: ServerResponse) { return this; }) as typeof rawRes.end;
14245
+ return { req: createRequest(rawReq), res: createResponse(rawRes), raw: rawRes };
14246
+ }
14247
+
14248
+ // before(): blocks an unauthenticated request (401, does not call next()).
14249
+ {
14250
+ const chain = new MiddlewareChain();
14251
+ chain.use(before${name});
14252
+ let reached = false;
14253
+ chain.use(async (_r, _s, next) => { reached = true; next(); });
14254
+ const { req, res, raw } = realPair({});
14255
+ await chain.run(req, res);
14256
+ assert("before blocks unauthenticated (401, chain short-circuits)", raw.statusCode === 401 && reached === false);
14257
+ }
14258
+
14259
+ // before(): lets an authenticated request through to the next middleware.
14260
+ {
14261
+ const chain = new MiddlewareChain();
14262
+ chain.use(before${name});
14263
+ let reached = false;
14264
+ chain.use(async (_r, _s, next) => { reached = true; next(); });
14265
+ const { req, res } = realPair({ authorization: "Bearer test" });
14266
+ await chain.run(req, res);
14267
+ assert("before passes an authenticated request through", reached === true);
14268
+ }
14269
+
14270
+ // after(): always continues the chain.
14271
+ {
14272
+ const chain = new MiddlewareChain();
14273
+ chain.use(after${name});
14274
+ let reached = false;
14275
+ chain.use(async (_r, _s, next) => { reached = true; next(); });
14276
+ const { req, res } = realPair({});
14277
+ await chain.run(req, res);
14278
+ assert("after runs and continues the chain", reached === true);
14279
+ }`;
14280
+ writeTest(snake, standaloneTest(doc, body));
14281
+ }
14282
+ function emitServiceTest(name, snake, camel) {
14283
+ const doc = `/**
14284
+ * Real ServiceRunner test for the ${name} service \u2014 no mocks.
14285
+ *
14286
+ * Generated with src/services/${snake}.ts by \`tina4nodejs generate service
14287
+ * ${name}\`. Registers the scaffold on a REAL ServiceRunner and confirms the
14288
+ * descriptor; the task body is an AI-FILL stub that throws until filled.
14289
+ * Run with: npx tsx tests/${snake}.test.ts
14290
+ */
14291
+ import { ServiceRunner } from "tina4-nodejs";
14292
+ import service, { ${camel}Task } from "../src/services/${snake}.js";`;
14293
+ const body = `assert("descriptor has a name + callable handler",
14294
+ service.name === "${snake}" && typeof service.handler === "function");
14295
+
14296
+ // Register on a REAL ServiceRunner and confirm it is listed.
14297
+ ServiceRunner.register(service.name, service.handler, { interval: (service as { interval?: number }).interval });
14298
+ assert("registers on a real ServiceRunner", ServiceRunner.list().some((s) => s.name === "${snake}"));
14299
+ ServiceRunner.remove("${snake}");
14300
+
14301
+ // The scaffolded task body is an AI-FILL stub \u2014 it throws until filled.
14302
+ await assertThrows("task is a live stub (throws until filled)", () => ${camel}Task({} as never));`;
14303
+ writeTest(snake, standaloneTest(doc, body));
14304
+ }
14305
+ function emitQueueTest(topic, slug, pascal) {
14306
+ const doc = `/**
14307
+ * Real file-backed Queue test for the ${topic} worker \u2014 no mocks.
14308
+ *
14309
+ * Generated with src/services/${slug}_consumer.ts by \`tina4nodejs generate
14310
+ * queue ${topic}\`. Pushes a REAL job onto the real file-backed Queue and
14311
+ * asserts it is enqueued, and that the consumer is wired as a daemon. The
14312
+ * per-job handle is an AI-FILL stub that throws until filled.
14313
+ * Run with: npx tsx tests/${slug}.test.ts
14314
+ */
14315
+ import { Queue } from "tina4-nodejs";
14316
+ import worker, { publish${pascal}, handle${pascal} } from "../src/services/${slug}_consumer.js";`;
14317
+ const body = `const jobId = publish${pascal}({ hello: "world" });
14318
+ assert("publish enqueues a real job (returns an id)", typeof jobId === "string" && jobId.length > 0);
14319
+ assert("the job is really on the queue", new Queue({ topic: "${topic}" }).size() >= 1);
14320
+
14321
+ assert("consumer default export is a daemon", worker.daemon === true);
14322
+ assert("consumer handler is wired", typeof worker.handler === "function");
14323
+
14324
+ // The per-job handler is an AI-FILL stub \u2014 it throws until filled.
14325
+ await assertThrows("handle is a live stub (throws until filled)", () => handle${pascal}({}));`;
14326
+ writeTest(slug, standaloneTest(doc, body));
14327
+ }
14328
+ function emitValidatorTest(name, snake, pascal) {
14329
+ const doc = `/**
14330
+ * Real validation test for validate${pascal} \u2014 no mocks.
14331
+ *
14332
+ * Generated with src/validators/${snake}.ts by \`tina4nodejs generate validator
14333
+ * ${name}\`. The scaffold ships a starter rule (required "name"), so this passes
14334
+ * on generation \u2014 adjust the rules for your payload and update these cases.
14335
+ * Run with: npx tsx tests/${snake}.test.ts
14336
+ */
14337
+ import { validate${pascal} } from "../src/validators/${snake}.js";`;
14338
+ const body = `assert("valid input passes", validate${pascal}({ name: "Ada" }).isValid());
14339
+
14340
+ const bad = validate${pascal}({});
14341
+ assert("invalid input fails", bad.isValid() === false);
14342
+ assert("invalid input reports errors", bad.errors().length > 0);`;
14343
+ writeTest(snake, standaloneTest(doc, body));
14344
+ }
14345
+ function emitSeederTest(model, table2) {
14346
+ const doc = `/**
14347
+ * Real seeding test for the ${model} seeder \u2014 no mocks, real SQLite.
14348
+ *
14349
+ * Generated with src/seeds/${table2}_seeder.ts by \`tina4nodejs generate seeder
14350
+ * ${model}\`. Binds a real SQLite DB, creates the table, runs the scaffolded
14351
+ * seeder (auto-fills every field via FakeData) and asserts rows were created.
14352
+ * Run with: npx tsx tests/${table2}_seeder.test.ts
14353
+ */
14354
+ import { initDatabase, FakeData } from "tina4-nodejs/orm";
14355
+ import ${model} from "../src/models/${model}.js";
14356
+ import { fieldOverrides, run } from "../src/seeds/${table2}_seeder.js";`;
14357
+ const body = `process.env.TINA4_DATABASE_URL = "sqlite:///test_${table2}_seeder.db";
14358
+ await initDatabase({ url: process.env.TINA4_DATABASE_URL });
14359
+ await ${model}.createTable();
14360
+
14361
+ assert("fieldOverrides returns an object", typeof fieldOverrides(new FakeData()) === "object");
14362
+
14363
+ await run(); // run() re-binds the same DB URL and seeds via seedOrm
14364
+ const rows = await ${model}.all();
14365
+ assert("run() seeds real rows", rows.length >= 1);`;
14366
+ writeTest(`${table2}_seeder`, standaloneTest(doc, body));
14367
+ }
14368
+ function emitWebsocketTest(wsPath, base, handler) {
14369
+ const doc = `/**
14370
+ * Real handler test for the ${wsPath} WebSocket route \u2014 no mocks.
14371
+ *
14372
+ * Generated with src/routes/ws_${base}.ts by \`tina4nodejs generate websocket
14373
+ * ...\`. Confirms the handler registers on the REAL Router (importing runs the
14374
+ * module-level websocket() call) and drives the real handler for the "close"
14375
+ * event (no socket needed). The "message" branch is an AI-FILL stub that throws
14376
+ * until filled. Run with: npx tsx tests/ws_${base}.test.ts
14377
+ */
14378
+ import { Router } from "tina4-nodejs";
14379
+ import { ${handler} } from "../src/routes/ws_${base}.js"; // importing registers via websocket()`;
14380
+ const body = `assert("handler registered on the real router",
14381
+ Router.getWebSocketRoutes().some((r) => r.pattern === "${wsPath}"));
14382
+
14383
+ // The "close" branch returns cleanly without a live connection.
14384
+ const closed = await ${handler}(null as never, "close", "");
14385
+ assert("close event handled cleanly", closed === undefined);
14386
+
14387
+ // The "message" branch is an AI-FILL stub \u2014 it throws until filled.
14388
+ await assertThrows("message branch is a live stub (throws until filled)",
14389
+ () => ${handler}(null as never, "message", "hi"));`;
14390
+ writeTest(`ws_${base}`, standaloneTest(doc, body));
14391
+ }
14392
+ function emitListenerTest(event, slug) {
14393
+ const doc = `/**
14394
+ * Real event-bus test for the '${event}' listener \u2014 no mocks.
14395
+ *
14396
+ * Generated with src/listeners/${slug}.ts by \`tina4nodejs generate listener
14397
+ * ${event}\`. Confirms the listener binds on the REAL event bus (importing runs
14398
+ * the module-level Events.on) and that emitting the event reaches it. The
14399
+ * reaction body is an AI-FILL stub, so a strict emit re-raises here (proving it
14400
+ * ran). Run with: npx tsx tests/${slug}.test.ts
14401
+ */
14402
+ import { Events } from "tina4-nodejs";
14403
+ import "../src/listeners/${slug}.js"; // importing registers the listener via Events.on()`;
14404
+ const body = `assert("listener registered on the real event bus", Events.listeners("${event}").length >= 1);
14405
+
14406
+ // strict emit re-raises the stub error, proving the listener actually ran.
14407
+ await assertThrows("emitting the event reaches the (stub) listener",
14408
+ () => Events.emit("${event}", { strict: true }, { id: 1 }));`;
14409
+ writeTest(slug, standaloneTest(doc, body));
14410
+ }
14411
+ function emitAuthTest() {
14412
+ const doc = `/**
14413
+ * Real auth test \u2014 register / login / me via the real TestClient.
14414
+ *
14415
+ * Generated with the auth scaffold by \`tina4nodejs generate auth\`. No mocks:
14416
+ * real Router + route discovery, real Auth (PBKDF2 + JWT), real SQLite. register
14417
+ * + login are public; the token from login authenticates GET /api/auth/me.
14418
+ * Run with: npx tsx tests/auth.test.ts
14419
+ */
14420
+ import { dirname, resolve } from "node:path";
14421
+ import { fileURLToPath } from "node:url";
14422
+ import { Router, TestClient, discoverRoutes } from "tina4-nodejs";
14423
+ import { initDatabase } from "tina4-nodejs/orm";
14424
+ import User from "../src/models/User.js";
14425
+
14426
+ process.env.TINA4_SECRET = process.env.TINA4_SECRET ?? "test-secret";
14427
+ delete process.env.TINA4_API_KEY;
14428
+ const here = dirname(fileURLToPath(import.meta.url));`;
14429
+ const body = `await initDatabase({ url: "sqlite:///test_auth.db" });
14430
+ await User.createTable();
14431
+ for (const existing of await User.all()) await existing.delete(); // start from an empty table
14432
+
14433
+ const router = new Router();
14434
+ for (const def of await discoverRoutes(resolve(here, "../src/routes"))) router.addRoute(def);
14435
+ const client = new TestClient(router);
14436
+
14437
+ const registered = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
14438
+ assert("register a new user -> 201", registered.status === 201);
14439
+
14440
+ const duplicate = await client.post("/api/auth/register", { json: { email: "a@b.c", password: "secret12" } });
14441
+ assert("duplicate register -> 409", duplicate.status === 409);
14442
+
14443
+ const login = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "secret12" } });
14444
+ assert("login -> 200", login.status === 200);
14445
+ const token = (login.json() as { token?: string }).token;
14446
+ assert("login returns a token", typeof token === "string" && token.length > 0);
14447
+
14448
+ const me = await client.get("/api/auth/me", { headers: { authorization: \`Bearer \${token}\` } });
14449
+ assert("authenticated GET /api/auth/me -> 200 (token accepted)", me.status === 200);
14450
+ assert("me returns the authenticated user's email",
14451
+ ((me.json() as { user?: { email?: string } }).user?.email) === "a@b.c");
14452
+
14453
+ const anon = await client.get("/api/auth/me");
14454
+ assert("anonymous GET /api/auth/me -> 401", anon.status === 401);
14455
+
14456
+ const bad = await client.post("/api/auth/login", { json: { email: "a@b.c", password: "WRONG" } });
14457
+ assert("wrong password -> 401", bad.status === 401);`;
14458
+ writeTest("auth", standaloneTest(doc, body));
14459
+ }
14460
+ function emitMigrationTest(migrationName, table2) {
14461
+ const doc = `/**
14462
+ * Real migration test for ${migrationName} \u2014 no mocks, real SQLite.
14463
+ *
14464
+ * Generated with the migration by \`tina4nodejs generate migration
14465
+ * ${migrationName}\`. Applies the generated UP SQL against a fresh real
14466
+ * in-memory SQLite database and asserts the table exists, then applies the DOWN
14467
+ * SQL and asserts it is gone \u2014 the raw SQL the migration runner executes.
14468
+ * Run with: npx tsx tests/${table2}_migration.test.ts
14469
+ */
14470
+ import { dirname, join, resolve } from "node:path";
14471
+ import { fileURLToPath } from "node:url";
14472
+ import { readdirSync, readFileSync } from "node:fs";
14473
+ import { SQLiteAdapter } from "tina4-nodejs/orm";`;
14474
+ const body = `const here = dirname(fileURLToPath(import.meta.url));
14475
+ const migrationsDir = resolve(here, "../migrations");
14476
+
14477
+ const upFile = readdirSync(migrationsDir).find((f) => f.endsWith("_${migrationName}.sql") && !f.endsWith(".down.sql"));
14478
+ assert("generated UP migration file exists", Boolean(upFile));
14479
+ const downFile = upFile!.replace(/\\.sql$/, ".down.sql");
14480
+
14481
+ function statements(sql: string): string[] {
14482
+ const noComments = sql.split("\\n").filter((l) => !l.trim().startsWith("--")).join("\\n");
14483
+ return noComments.split(";").map((s) => s.trim()).filter(Boolean);
14484
+ }
14485
+
14486
+ const upText = readFileSync(join(migrationsDir, upFile!), "utf-8");
14487
+ const upSql = upText.split("-- UP")[1].split("-- DOWN")[0];
14488
+ const downSql = readFileSync(join(migrationsDir, downFile), "utf-8");
14489
+
14490
+ const db = new SQLiteAdapter(":memory:");
14491
+ for (const stmt of statements(upSql)) db.execute(stmt);
14492
+ assert("UP creates the ${table2} table", db.tableExists("${table2}"));
14493
+
14494
+ for (const stmt of statements(downSql)) db.execute(stmt);
14495
+ assert("DOWN drops the ${table2} table", db.tableExists("${table2}") === false);`;
14496
+ writeTest(`${table2}_migration`, standaloneTest(doc, body));
14497
+ }
14498
+ var FIELD_TYPE_MAP, SQL_RESERVED_TABLE_NAMES, RESOLUTION_ENVELOPE_VERSION, __resolution, TINA4_EDIT_MARKER, DEFAULT_FIELDS, GENERATORS, GENERATOR_LIST, NEXT_STEPS;
14499
+ var init_generate = __esm({
14500
+ "../cli/src/commands/generate.ts"() {
14501
+ "use strict";
14502
+ FIELD_TYPE_MAP = {
14503
+ string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
14504
+ str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
14505
+ int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
14506
+ integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
14507
+ float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
14508
+ number: { orm: '"number"', sql: "REAL", defaultVal: "0" },
14509
+ numeric: { orm: '"number"', sql: "REAL", defaultVal: "0" },
14510
+ decimal: { orm: '"number"', sql: "REAL", defaultVal: "0" },
14511
+ bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
14512
+ boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
14513
+ text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
14514
+ datetime: { orm: '"datetime"', sql: "TEXT", defaultVal: "NULL" },
14515
+ blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
14516
+ };
14517
+ SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
14518
+ "order",
14519
+ "group",
14520
+ "user",
14521
+ "table",
14522
+ "select",
14523
+ "from",
14524
+ "where",
14525
+ "index",
14526
+ "key",
14527
+ "values",
14528
+ "column",
14529
+ "constraint",
14530
+ "check",
14531
+ "default",
14532
+ "primary",
14533
+ "foreign",
14534
+ "references",
14535
+ "unique",
14536
+ "join",
14537
+ "union",
14538
+ "having",
14539
+ "limit",
14540
+ "offset",
14541
+ "desc",
14542
+ "asc",
14543
+ "case",
14544
+ "when",
14545
+ "then",
14546
+ "else",
14547
+ "end",
14548
+ "and",
14549
+ "or",
14550
+ "not",
14551
+ "null",
14552
+ "insert",
14553
+ "update",
14554
+ "delete",
14555
+ "create",
14556
+ "drop",
14557
+ "alter",
14558
+ "grant",
14559
+ "revoke",
14560
+ "commit",
14561
+ "rollback",
14562
+ "view",
14563
+ "trigger",
14564
+ "procedure",
14565
+ "function",
14566
+ "database",
14567
+ "schema",
14568
+ "session",
14569
+ "set",
14570
+ "into",
14571
+ "as",
14572
+ "on",
14573
+ "by",
14574
+ "inner",
14575
+ "outer",
14576
+ "left",
14577
+ "right",
14578
+ "full",
14579
+ "natural",
14580
+ "using",
14581
+ "with",
14582
+ "distinct",
14583
+ "between",
14584
+ "exists",
14585
+ "like",
14586
+ "in",
14587
+ "is",
14588
+ "all",
14589
+ "any",
14590
+ "cross",
14591
+ "add",
14592
+ "row",
14593
+ "rows",
14594
+ "range",
14595
+ "current",
14596
+ "to"
14597
+ ]);
14598
+ RESOLUTION_ENVELOPE_VERSION = "generate_v1_1";
14599
+ __resolution = {
14600
+ target: "",
14601
+ input: { name: "", fields: null },
14602
+ body: { transformations: [] },
14603
+ actionsTaken: [],
14604
+ dryRun: false,
14605
+ jsonMode: false
14606
+ };
14607
+ TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
14608
+ DEFAULT_FIELDS = [["name", "string"]];
14609
+ GENERATORS = {
14610
+ model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
14611
+ route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
14612
+ crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
14613
+ migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
14614
+ middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
14615
+ test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
14616
+ form: { handler: generateForm, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" },
14617
+ view: { handler: generateView, usage: '<Name> [--fields "..."]', summary: "List + detail view templates" },
14618
+ auth: { handler: (_n, f) => generateAuth(f), usage: "", summary: "Login/register routes (public) + User model + templates" },
14619
+ service: { handler: generateService, usage: '<Name> [--every 5m | --cron "..."]', summary: "Scheduled ServiceRunner task (src/services/)" },
14620
+ queue: { handler: generateQueue, usage: "<topic>", summary: "Producer + consumer daemon worker (src/services/)" },
14621
+ validator: { handler: generateValidator, usage: "<Name>", summary: "Request-body Validator (src/validators/)" },
14622
+ seeder: { handler: generateSeeder, usage: "<Model>", summary: "FakeData + seedOrm seeder (src/seeds/)" },
14623
+ websocket: { handler: generateWebsocket, usage: "<path>", summary: "websocket() handler (src/routes/)" },
14624
+ listener: { handler: generateListener, usage: "<event>", summary: "Events.on(event) listener (src/listeners/)" }
14625
+ };
14626
+ GENERATOR_LIST = Object.keys(GENERATORS).join(", ");
14627
+ NEXT_STEPS = {
14628
+ model: ({ name, table: table2 }) => [
14629
+ `Edit src/models/${name}.ts to add fields beyond the default 'name'`,
14630
+ `Apply the migration: npx tina4nodejs migrate`,
14631
+ `Run its test: npx tsx tests/${table2}_model.test.ts`,
14632
+ `Add CRUD scaffolding: npx tina4nodejs generate crud ${name}`
14633
+ ],
14634
+ route: ({ name, table: table2 }) => [
14635
+ `Fill the AI-FILL stubs in src/routes/api/${name.replace(/^\//, "")}/`,
14636
+ `Run its test: npx tsx tests/${table2}.test.ts`,
14637
+ `Serve and try: npx tina4nodejs serve -> curl http://localhost:7148/api/${name.replace(/^\//, "")}`
14638
+ ],
14639
+ crud: ({ name, table: table2 }) => [
14640
+ `Apply the migration: npx tina4nodejs migrate`,
14641
+ `Serve and try: npx tina4nodejs serve -> visit /swagger`,
14642
+ `Run the gate test: npx tsx tests/${toPlural(table2)}.test.ts`,
14643
+ `Change fields: edit src/models/${name}.ts then re-run generate crud`
14644
+ ],
14645
+ migration: () => [
14646
+ `Apply pending migrations: npx tina4nodejs migrate`,
14647
+ `Check status: npx tina4nodejs migrate:status`,
14648
+ `Roll back the batch: npx tina4nodejs migrate:rollback`
14649
+ ],
14650
+ middleware: ({ name }) => [
14651
+ `Wire it: router.middleware(before${name}, after${name}) \u2014 or bind per-route`,
14652
+ `Run its test: npx tsx tests/${toSnake(name)}.test.ts`
14653
+ ],
14654
+ test: ({ name }) => [
14655
+ `Fill the TODOs in tests/${toSnake(name)}.test.ts`,
14656
+ `Run it: npx tsx tests/${toSnake(name)}.test.ts`
14657
+ ],
14658
+ form: ({ name, table: table2 }) => [
14659
+ `Render from a route: res.render("forms/${table2}.twig", { item })`,
14660
+ `Add the POST route: npx tina4nodejs generate route ${toPlural(table2)} --model ${name}`
14661
+ ],
14662
+ view: ({ table: table2 }) => [
14663
+ `Wire routes to render list -> ${toPlural(table2)}.twig, detail -> ${table2}.twig`,
14664
+ `Customize the templates in src/templates/pages/`
14665
+ ],
14666
+ auth: () => [
14667
+ `Apply the migration: npx tina4nodejs migrate`,
14668
+ `Run the auth test: npx tsx tests/auth.test.ts`,
14669
+ `Try register: curl -X POST http://localhost:7148/api/auth/register -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`,
14670
+ `Login: curl -X POST http://localhost:7148/api/auth/login -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`
14671
+ ],
14672
+ service: ({ name }) => [
14673
+ `Wire ServiceRunner in app.ts: await ServiceRunner.discover("src/services"); ServiceRunner.start();`,
14674
+ `Fill the task body in src/services/${toSnake(name)}.ts`,
14675
+ `Run its test: npx tsx tests/${toSnake(name)}.test.ts`
14676
+ ],
14677
+ queue: ({ name }) => {
14678
+ const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
14679
+ return [
14680
+ `Fill handle${toPascal(name)}() in src/services/${slug}_consumer.ts`,
14681
+ `Produce a job: publish${toPascal(name)}({ ... })`,
14682
+ `Run the worker: npx tina4nodejs queue work ${name}`,
14683
+ `Run its test: npx tsx tests/${slug}.test.ts`
14684
+ ];
14685
+ },
14686
+ validator: ({ name }) => [
14687
+ `Add rules in src/validators/${toSnake(name)}.ts (.email/.minLength/.integer/.inList/.pattern)`,
14688
+ `Run its test: npx tsx tests/${toSnake(name)}.test.ts`
14689
+ ],
14690
+ seeder: ({ name, table: table2 }) => [
14691
+ `Override any fields that need a specific shape in src/seeds/${table2}_seeder.ts`,
14692
+ `Seed the table: npx tina4nodejs seed`,
14693
+ `Run its test: npx tsx tests/${table2}_seeder.test.ts`
14694
+ ],
14695
+ websocket: ({ name }) => {
14696
+ const raw = name.trim();
14697
+ const slugRaw = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
14698
+ const base = slugRaw.startsWith("ws_") ? slugRaw.slice(3) : slugRaw;
14699
+ return [
14700
+ `Import once in app.ts to register: import "./src/routes/ws_${base}.js";`,
14701
+ `Fill the "message" branch in src/routes/ws_${base}.ts`,
14702
+ `Run its test: npx tsx tests/ws_${base}.test.ts`
14703
+ ];
14704
+ },
14705
+ listener: ({ name }) => {
14706
+ const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
14707
+ return [
14708
+ `Import once in app.ts to register: import "./src/listeners/${slug}.js";`,
14709
+ `Fill the reaction in src/listeners/${slug}.ts`,
14710
+ `Run its test: npx tsx tests/${slug}.test.ts`
14711
+ ];
14712
+ }
14713
+ };
14714
+ }
14715
+ });
14716
+
12685
14717
  // ../core/src/mcp.ts
12686
14718
  var mcp_exports = {};
12687
14719
  __export(mcp_exports, {
@@ -13182,10 +15214,10 @@ function registerDevTools(server) {
13182
15214
  "swagger_spec",
13183
15215
  (_args) => {
13184
15216
  try {
13185
- const { generate: generate2 } = reqSibling("swagger");
15217
+ const { generate: generate3 } = reqSibling("swagger");
13186
15218
  const { defaultRouter: defaultRouter2 } = req("./router.js");
13187
15219
  const routes = defaultRouter2?.getRoutes?.() ?? [];
13188
- return generate2?.(routes, []) ?? { info: "Swagger not available" };
15220
+ return generate3?.(routes, []) ?? { info: "Swagger not available" };
13189
15221
  } catch (e) {
13190
15222
  return { error: e.message };
13191
15223
  }
@@ -13344,20 +15376,43 @@ function registerDevTools(server) {
13344
15376
  );
13345
15377
  server.registerTool(
13346
15378
  "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 };
15379
+ async (args) => {
15380
+ try {
15381
+ const rawDesc = String(args.description ?? "").trim();
15382
+ if (!rawDesc) return { ok: false, error: "description is required" };
15383
+ const slug = rawDesc.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
15384
+ if (!slug) return { ok: false, error: "description sanitised to an empty slug" };
15385
+ const migrationsDir = path3.join(projectRoot3, "migrations");
15386
+ if (fs4.existsSync(migrationsDir)) {
15387
+ const upSuffix = `_${slug}.sql`;
15388
+ const downSuffix = `_${slug}.down.sql`;
15389
+ const existing = fs4.readdirSync(migrationsDir).filter(
15390
+ (f) => f.endsWith(upSuffix) && !f.endsWith(downSuffix) || f.endsWith(downSuffix)
15391
+ );
15392
+ if (existing.length > 0) {
15393
+ return {
15394
+ ok: false,
15395
+ error: `A migration with slug "${slug}" already exists`,
15396
+ existing
15397
+ };
15398
+ }
15399
+ }
15400
+ const originalCwd = process.cwd();
15401
+ try {
15402
+ process.chdir(projectRoot3);
15403
+ const gen = await Promise.resolve().then(() => (init_generate(), generate_exports));
15404
+ const envelope = await gen.generateProgrammatic("migration", slug, ["--no-test"]);
15405
+ const migrationPath = envelope.resolution?.migration_path;
15406
+ const created = migrationPath ? path3.basename(migrationPath) : "";
15407
+ return { ok: true, created, resolution: envelope };
15408
+ } finally {
15409
+ process.chdir(originalCwd);
15410
+ }
15411
+ } catch (e) {
15412
+ return { ok: false, error: e.message };
15413
+ }
13359
15414
  },
13360
- "Create a new migration file",
15415
+ "Create a new migration file (delegates to `generate migration` \u2014 emits the ADR-0063 generate_v1_1 envelope + timestamped filename)",
13361
15416
  schemaFromParams([{ name: "description", type: "string" }])
13362
15417
  );
13363
15418
  server.registerTool(
@@ -14125,14 +16180,14 @@ data: ${channel.buffer.shift()}
14125
16180
  `;
14126
16181
  continue;
14127
16182
  }
14128
- const gotMessage = await new Promise((resolve20) => {
16183
+ const gotMessage = await new Promise((resolve21) => {
14129
16184
  const timer = setTimeout(() => {
14130
16185
  channel.wake = null;
14131
- resolve20(false);
16186
+ resolve21(false);
14132
16187
  }, keepaliveMs);
14133
16188
  channel.wake = () => {
14134
16189
  clearTimeout(timer);
14135
- resolve20(true);
16190
+ resolve21(true);
14136
16191
  };
14137
16192
  });
14138
16193
  if (!gotMessage) yield `: keep-alive
@@ -15451,8 +17506,8 @@ __export(context_exports, {
15451
17506
  fts5Supported: () => fts5Supported
15452
17507
  });
15453
17508
  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";
17509
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
17510
+ 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
17511
  function fts5Supported() {
15457
17512
  try {
15458
17513
  const conn = new DatabaseSync2(":memory:");
@@ -15475,13 +17530,13 @@ function realResolve(abs) {
15475
17530
  } catch {
15476
17531
  }
15477
17532
  try {
15478
- return join16(realpathSync5(dirname8(abs)), basename4(abs));
17533
+ return join17(realpathSync5(dirname8(abs)), basename4(abs));
15479
17534
  } catch {
15480
17535
  return abs;
15481
17536
  }
15482
17537
  }
15483
17538
  function dbKey(db) {
15484
- return resolve7(db ? String(db) : join16(process.cwd(), ".tina4", "context.db"));
17539
+ return resolve8(db ? String(db) : join17(process.cwd(), ".tina4", "context.db"));
15485
17540
  }
15486
17541
  function defaultContext(root, db) {
15487
17542
  const key = dbKey(db);
@@ -15565,7 +17620,7 @@ var init_context = __esm({
15565
17620
  if (!this.available) return;
15566
17621
  const parent = dirname8(this.path);
15567
17622
  if (parent !== "" && parent !== ".") {
15568
- mkdirSync9(parent, { recursive: true });
17623
+ mkdirSync10(parent, { recursive: true });
15569
17624
  }
15570
17625
  this.conn = new DatabaseSync2(this.path);
15571
17626
  this.ensureTable();
@@ -15637,7 +17692,7 @@ var init_context = __esm({
15637
17692
  */
15638
17693
  indexRoot(root) {
15639
17694
  if (!this.available) return 0;
15640
- const rootAbs = realResolve(resolve7(String(root)));
17695
+ const rootAbs = realResolve(resolve8(String(root)));
15641
17696
  this.root = rootAbs;
15642
17697
  let total = 0;
15643
17698
  const walk2 = (dir) => {
@@ -15654,11 +17709,11 @@ var init_context = __esm({
15654
17709
  files.sort();
15655
17710
  for (const fn of files) {
15656
17711
  if (!_Context.eligible(fn)) continue;
15657
- const full = join16(dir, fn);
15658
- const rel = relative3(rootAbs, full);
17712
+ const full = join17(dir, fn);
17713
+ const rel = relative4(rootAbs, full);
15659
17714
  total += this.indexPath(full, rel);
15660
17715
  }
15661
- for (const d of subdirs) walk2(join16(dir, d));
17716
+ for (const d of subdirs) walk2(join17(dir, d));
15662
17717
  };
15663
17718
  walk2(rootAbs);
15664
17719
  return total;
@@ -15674,9 +17729,9 @@ var init_context = __esm({
15674
17729
  reindexFile(changedPath) {
15675
17730
  if (!this.available || this.root === null) return -1;
15676
17731
  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);
17732
+ const abs = isAbsolute4(raw) ? raw : join17(process.cwd(), raw);
17733
+ const resolved = realResolve(resolve8(abs));
17734
+ const rel = relative4(this.root, resolved);
15680
17735
  if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) {
15681
17736
  return -1;
15682
17737
  }
@@ -15687,7 +17742,7 @@ var init_context = __esm({
15687
17742
  }
15688
17743
  if (!_Context.eligible(basename4(rel))) return -1;
15689
17744
  const stored = rel;
15690
- if (!existsSync14(abs)) {
17745
+ if (!existsSync15(abs)) {
15691
17746
  this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
15692
17747
  return 0;
15693
17748
  }
@@ -16433,7 +18488,7 @@ var init_websocket = __esm({
16433
18488
  * Start the WebSocket server.
16434
18489
  */
16435
18490
  async start() {
16436
- return new Promise((resolve20, reject) => {
18491
+ return new Promise((resolve21, reject) => {
16437
18492
  this.server = createServer((req2, res) => {
16438
18493
  res.writeHead(426, { "Content-Type": "text/plain" });
16439
18494
  res.end("Upgrade Required");
@@ -16443,7 +18498,7 @@ var init_websocket = __esm({
16443
18498
  });
16444
18499
  this.server.listen(this.port, () => {
16445
18500
  this.startIdleReaper();
16446
- resolve20();
18501
+ resolve21();
16447
18502
  });
16448
18503
  this.server.on("error", (err) => {
16449
18504
  this.emit("error", err);
@@ -17664,8 +19719,8 @@ var init_job = __esm({
17664
19719
  });
17665
19720
 
17666
19721
  // ../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";
19722
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync6, existsSync as existsSync16 } from "node:fs";
19723
+ import { join as join18 } from "node:path";
17669
19724
  import { randomUUID as randomUUID5 } from "node:crypto";
17670
19725
  var LiteBackend;
17671
19726
  var init_liteBackend = __esm({
@@ -17689,22 +19744,22 @@ var init_liteBackend = __esm({
17689
19744
  this.visibilityTimeout = visibilityTimeout;
17690
19745
  }
17691
19746
  ensureDir(queue) {
17692
- const dir = join17(this.basePath, queue);
17693
- mkdirSync10(dir, { recursive: true });
19747
+ const dir = join18(this.basePath, queue);
19748
+ mkdirSync11(dir, { recursive: true });
17694
19749
  return dir;
17695
19750
  }
17696
19751
  ensureFailedDir(queue) {
17697
- const dir = join17(this.basePath, queue, "failed");
17698
- mkdirSync10(dir, { recursive: true });
19752
+ const dir = join18(this.basePath, queue, "failed");
19753
+ mkdirSync11(dir, { recursive: true });
17699
19754
  return dir;
17700
19755
  }
17701
19756
  ensureReservedDir(queue) {
17702
- const dir = join17(this.basePath, queue, "reserved");
17703
- mkdirSync10(dir, { recursive: true });
19757
+ const dir = join18(this.basePath, queue, "reserved");
19758
+ mkdirSync11(dir, { recursive: true });
17704
19759
  return dir;
17705
19760
  }
17706
19761
  reservedPath(queue, jobId) {
17707
- return join17(this.ensureReservedDir(queue), `${jobId}.queue-data`);
19762
+ return join18(this.ensureReservedDir(queue), `${jobId}.queue-data`);
17708
19763
  }
17709
19764
  nowIso() {
17710
19765
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -17742,7 +19797,7 @@ var init_liteBackend = __esm({
17742
19797
  error: void 0
17743
19798
  };
17744
19799
  const prefix = this.nextPrefix();
17745
- writeFileSync8(join17(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
19800
+ writeFileSync9(join18(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
17746
19801
  return id;
17747
19802
  }
17748
19803
  /**
@@ -17761,7 +19816,7 @@ var init_liteBackend = __esm({
17761
19816
  }
17762
19817
  const candidates = [];
17763
19818
  for (const filename of filenames) {
17764
- const filePath = join17(dir, filename);
19819
+ const filePath = join18(dir, filename);
17765
19820
  let job;
17766
19821
  try {
17767
19822
  job = JSON.parse(readFileSync14(filePath, "utf-8"));
@@ -17804,7 +19859,7 @@ var init_liteBackend = __esm({
17804
19859
  createdAt: job.createdAt ?? now,
17805
19860
  topic: job.topic ?? queue
17806
19861
  };
17807
- writeFileSync8(this.reservedPath(queue, record.id), JSON.stringify(record, null, 2));
19862
+ writeFileSync9(this.reservedPath(queue, record.id), JSON.stringify(record, null, 2));
17808
19863
  }
17809
19864
  /**
17810
19865
  * Return expired reservations to the queue (at-least-once delivery).
@@ -17825,7 +19880,7 @@ var init_liteBackend = __esm({
17825
19880
  return;
17826
19881
  }
17827
19882
  for (const filename of filenames) {
17828
- const filePath = join17(reservedDir, filename);
19883
+ const filePath = join18(reservedDir, filename);
17829
19884
  let record;
17830
19885
  try {
17831
19886
  record = JSON.parse(readFileSync14(filePath, "utf-8"));
@@ -17863,7 +19918,7 @@ var init_liteBackend = __esm({
17863
19918
  this.reclaimExpired(queue, bridge.getMaxRetries(), this.nowIso());
17864
19919
  const now = this.nowIso();
17865
19920
  for (const [filename, job] of this.availableCandidates(queue, now)) {
17866
- const filePath = join17(dir, filename);
19921
+ const filePath = join18(dir, filename);
17867
19922
  job.topic = queue;
17868
19923
  job.priority = job.priority ?? 0;
17869
19924
  this.writeReserved(queue, job);
@@ -17888,7 +19943,7 @@ var init_liteBackend = __esm({
17888
19943
  const results = [];
17889
19944
  for (const [filename, job] of this.availableCandidates(queue, now)) {
17890
19945
  if (results.length >= count) break;
17891
- const filePath = join17(dir, filename);
19946
+ const filePath = join18(dir, filename);
17892
19947
  job.topic = queue;
17893
19948
  job.priority = job.priority ?? 0;
17894
19949
  this.writeReserved(queue, job);
@@ -17941,7 +19996,7 @@ var init_liteBackend = __esm({
17941
19996
  let count = 0;
17942
19997
  for (const file of files) {
17943
19998
  try {
17944
- const job = JSON.parse(readFileSync14(join17(scanDir, file), "utf-8"));
19999
+ const job = JSON.parse(readFileSync14(join18(scanDir, file), "utf-8"));
17945
20000
  if (job.status === status2) count++;
17946
20001
  } catch {
17947
20002
  }
@@ -17954,28 +20009,28 @@ var init_liteBackend = __esm({
17954
20009
  try {
17955
20010
  const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
17956
20011
  for (const file of files) {
17957
- unlinkSync6(join17(dir, file));
20012
+ unlinkSync6(join18(dir, file));
17958
20013
  count++;
17959
20014
  }
17960
20015
  } catch {
17961
20016
  }
17962
- const failedDir = join17(dir, "failed");
20017
+ const failedDir = join18(dir, "failed");
17963
20018
  try {
17964
- if (existsSync15(failedDir)) {
20019
+ if (existsSync16(failedDir)) {
17965
20020
  const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
17966
20021
  for (const file of files) {
17967
- unlinkSync6(join17(failedDir, file));
20022
+ unlinkSync6(join18(failedDir, file));
17968
20023
  count++;
17969
20024
  }
17970
20025
  }
17971
20026
  } catch {
17972
20027
  }
17973
- const reservedDir = join17(dir, "reserved");
20028
+ const reservedDir = join18(dir, "reserved");
17974
20029
  try {
17975
- if (existsSync15(reservedDir)) {
20030
+ if (existsSync16(reservedDir)) {
17976
20031
  const files = readdirSync8(reservedDir).filter((f) => f.endsWith(".queue-data"));
17977
20032
  for (const file of files) {
17978
- unlinkSync6(join17(reservedDir, file));
20033
+ unlinkSync6(join18(reservedDir, file));
17979
20034
  count++;
17980
20035
  }
17981
20036
  }
@@ -17998,7 +20053,7 @@ var init_liteBackend = __esm({
17998
20053
  const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
17999
20054
  for (const file of files) {
18000
20055
  try {
18001
- const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
20056
+ const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
18002
20057
  const attempts = job.attempts || 0;
18003
20058
  if (attempts > 0 && attempts < maxRetries) {
18004
20059
  results.push(job);
@@ -18021,9 +20076,9 @@ var init_liteBackend = __esm({
18021
20076
  try {
18022
20077
  const queues = readdirSync8(this.basePath);
18023
20078
  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)) {
20079
+ const failedDir = join18(this.basePath, q, "failed");
20080
+ const filePath = join18(failedDir, `${jobId}.queue-data`);
20081
+ if (existsSync16(filePath)) {
18027
20082
  const job = JSON.parse(readFileSync14(filePath, "utf-8"));
18028
20083
  job.status = "pending";
18029
20084
  job.attempts = (job.attempts || 0) + 1;
@@ -18031,8 +20086,8 @@ var init_liteBackend = __esm({
18031
20086
  job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
18032
20087
  job.delayUntil = delaySeconds ? new Date(Date.now() + delaySeconds * 1e3).toISOString() : null;
18033
20088
  const prefix = this.nextPrefix();
18034
- const queueDir = join17(this.basePath, q);
18035
- writeFileSync8(join17(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
20089
+ const queueDir = join18(this.basePath, q);
20090
+ writeFileSync9(join18(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
18036
20091
  unlinkSync6(filePath);
18037
20092
  return true;
18038
20093
  }
@@ -18048,7 +20103,7 @@ var init_liteBackend = __esm({
18048
20103
  const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
18049
20104
  for (const file of files) {
18050
20105
  try {
18051
- const job = JSON.parse(readFileSync14(join17(failedDir, file), "utf-8"));
20106
+ const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
18052
20107
  if ((job.attempts || 0) >= maxRetries) {
18053
20108
  job.status = "dead";
18054
20109
  results.push(job);
@@ -18069,7 +20124,7 @@ var init_liteBackend = __esm({
18069
20124
  const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
18070
20125
  for (const file of files) {
18071
20126
  try {
18072
- unlinkSync6(join17(failedDir, file));
20127
+ unlinkSync6(join18(failedDir, file));
18073
20128
  count++;
18074
20129
  } catch {
18075
20130
  }
@@ -18082,9 +20137,9 @@ var init_liteBackend = __esm({
18082
20137
  const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
18083
20138
  for (const file of files) {
18084
20139
  try {
18085
- const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
20140
+ const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
18086
20141
  if (job.status === status2) {
18087
- unlinkSync6(join17(dir, file));
20142
+ unlinkSync6(join18(dir, file));
18088
20143
  count++;
18089
20144
  }
18090
20145
  } catch {
@@ -18108,7 +20163,7 @@ var init_liteBackend = __esm({
18108
20163
  const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data"));
18109
20164
  for (const file of files) {
18110
20165
  try {
18111
- const filePath = join17(failedDir, file);
20166
+ const filePath = join18(failedDir, file);
18112
20167
  const job = JSON.parse(readFileSync14(filePath, "utf-8"));
18113
20168
  if ((job.attempts || 0) >= maxRetries) {
18114
20169
  continue;
@@ -18118,7 +20173,7 @@ var init_liteBackend = __esm({
18118
20173
  job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
18119
20174
  job.delayUntil = null;
18120
20175
  const prefix = this.nextPrefix();
18121
- writeFileSync8(join17(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
20176
+ writeFileSync9(join18(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
18122
20177
  unlinkSync6(filePath);
18123
20178
  count++;
18124
20179
  } catch {
@@ -18138,7 +20193,7 @@ var init_liteBackend = __esm({
18138
20193
  }
18139
20194
  for (const file of files) {
18140
20195
  if (!file.includes(id)) continue;
18141
- const filePath = join17(dir, file);
20196
+ const filePath = join18(dir, file);
18142
20197
  let job;
18143
20198
  try {
18144
20199
  job = JSON.parse(readFileSync14(filePath, "utf-8"));
@@ -18186,7 +20241,7 @@ var init_liteBackend = __esm({
18186
20241
  error
18187
20242
  };
18188
20243
  const prefix = this.nextPrefix();
18189
- writeFileSync8(join17(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
20244
+ writeFileSync9(join18(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
18190
20245
  }
18191
20246
  /**
18192
20247
  * Move the job to the dead-letter (failed/) directory. Terminal until a
@@ -18206,7 +20261,7 @@ var init_liteBackend = __esm({
18206
20261
  error,
18207
20262
  failedAt: (/* @__PURE__ */ new Date()).toISOString()
18208
20263
  };
18209
- writeFileSync8(join17(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
20264
+ writeFileSync9(join18(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
18210
20265
  }
18211
20266
  /**
18212
20267
  * Record a failed attempt.
@@ -18242,7 +20297,7 @@ var init_liteBackend = __esm({
18242
20297
  retryJob(queue, job, delaySeconds) {
18243
20298
  this.clearReservation(queue, job.id);
18244
20299
  try {
18245
- unlinkSync6(join17(this.ensureFailedDir(queue), `${job.id}.queue-data`));
20300
+ unlinkSync6(join18(this.ensureFailedDir(queue), `${job.id}.queue-data`));
18246
20301
  } catch {
18247
20302
  }
18248
20303
  job.attempts = (job.attempts || 0) + 1;
@@ -18661,7 +20716,7 @@ var init_queue = __esm({
18661
20716
  const jobs = this.popBatch(resolvedBatchSize);
18662
20717
  if (jobs.length === 0) {
18663
20718
  if (resolvedPollInterval <= 0) break;
18664
- await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
20719
+ await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
18665
20720
  continue;
18666
20721
  }
18667
20722
  yield jobs;
@@ -18671,7 +20726,7 @@ var init_queue = __esm({
18671
20726
  const raw = this.pop();
18672
20727
  if (raw === null) {
18673
20728
  if (resolvedPollInterval <= 0) break;
18674
- await new Promise((resolve20) => setTimeout(resolve20, resolvedPollInterval));
20729
+ await new Promise((resolve21) => setTimeout(resolve21, resolvedPollInterval));
18675
20730
  continue;
18676
20731
  }
18677
20732
  yield createJob(raw, this);
@@ -20746,8 +22801,8 @@ ${end}
20746
22801
 
20747
22802
  // ../core/src/devAdmin.ts
20748
22803
  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";
22804
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync12, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync14 } from "node:fs";
22805
+ import { join as join22, dirname as dirname10, resolve as resolve12, relative as relative8 } from "node:path";
20751
22806
  import { fileURLToPath as fileURLToPath5 } from "node:url";
20752
22807
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
20753
22808
  function escapeHtml(value) {
@@ -20861,12 +22916,12 @@ function mapQueueJob(job, topic, status2) {
20861
22916
  };
20862
22917
  }
20863
22918
  function readQueueDir(dir, topic, status2) {
20864
- if (!existsSync19(dir)) return [];
22919
+ if (!existsSync20(dir)) return [];
20865
22920
  const jobs = [];
20866
22921
  for (const filename of readdirSync12(dir).sort()) {
20867
22922
  if (!filename.endsWith(".queue-data")) continue;
20868
22923
  try {
20869
- jobs.push(mapQueueJob(JSON.parse(readFileSync18(join21(dir, filename), "utf-8")), topic, status2));
22924
+ jobs.push(mapQueueJob(JSON.parse(readFileSync18(join22(dir, filename), "utf-8")), topic, status2));
20870
22925
  } catch {
20871
22926
  }
20872
22927
  }
@@ -20983,8 +23038,8 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
20983
23038
  function resolveDevEnvVar(key) {
20984
23039
  const live = process.env[key];
20985
23040
  if (live !== void 0 && live !== "") return live;
20986
- const envPath = join21(process.cwd(), ".env");
20987
- if (!existsSync19(envPath)) return "";
23041
+ const envPath = join22(process.cwd(), ".env");
23042
+ if (!existsSync20(envPath)) return "";
20988
23043
  for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
20989
23044
  const t = line.trim();
20990
23045
  if (!t || t.startsWith("#") || !t.includes("=")) continue;
@@ -20994,8 +23049,8 @@ function resolveDevEnvVar(key) {
20994
23049
  return "";
20995
23050
  }
20996
23051
  function upsertDevEnvVar(key, value) {
20997
- const envPath = join21(process.cwd(), ".env");
20998
- const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
23052
+ const envPath = join22(process.cwd(), ".env");
23053
+ const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
20999
23054
  let found = false;
21000
23055
  const out = [];
21001
23056
  for (const line of lines) {
@@ -21010,7 +23065,7 @@ function upsertDevEnvVar(key, value) {
21010
23065
  } else out.push(line);
21011
23066
  }
21012
23067
  if (!found) out.push(`${key}=${value}`);
21013
- writeFileSync12(envPath, out.join("\n").replace(/\n+$/, "") + "\n");
23068
+ writeFileSync13(envPath, out.join("\n").replace(/\n+$/, "") + "\n");
21014
23069
  }
21015
23070
  function formatUptime(seconds) {
21016
23071
  const d = Math.floor(seconds / 86400);
@@ -21025,9 +23080,9 @@ function formatUptime(seconds) {
21025
23080
  return parts.join(" ");
21026
23081
  }
21027
23082
  function parseEnvFile() {
21028
- const envPath = join21(process.cwd(), ".env");
23083
+ const envPath = join22(process.cwd(), ".env");
21029
23084
  const result = {};
21030
- if (!existsSync19(envPath)) return result;
23085
+ if (!existsSync20(envPath)) return result;
21031
23086
  const lines = readFileSync18(envPath, "utf-8").split("\n");
21032
23087
  for (const line of lines) {
21033
23088
  const trimmed = line.trim();
@@ -21039,9 +23094,9 @@ function parseEnvFile() {
21039
23094
  }
21040
23095
  function walkDirRecursive(dir) {
21041
23096
  const results = [];
21042
- if (!existsSync19(dir)) return results;
23097
+ if (!existsSync20(dir)) return results;
21043
23098
  for (const entry of readdirSync12(dir)) {
21044
- const full = join21(dir, entry);
23099
+ const full = join22(dir, entry);
21045
23100
  if (statSync14(full).isDirectory()) {
21046
23101
  results.push(...walkDirRecursive(full));
21047
23102
  } else {
@@ -21058,25 +23113,25 @@ function handleGalleryDeploy(router) {
21058
23113
  res.json({ error: "No gallery item specified" }, 400);
21059
23114
  return;
21060
23115
  }
21061
- const galleryDir = resolve11(__devAdminDirname, "..", "gallery");
21062
- const gallerySrc = join21(galleryDir, name, "src");
21063
- if (!existsSync19(gallerySrc)) {
23116
+ const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
23117
+ const gallerySrc = join22(galleryDir, name, "src");
23118
+ if (!existsSync20(gallerySrc)) {
21064
23119
  res.json({ error: `Gallery item '${name}' not found` }, 404);
21065
23120
  return;
21066
23121
  }
21067
- const projectSrc = resolve11(process.cwd(), "src");
23122
+ const projectSrc = resolve12(process.cwd(), "src");
21068
23123
  const copied = [];
21069
23124
  const allFiles = walkDirRecursive(gallerySrc);
21070
23125
  for (const srcFile of allFiles) {
21071
- const rel = relative7(gallerySrc, srcFile);
21072
- const dest = join21(projectSrc, rel);
21073
- mkdirSync13(dirname10(dest), { recursive: true });
23126
+ const rel = relative8(gallerySrc, srcFile);
23127
+ const dest = join22(projectSrc, rel);
23128
+ mkdirSync14(dirname10(dest), { recursive: true });
21074
23129
  copyFileSync(srcFile, dest);
21075
23130
  copied.push(rel);
21076
23131
  }
21077
23132
  try {
21078
- const routesDir = resolve11(process.cwd(), "src", "routes");
21079
- if (existsSync19(routesDir)) {
23133
+ const routesDir = resolve12(process.cwd(), "src", "routes");
23134
+ if (existsSync20(routesDir)) {
21080
23135
  const { discoverRoutes: discoverRoutes2 } = await Promise.resolve().then(() => (init_routeDiscovery(), routeDiscovery_exports));
21081
23136
  const routes = await discoverRoutes2(routesDir);
21082
23137
  for (const route of routes) {
@@ -21092,7 +23147,7 @@ function handleGalleryDeploy(router) {
21092
23147
  };
21093
23148
  }
21094
23149
  function safeJoin(projectRoot3, rel) {
21095
- const resolved = resolve11(projectRoot3, rel);
23150
+ const resolved = resolve12(projectRoot3, rel);
21096
23151
  if (!resolved.startsWith(projectRoot3)) return null;
21097
23152
  return resolved;
21098
23153
  }
@@ -22055,16 +24110,16 @@ var init_devAdmin = __esm({
22055
24110
  failed: queue.size("failed"),
22056
24111
  reserved: queue.size("reserved")
22057
24112
  };
22058
- const topicDir = join21(queueBasePath2(), topic);
24113
+ const topicDir = join22(queueBasePath2(), topic);
22059
24114
  const jobs = [];
22060
24115
  if (!statusFilter || statusFilter === "pending") {
22061
24116
  jobs.push(...readQueueDir(topicDir, topic, "pending"));
22062
24117
  }
22063
24118
  if (!statusFilter || statusFilter === "reserved") {
22064
- jobs.push(...readQueueDir(join21(topicDir, "reserved"), topic, "reserved"));
24119
+ jobs.push(...readQueueDir(join22(topicDir, "reserved"), topic, "reserved"));
22065
24120
  }
22066
24121
  if (!statusFilter || statusFilter === "failed" || statusFilter === "dead") {
22067
- jobs.push(...readQueueDir(join21(topicDir, "failed"), topic, "dead_letter"));
24122
+ jobs.push(...readQueueDir(join22(topicDir, "failed"), topic, "dead_letter"));
22068
24123
  }
22069
24124
  res.json({ stats, jobs });
22070
24125
  } catch (e) {
@@ -22080,10 +24135,10 @@ var init_devAdmin = __esm({
22080
24135
  const { queueBasePath: queueBasePath2 } = await Promise.resolve().then(() => (init_queue(), queue_exports));
22081
24136
  const queueDir = queueBasePath2();
22082
24137
  let topics = [];
22083
- if (existsSync19(queueDir)) {
24138
+ if (existsSync20(queueDir)) {
22084
24139
  topics = readdirSync12(queueDir).filter((d) => {
22085
24140
  try {
22086
- return statSync14(join21(queueDir, d)).isDirectory();
24141
+ return statSync14(join22(queueDir, d)).isDirectory();
22087
24142
  } catch {
22088
24143
  return false;
22089
24144
  }
@@ -22398,7 +24453,7 @@ var init_devAdmin = __esm({
22398
24453
  const count = parseInt(String(body.count ?? "10"), 10) || 10;
22399
24454
  try {
22400
24455
  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));
24456
+ const dirs = ["src/orm", "src/models"].map((d) => resolve12(process.cwd(), d)).filter((d) => existsSync20(d));
22402
24457
  const classes = [];
22403
24458
  for (const dir of dirs) {
22404
24459
  for (const m of await orm.discoverModels(dir)) classes.push(m.modelClass);
@@ -22425,7 +24480,7 @@ var init_devAdmin = __esm({
22425
24480
  const run = promisify(execFile);
22426
24481
  try {
22427
24482
  const { stdout, stderr } = await run("npm", ["test"], {
22428
- cwd: resolve11(process.cwd()),
24483
+ cwd: resolve12(process.cwd()),
22429
24484
  timeout: 18e4,
22430
24485
  encoding: "utf-8",
22431
24486
  maxBuffer: 8 * 1024 * 1024
@@ -22539,8 +24594,8 @@ var init_devAdmin = __esm({
22539
24594
  return;
22540
24595
  }
22541
24596
  try {
22542
- const envPath = join21(process.cwd(), ".env");
22543
- const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
24597
+ const envPath = join22(process.cwd(), ".env");
24598
+ const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
22544
24599
  const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
22545
24600
  const newLines = [];
22546
24601
  for (const line of lines) {
@@ -22567,7 +24622,7 @@ var init_devAdmin = __esm({
22567
24622
  for (const [key, found] of Object.entries(keysFound)) {
22568
24623
  if (!found) newLines.push(`${key}=${values[key]}`);
22569
24624
  }
22570
- writeFileSync12(envPath, newLines.join("\n") + "\n");
24625
+ writeFileSync13(envPath, newLines.join("\n") + "\n");
22571
24626
  res.json({ success: true });
22572
24627
  } catch (e) {
22573
24628
  const msg = e instanceof Error ? e.message : String(e);
@@ -22577,26 +24632,26 @@ var init_devAdmin = __esm({
22577
24632
  __devAdminFilename = fileURLToPath5(import.meta.url);
22578
24633
  __devAdminDirname = dirname10(__devAdminFilename);
22579
24634
  handleGalleryList = (_req, res) => {
22580
- const galleryDir = resolve11(__devAdminDirname, "..", "gallery");
24635
+ const galleryDir = resolve12(__devAdminDirname, "..", "gallery");
22581
24636
  const items = [];
22582
- if (existsSync19(galleryDir)) {
24637
+ if (existsSync20(galleryDir)) {
22583
24638
  const entries = readdirSync12(galleryDir).sort();
22584
24639
  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)) {
24640
+ const entryPath = join22(galleryDir, entry);
24641
+ const metaFile = join22(entryPath, "meta.json");
24642
+ if (statSync14(entryPath).isDirectory() && existsSync20(metaFile)) {
22588
24643
  try {
22589
24644
  const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
22590
24645
  meta.id = entry;
22591
- const srcDir = join21(entryPath, "src");
22592
- if (existsSync19(srcDir)) {
24646
+ const srcDir = join22(entryPath, "src");
24647
+ if (existsSync20(srcDir)) {
22593
24648
  const allFiles = walkDirRecursive(srcDir);
22594
- meta.files = allFiles.map((f) => relative7(srcDir, f));
24649
+ meta.files = allFiles.map((f) => relative8(srcDir, f));
22595
24650
  }
22596
- const projectSrc = resolve11(process.cwd(), "src");
22597
- if (existsSync19(srcDir) && meta.files) {
24651
+ const projectSrc = resolve12(process.cwd(), "src");
24652
+ if (existsSync20(srcDir) && meta.files) {
22598
24653
  meta.deployed = meta.files.every(
22599
- (f) => existsSync19(join21(projectSrc, f))
24654
+ (f) => existsSync20(join22(projectSrc, f))
22600
24655
  );
22601
24656
  } else {
22602
24657
  meta.deployed = false;
@@ -22692,10 +24747,10 @@ var init_devAdmin = __esm({
22692
24747
  handleFiles = async (req2, res) => {
22693
24748
  const url = new URL(req2.url ?? "/", "http://localhost");
22694
24749
  const rel = url.searchParams.get("path") ?? ".";
22695
- const root = resolve11(process.cwd());
24750
+ const root = resolve12(process.cwd());
22696
24751
  const target = safeJoin(root, rel);
22697
24752
  const { branch, gitRoot, status: gitStatus } = await devGitInfo(root);
22698
- if (!target || !existsSync19(target) || !statSync14(target).isDirectory()) {
24753
+ if (!target || !existsSync20(target) || !statSync14(target).isDirectory()) {
22699
24754
  res.json({ path: rel, branch, entries: [], error: "not a directory" });
22700
24755
  return;
22701
24756
  }
@@ -22708,8 +24763,8 @@ var init_devAdmin = __esm({
22708
24763
  const entries = [];
22709
24764
  for (const name of readdirSync12(target).sort()) {
22710
24765
  if (devFilesHidden(name)) continue;
22711
- const full = join21(target, name);
22712
- const entryRel = relative7(root, full).replace(/\\/g, "/");
24766
+ const full = join22(target, name);
24767
+ const entryRel = relative8(root, full).replace(/\\/g, "/");
22713
24768
  if (isSecretPath(entryRel)) continue;
22714
24769
  let isDir = false;
22715
24770
  let size = null;
@@ -22754,7 +24809,7 @@ var init_devAdmin = __esm({
22754
24809
  size
22755
24810
  });
22756
24811
  }
22757
- res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
24812
+ res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
22758
24813
  };
22759
24814
  DEV_ADMIN_LANG_MAP = {
22760
24815
  ".py": "python",
@@ -22799,15 +24854,15 @@ var init_devAdmin = __esm({
22799
24854
  res.json({ error: "Refused: secret file", path: rel, content: "", language: "text", bytes: 0 }, 403);
22800
24855
  return;
22801
24856
  }
22802
- const root = resolve11(process.cwd());
24857
+ const root = resolve12(process.cwd());
22803
24858
  const target = safeJoin(root, rel);
22804
- if (!target || !existsSync19(target) || !statSync14(target).isFile()) {
24859
+ if (!target || !existsSync20(target) || !statSync14(target).isFile()) {
22805
24860
  res.json({ error: `File not found: ${rel}` }, 404);
22806
24861
  return;
22807
24862
  }
22808
24863
  try {
22809
24864
  const content = readFileSync18(target, "utf-8");
22810
- const path8 = relative7(root, target);
24865
+ const path8 = relative8(root, target);
22811
24866
  res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
22812
24867
  } catch (e) {
22813
24868
  res.json({ error: e.message }, 500);
@@ -22817,22 +24872,22 @@ var init_devAdmin = __esm({
22817
24872
  const body = req2.body || {};
22818
24873
  const rel = body.path || "";
22819
24874
  const content = body.content ?? "";
22820
- const root = resolve11(process.cwd());
24875
+ const root = resolve12(process.cwd());
22821
24876
  const target = safeJoin(root, rel);
22822
24877
  if (!target) {
22823
24878
  res.json({ error: `Path escapes project directory: ${rel}` }, 400);
22824
24879
  return;
22825
24880
  }
22826
24881
  try {
22827
- mkdirSync13(dirname10(target), { recursive: true });
22828
- const existed = existsSync19(target);
22829
- writeFileSync12(target, content, "utf-8");
24882
+ mkdirSync14(dirname10(target), { recursive: true });
24883
+ const existed = existsSync20(target);
24884
+ writeFileSync13(target, content, "utf-8");
22830
24885
  try {
22831
24886
  const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
22832
- Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
24887
+ Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
22833
24888
  } catch {
22834
24889
  }
22835
- res.json({ ok: true, path: relative7(root, target), bytes: Buffer.byteLength(content, "utf-8") });
24890
+ res.json({ ok: true, path: relative8(root, target), bytes: Buffer.byteLength(content, "utf-8") });
22836
24891
  } catch (e) {
22837
24892
  res.json({ error: e.message }, 500);
22838
24893
  }
@@ -22844,9 +24899,9 @@ var init_devAdmin = __esm({
22844
24899
  res.json({ error: "Refused: secret file" }, 403);
22845
24900
  return;
22846
24901
  }
22847
- const root = resolve11(process.cwd());
24902
+ const root = resolve12(process.cwd());
22848
24903
  const target = safeJoin(root, rel);
22849
- if (!target || !existsSync19(target) || !statSync14(target).isFile()) {
24904
+ if (!target || !existsSync20(target) || !statSync14(target).isFile()) {
22850
24905
  res.raw.writeHead(404);
22851
24906
  res.raw.end("Not found");
22852
24907
  return;
@@ -22879,22 +24934,22 @@ var init_devAdmin = __esm({
22879
24934
  const body = req2.body || {};
22880
24935
  const from = body.from || "";
22881
24936
  const to = body.to || "";
22882
- const root = resolve11(process.cwd());
24937
+ const root = resolve12(process.cwd());
22883
24938
  const src = safeJoin(root, from);
22884
24939
  const dst = safeJoin(root, to);
22885
24940
  if (!src || !dst) {
22886
24941
  res.json({ error: "Invalid path" }, 400);
22887
24942
  return;
22888
24943
  }
22889
- if (!existsSync19(src)) {
24944
+ if (!existsSync20(src)) {
22890
24945
  res.json({ error: `Source not found: ${from}` }, 404);
22891
24946
  return;
22892
24947
  }
22893
24948
  try {
22894
24949
  const { renameSync: renameSync3 } = await import("node:fs");
22895
- mkdirSync13(dirname10(dst), { recursive: true });
24950
+ mkdirSync14(dirname10(dst), { recursive: true });
22896
24951
  renameSync3(src, dst);
22897
- res.json({ ok: true, from: relative7(root, src), to: relative7(root, dst) });
24952
+ res.json({ ok: true, from: relative8(root, src), to: relative8(root, dst) });
22898
24953
  } catch (e) {
22899
24954
  res.json({ error: e.message }, 500);
22900
24955
  }
@@ -22902,20 +24957,20 @@ var init_devAdmin = __esm({
22902
24957
  handleFileDelete = async (req2, res) => {
22903
24958
  const body = req2.body || {};
22904
24959
  const rel = body.path || "";
22905
- const root = resolve11(process.cwd());
24960
+ const root = resolve12(process.cwd());
22906
24961
  const target = safeJoin(root, rel);
22907
24962
  if (!target) {
22908
24963
  res.json({ error: "Invalid path" }, 400);
22909
24964
  return;
22910
24965
  }
22911
- if (!existsSync19(target)) {
24966
+ if (!existsSync20(target)) {
22912
24967
  res.json({ error: `Not found: ${rel}` }, 404);
22913
24968
  return;
22914
24969
  }
22915
24970
  try {
22916
24971
  const { rmSync } = await import("node:fs");
22917
24972
  rmSync(target, { recursive: true, force: true });
22918
- res.json({ ok: true, deleted: relative7(root, target) });
24973
+ res.json({ ok: true, deleted: relative8(root, target) });
22919
24974
  } catch (e) {
22920
24975
  res.json({ error: e.message }, 500);
22921
24976
  }
@@ -22953,7 +25008,7 @@ var init_devAdmin = __esm({
22953
25008
  const { execFileSync: execFileSync7 } = await import("node:child_process");
22954
25009
  const args = ["install", dev ? "--save-dev" : "--save", pkg];
22955
25010
  const output = execFileSync7("npm", args, {
22956
- cwd: resolve11(process.cwd()),
25011
+ cwd: resolve12(process.cwd()),
22957
25012
  timeout: 12e4,
22958
25013
  encoding: "utf-8"
22959
25014
  }).toString();
@@ -22965,7 +25020,7 @@ var init_devAdmin = __esm({
22965
25020
  handleGitStatus = async (_req, res) => {
22966
25021
  try {
22967
25022
  const { execFileSync: execFileSync7 } = await import("node:child_process");
22968
- const cwd = resolve11(process.cwd());
25023
+ const cwd = resolve12(process.cwd());
22969
25024
  try {
22970
25025
  execFileSync7("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 3e3 });
22971
25026
  } catch {
@@ -23104,7 +25159,7 @@ var init_devAdmin = __esm({
23104
25159
  try {
23105
25160
  const { execFileSync: execFileSync7 } = await import("node:child_process");
23106
25161
  const output = execFileSync7("npx", ["tina4nodejs", "generate", kind, name], {
23107
- cwd: resolve11(process.cwd()),
25162
+ cwd: resolve12(process.cwd()),
23108
25163
  timeout: 3e4,
23109
25164
  encoding: "utf-8"
23110
25165
  }).toString();
@@ -23249,21 +25304,21 @@ var init_devAdmin = __esm({
23249
25304
  });
23250
25305
  };
23251
25306
  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");
25307
+ const { readFileSync: readFileSync27, existsSync: existsSync28 } = await import("node:fs");
25308
+ const { dirname: dirname15, join: join33, resolve: resolve21 } = await import("node:path");
23254
25309
  const { fileURLToPath: fileURLToPath8 } = await import("node:url");
23255
25310
  const dir = dirname15(fileURLToPath8(import.meta.url));
23256
25311
  const candidates = [
23257
- join32(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
25312
+ join33(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
23258
25313
  // src/../public/js/
23259
- join32(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
25314
+ join33(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
23260
25315
  // 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")
25316
+ resolve21(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
25317
+ resolve21(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
23263
25318
  // project public/
23264
25319
  ];
23265
25320
  for (const jsPath of candidates) {
23266
- if (existsSync27(jsPath)) {
25321
+ if (existsSync28(jsPath)) {
23267
25322
  try {
23268
25323
  const content = readFileSync27(jsPath, "utf-8");
23269
25324
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
@@ -23288,8 +25343,8 @@ var init_devAdmin = __esm({
23288
25343
  });
23289
25344
 
23290
25345
  // ../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";
25346
+ import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync21 } from "node:fs";
25347
+ import { join as join23, resolve as resolve13 } from "node:path";
23293
25348
  var I18n;
23294
25349
  var init_i18n = __esm({
23295
25350
  "../core/src/i18n.ts"() {
@@ -23309,7 +25364,7 @@ var init_i18n = __esm({
23309
25364
  * (BUG-7, BREAKING in 3.13.x — was previously (localeDir, defaultLocale)).
23310
25365
  */
23311
25366
  constructor(locale, path8) {
23312
- this._localeDir = resolve12(
25367
+ this._localeDir = resolve13(
23313
25368
  path8 ?? process.env.TINA4_LOCALE_DIR ?? "src/locales"
23314
25369
  );
23315
25370
  this._defaultLocale = locale ?? process.env.TINA4_LOCALE ?? "en";
@@ -23366,7 +25421,7 @@ var init_i18n = __esm({
23366
25421
  }
23367
25422
  /** List available locale codes based on JSON files in the locale directory. */
23368
25423
  availableLocales() {
23369
- if (!existsSync20(this._localeDir)) {
25424
+ if (!existsSync21(this._localeDir)) {
23370
25425
  return [this._defaultLocale];
23371
25426
  }
23372
25427
  try {
@@ -23382,8 +25437,8 @@ var init_i18n = __esm({
23382
25437
  if (this._translations.has(locale)) {
23383
25438
  return;
23384
25439
  }
23385
- const filePath = join22(this._localeDir, `${locale}.json`);
23386
- if (existsSync20(filePath)) {
25440
+ const filePath = join23(this._localeDir, `${locale}.json`);
25441
+ if (existsSync21(filePath)) {
23387
25442
  try {
23388
25443
  const raw = readFileSync19(filePath, "utf-8");
23389
25444
  const data = JSON.parse(raw);
@@ -23395,8 +25450,8 @@ var init_i18n = __esm({
23395
25450
  }
23396
25451
  }
23397
25452
  for (const ext of [".yml", ".yaml"]) {
23398
- const yamlPath = join22(this._localeDir, `${locale}${ext}`);
23399
- if (existsSync20(yamlPath)) {
25453
+ const yamlPath = join23(this._localeDir, `${locale}${ext}`);
25454
+ if (existsSync21(yamlPath)) {
23400
25455
  try {
23401
25456
  const raw = readFileSync19(yamlPath, "utf-8");
23402
25457
  const data = _I18n._parseSimpleYaml(raw);
@@ -23741,7 +25796,7 @@ function sanitizeSecurity(reqs, schemes) {
23741
25796
  return clean;
23742
25797
  });
23743
25798
  }
23744
- function generate(routes, models = []) {
25799
+ function generate2(routes, models = []) {
23745
25800
  const info = {
23746
25801
  title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
23747
25802
  // The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
@@ -24189,7 +26244,7 @@ __export(src_exports, {
24189
26244
  addSchema: () => addSchema,
24190
26245
  addSecurityScheme: () => addSecurityScheme,
24191
26246
  createSwaggerRoutes: () => createSwaggerRoutes,
24192
- generate: () => generate,
26247
+ generate: () => generate2,
24193
26248
  resetRegistry: () => resetRegistry,
24194
26249
  swaggerEnabled: () => swaggerEnabled
24195
26250
  });
@@ -24543,8 +26598,8 @@ function writeMcpDiscovery(projectRoot3, port) {
24543
26598
  const lines = contents.split(/\r?\n/);
24544
26599
  const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
24545
26600
  if (!already) {
24546
- const sep6 = contents.endsWith("\n") || contents === "" ? "" : "\n";
24547
- fs8.writeFileSync(gitignorePath, `${contents}${sep6}${GITIGNORE_LINE}
26601
+ const sep7 = contents.endsWith("\n") || contents === "" ? "" : "\n";
26602
+ fs8.writeFileSync(gitignorePath, `${contents}${sep7}${GITIGNORE_LINE}
24548
26603
  `, "utf-8");
24549
26604
  }
24550
26605
  }
@@ -24563,8 +26618,8 @@ var init_docsAutoDiscovery = __esm({
24563
26618
  // ../core/src/server.ts
24564
26619
  import { createServer as createServer2 } from "node:http";
24565
26620
  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";
26621
+ import { resolve as resolve15, dirname as dirname11, join as join25, relative as relative9 } from "node:path";
26622
+ import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
24568
26623
  import { isatty } from "node:tty";
24569
26624
  import { fileURLToPath as fileURLToPath6 } from "node:url";
24570
26625
  import { execFileSync as execFileSync3, exec } from "node:child_process";
@@ -24602,8 +26657,8 @@ function swaggerAdvertised() {
24602
26657
  return TRUTHY2.includes(raw);
24603
26658
  }
24604
26659
  async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
24605
- const dir = resolve14(base, migrationDir);
24606
- if (!existsSync22(dir)) return;
26660
+ const dir = resolve15(base, migrationDir);
26661
+ if (!existsSync23(dir)) return;
24607
26662
  let hasSql = false;
24608
26663
  try {
24609
26664
  hasSql = readdirSync14(dir).some((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"));
@@ -24744,12 +26799,12 @@ async function renderErrorPage(code, data, templatesDir) {
24744
26799
  }
24745
26800
  return instance;
24746
26801
  };
24747
- const userTemplatePath = join24(templatesDir, templateFile);
24748
- if (existsSync22(userTemplatePath)) {
26802
+ const userTemplatePath = join25(templatesDir, templateFile);
26803
+ if (existsSync23(userTemplatePath)) {
24749
26804
  return getCachedFrond(templatesDir).render(templateFile, data);
24750
26805
  }
24751
- const builtinTemplatePath = join24(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
24752
- if (existsSync22(builtinTemplatePath)) {
26806
+ const builtinTemplatePath = join25(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
26807
+ if (existsSync23(builtinTemplatePath)) {
24753
26808
  return getCachedFrond(BUILTIN_ERROR_TEMPLATES_DIR).render(templateFile, data);
24754
26809
  }
24755
26810
  return null;
@@ -24766,29 +26821,29 @@ function injectDevToolbar(html, ctx) {
24766
26821
  }
24767
26822
  function walkGalleryFiles(dir) {
24768
26823
  const results = [];
24769
- if (!existsSync22(dir)) return results;
26824
+ if (!existsSync23(dir)) return results;
24770
26825
  for (const f of readdirSync14(dir)) {
24771
- const full = join24(dir, f);
26826
+ const full = join25(dir, f);
24772
26827
  if (statSync15(full).isDirectory()) results.push(...walkGalleryFiles(full));
24773
26828
  else results.push(full);
24774
26829
  }
24775
26830
  return results;
24776
26831
  }
24777
26832
  function getGalleryDeployedState() {
24778
- const galleryDir = resolve14(__dirname, "..", "gallery");
26833
+ const galleryDir = resolve15(__dirname, "..", "gallery");
24779
26834
  const state = {};
24780
- if (!existsSync22(galleryDir)) return state;
26835
+ if (!existsSync23(galleryDir)) return state;
24781
26836
  try {
24782
26837
  const entries = readdirSync14(galleryDir).sort();
24783
26838
  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)) {
26839
+ const entryPath = join25(galleryDir, entry);
26840
+ const metaFile = join25(entryPath, "meta.json");
26841
+ if (statSync15(entryPath).isDirectory() && existsSync23(metaFile)) {
26842
+ const srcDir = join25(entryPath, "src");
26843
+ if (existsSync23(srcDir)) {
24789
26844
  const files = walkGalleryFiles(srcDir);
24790
- const projectSrc = resolve14(process.cwd(), "src");
24791
- state[entry] = files.every((f) => existsSync22(join24(projectSrc, relative8(srcDir, f))));
26845
+ const projectSrc = resolve15(process.cwd(), "src");
26846
+ state[entry] = files.every((f) => existsSync23(join25(projectSrc, relative9(srcDir, f))));
24792
26847
  } else {
24793
26848
  state[entry] = false;
24794
26849
  }
@@ -24816,9 +26871,9 @@ function resolveTemplate(pathname, templatesDir) {
24816
26871
  const isDev2 = (process.env.TINA4_DEBUG ?? "false").toLowerCase() === "true";
24817
26872
  if (isDev2) {
24818
26873
  if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
24819
- const pagesDir = resolve14(templatesDir, TEMPLATE_PAGES_DIR);
26874
+ const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
24820
26875
  for (const ext of [".twig", ".html"]) {
24821
- if (existsSync22(resolve14(pagesDir, cleanPath + ext))) {
26876
+ if (existsSync23(resolve15(pagesDir, cleanPath + ext))) {
24822
26877
  return `${TEMPLATE_PAGES_DIR}/${cleanPath}${ext}`;
24823
26878
  }
24824
26879
  }
@@ -24826,14 +26881,14 @@ function resolveTemplate(pathname, templatesDir) {
24826
26881
  }
24827
26882
  if (!templateCache) {
24828
26883
  templateCache = /* @__PURE__ */ new Map();
24829
- const pagesDir = resolve14(templatesDir, TEMPLATE_PAGES_DIR);
24830
- if (existsSync22(pagesDir)) {
26884
+ const pagesDir = resolve15(templatesDir, TEMPLATE_PAGES_DIR);
26885
+ if (existsSync23(pagesDir)) {
24831
26886
  const scan = (dir, prefix) => {
24832
26887
  for (const entry of readdirSync14(dir, { withFileTypes: true })) {
24833
26888
  if (entry.name.startsWith("_")) continue;
24834
26889
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
24835
26890
  if (entry.isDirectory()) {
24836
- scan(resolve14(dir, entry.name), rel);
26891
+ scan(resolve15(dir, entry.name), rel);
24837
26892
  } else if (entry.name.endsWith(".twig") || entry.name.endsWith(".html")) {
24838
26893
  const urlPath = rel.replace(/\.(twig|html)$/, "");
24839
26894
  if (!templateCache.has(urlPath)) {
@@ -25238,7 +27293,7 @@ function serveTemplateFallback(ctx) {
25238
27293
  if ((ctx.req.method ?? "GET") !== "GET") return false;
25239
27294
  const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
25240
27295
  if (!tplFile) return false;
25241
- const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(resolve14(ctx.templatesDir, tplFile), "utf-8");
27296
+ const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(resolve15(ctx.templatesDir, tplFile), "utf-8");
25242
27297
  ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
25243
27298
  ctx.res.raw.end(html);
25244
27299
  return true;
@@ -25277,9 +27332,9 @@ function serveMethodNotAllowed(ctx) {
25277
27332
  }
25278
27333
  function serveStaticAsset(ctx) {
25279
27334
  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;
27335
+ if (custom && existsSync23(custom) && tryServeStatic(custom, ctx.req, ctx.res)) return true;
27336
+ if (existsSync23(ctx.staticDir) && tryServeStatic(ctx.staticDir, ctx.req, ctx.res)) return true;
27337
+ if (existsSync23(ctx.srcPublicDir) && tryServeStatic(ctx.srcPublicDir, ctx.req, ctx.res)) return true;
25283
27338
  if (ctx.swaggerAssetsEnabled || !isSwaggerAssetPath(ctx.pathname)) {
25284
27339
  if (tryServeStatic(BUILTIN_PUBLIC_DIR, ctx.req, ctx.res)) return true;
25285
27340
  }
@@ -25303,10 +27358,10 @@ async function serveNotFound(ctx) {
25303
27358
  return true;
25304
27359
  }
25305
27360
  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");
27361
+ const root = base ? resolve15(base) : process.cwd();
27362
+ const staticDir = resolve15(root, "public");
27363
+ const srcPublicDir = resolve15(root, "src/public");
27364
+ const templatesDir = resolve15(root, "src/templates");
25310
27365
  let frondEngine = null;
25311
27366
  try {
25312
27367
  const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
@@ -25470,13 +27525,13 @@ ${reset2}
25470
27525
  };
25471
27526
  }
25472
27527
  }
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");
27528
+ const base = config?.basePath ? resolve15(config.basePath) : process.cwd();
27529
+ const routesDir = resolve15(base, config?.routesDir ?? "src/routes");
27530
+ const modelsDir = resolve15(base, config?.modelsDir ?? "src/models");
27531
+ const ormDir = resolve15(base, "src/orm");
27532
+ const staticDir = resolve15(base, config?.staticDir ?? "public");
27533
+ const srcPublicDir = resolve15(base, "src/public");
27534
+ const templatesDir = resolve15(base, config?.templatesDir ?? "src/templates");
25480
27535
  const router = new Router();
25481
27536
  const middleware = new MiddlewareChain();
25482
27537
  globalThis.__tina4_router = router;
@@ -25510,8 +27565,8 @@ ${reset2}
25510
27565
  } catch {
25511
27566
  }
25512
27567
  if (frondEngine) {
25513
- const localeDir = resolve14(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
25514
- if (existsSync22(localeDir)) {
27568
+ const localeDir = resolve15(base, process.env.TINA4_LOCALE_DIR ?? "src/locales");
27569
+ if (existsSync23(localeDir)) {
25515
27570
  try {
25516
27571
  const localeFiles = readdirSync14(localeDir).filter((f) => f.endsWith(".json"));
25517
27572
  if (localeFiles.length > 0 && !frondEngine.globals?.t) {
@@ -25526,7 +27581,7 @@ ${reset2}
25526
27581
  middleware.use(requestLogger());
25527
27582
  middleware.use(rateLimiter());
25528
27583
  MiddlewareRunner.use(SecurityHeadersMiddleware);
25529
- if (existsSync22(routesDir)) {
27584
+ if (existsSync23(routesDir)) {
25530
27585
  const routes = await discoverRoutes(routesDir);
25531
27586
  for (const route of routes) {
25532
27587
  router.addRoute(route);
@@ -25546,8 +27601,8 @@ ${reset2}
25546
27601
  console.log(`
25547
27602
  \x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
25548
27603
  }
25549
- const hasOrmDir = existsSync22(ormDir);
25550
- const hasModelsDir = existsSync22(modelsDir);
27604
+ const hasOrmDir = existsSync23(ormDir);
27605
+ const hasModelsDir = existsSync23(modelsDir);
25551
27606
  if (hasOrmDir || hasModelsDir) {
25552
27607
  try {
25553
27608
  const orm = await Promise.resolve().then(() => (init_index(), index_exports));
@@ -25604,7 +27659,7 @@ ${reset2}
25604
27659
  let modelDefs = [];
25605
27660
  try {
25606
27661
  const orm = await Promise.resolve().then(() => (init_index(), index_exports));
25607
- const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync22(d));
27662
+ const allModelDirs = [ormDir, modelsDir].filter((d) => existsSync23(d));
25608
27663
  const seenTables = /* @__PURE__ */ new Set();
25609
27664
  for (const dir of allModelDirs) {
25610
27665
  const discovered = await orm.discoverModels(dir);
@@ -25839,8 +27894,8 @@ var init_server = __esm({
25839
27894
  init_version();
25840
27895
  __filename = fileURLToPath6(import.meta.url);
25841
27896
  __dirname = dirname11(__filename);
25842
- BUILTIN_ERROR_TEMPLATES_DIR = resolve14(__dirname, "..", "templates");
25843
- BUILTIN_PUBLIC_DIR = resolve14(__dirname, "..", "public");
27897
+ BUILTIN_ERROR_TEMPLATES_DIR = resolve15(__dirname, "..", "templates");
27898
+ BUILTIN_PUBLIC_DIR = resolve15(__dirname, "..", "public");
25844
27899
  swaggerAssetsEnabled = false;
25845
27900
  DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 30;
25846
27901
  frondCache = /* @__PURE__ */ new Map();
@@ -25996,8 +28051,8 @@ var init_env = __esm({
25996
28051
 
25997
28052
  // ../core/src/fakeData.ts
25998
28053
  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";
28054
+ import { existsSync as existsSync24, readdirSync as readdirSync15 } from "node:fs";
28055
+ import { resolve as resolve16, join as join26 } from "node:path";
26001
28056
  function mulberry32(seed) {
26002
28057
  let s = seed | 0;
26003
28058
  return () => {
@@ -26431,12 +28486,12 @@ var init_fakeData = __esm({
26431
28486
  * Returns an array of executed file paths.
26432
28487
  */
26433
28488
  async seedDir(seedDir) {
26434
- const dir = resolve15(seedDir ?? "src/seeds");
26435
- if (!existsSync23(dir)) return [];
28489
+ const dir = resolve16(seedDir ?? "src/seeds");
28490
+ if (!existsSync24(dir)) return [];
26436
28491
  const files = readdirSync15(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")).sort();
26437
28492
  const executed = [];
26438
28493
  for (const file of files) {
26439
- const fullPath = join25(dir, file);
28494
+ const fullPath = join26(dir, file);
26440
28495
  try {
26441
28496
  const mod = await import(fullPath);
26442
28497
  if (typeof mod.default === "function") {
@@ -26535,7 +28590,7 @@ var init_mqttMessage = __esm({
26535
28590
  import net2 from "node:net";
26536
28591
  import tls from "node:tls";
26537
28592
  import { randomBytes as randomBytes7 } from "node:crypto";
26538
- import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
28593
+ import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
26539
28594
  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
28595
  var init_mqtt = __esm({
26541
28596
  "../core/src/mqtt.ts"() {
@@ -26738,7 +28793,7 @@ var init_mqtt = __esm({
26738
28793
  */
26739
28794
  async connect() {
26740
28795
  this.closeSocket();
26741
- if (this.secure && this.tlsVerify && this.caFile && !existsSync24(this.caFile)) {
28796
+ if (this.secure && this.tlsVerify && this.caFile && !existsSync25(this.caFile)) {
26742
28797
  throw new MqttError(
26743
28798
  `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
28799
  );
@@ -26975,7 +29030,7 @@ var init_mqtt = __esm({
26975
29030
  * a later client.
26976
29031
  */
26977
29032
  openSocket() {
26978
- return new Promise((resolve20, reject) => {
29033
+ return new Promise((resolve21, reject) => {
26979
29034
  let settled = false;
26980
29035
  const settle = (fn) => {
26981
29036
  if (settled) return;
@@ -27001,9 +29056,9 @@ var init_mqtt = __esm({
27001
29056
  rejectUnauthorized: this.tlsVerify
27002
29057
  };
27003
29058
  if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
27004
- sock = tls.connect(opts, () => settle(() => resolve20(sock)));
29059
+ sock = tls.connect(opts, () => settle(() => resolve21(sock)));
27005
29060
  } else {
27006
- sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
29061
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
27007
29062
  }
27008
29063
  sock.once("error", (err) => {
27009
29064
  settle(() => {
@@ -27042,13 +29097,13 @@ var init_mqtt = __esm({
27042
29097
  writePacket(header, body) {
27043
29098
  if (this.socket === null) return Promise.reject(new MqttError("not connected to an MQTT broker"));
27044
29099
  const packet = Buffer.concat([Buffer.from([header]), _Mqtt.encodeRemainingLength(body.length), body]);
27045
- return new Promise((resolve20, reject) => {
29100
+ return new Promise((resolve21, reject) => {
27046
29101
  this.socket.write(packet, (err) => {
27047
29102
  if (err) {
27048
29103
  reject(new MqttError(`MQTT write failed: ${err.message}`));
27049
29104
  } else {
27050
29105
  this.lastWriteAt = Date.now();
27051
- resolve20();
29106
+ resolve21();
27052
29107
  }
27053
29108
  });
27054
29109
  });
@@ -27081,7 +29136,7 @@ var init_mqtt = __esm({
27081
29136
  if (this.readBuffer.length >= need) return Promise.resolve(this.take(need));
27082
29137
  if (this.socket === null) return Promise.reject(this.socketError ?? new MqttError("not connected to an MQTT broker"));
27083
29138
  if (this.socketError !== null) return Promise.reject(this.socketError);
27084
- return new Promise((resolve20, reject) => {
29139
+ return new Promise((resolve21, reject) => {
27085
29140
  let timer = null;
27086
29141
  if (deadline !== null) {
27087
29142
  const remaining = deadline - Date.now();
@@ -27096,7 +29151,7 @@ var init_mqtt = __esm({
27096
29151
  }
27097
29152
  }, remaining);
27098
29153
  }
27099
- this.waiter = { need, resolve: resolve20, reject, timer };
29154
+ this.waiter = { need, resolve: resolve21, reject, timer };
27100
29155
  this.serviceWaiter();
27101
29156
  });
27102
29157
  }
@@ -27221,7 +29276,7 @@ var init_mqtt = __esm({
27221
29276
 
27222
29277
  // ../core/src/service.ts
27223
29278
  import { readdirSync as readdirSync16, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
27224
- import { join as join26, extname as extname6 } from "node:path";
29279
+ import { join as join27, extname as extname6 } from "node:path";
27225
29280
  import { pathToFileURL } from "node:url";
27226
29281
  function matchCronField(field, value) {
27227
29282
  if (field === "*") return true;
@@ -27395,7 +29450,7 @@ var init_service = __esm({
27395
29450
  for (const entry of entries) {
27396
29451
  const ext = extname6(entry);
27397
29452
  if (ext !== ".ts" && ext !== ".js") continue;
27398
- const fullPath = join26(dir, entry);
29453
+ const fullPath = join27(dir, entry);
27399
29454
  const stat = statSync16(fullPath);
27400
29455
  if (!stat.isFile()) continue;
27401
29456
  try {
@@ -27522,7 +29577,7 @@ var init_service = __esm({
27522
29577
  for (const entry of entries) {
27523
29578
  const ext = extname6(entry);
27524
29579
  if (ext !== ".ts" && ext !== ".js") continue;
27525
- const fullPath = join26(dir, entry);
29580
+ const fullPath = join27(dir, entry);
27526
29581
  if (watchedFiles.has(fullPath)) continue;
27527
29582
  watchedFiles.add(fullPath);
27528
29583
  watchFile(fullPath, { interval: 1e3 }, async () => {
@@ -28131,7 +30186,7 @@ var init_api = __esm({
28131
30186
  * `res.destroy()`.
28132
30187
  */
28133
30188
  openStreamRequest(method, url, headers, data, connectSec) {
28134
- return new Promise((resolve20, reject) => {
30189
+ return new Promise((resolve21, reject) => {
28135
30190
  let parsed;
28136
30191
  try {
28137
30192
  parsed = new URL2(url);
@@ -28153,7 +30208,7 @@ var init_api = __esm({
28153
30208
  options.rejectUnauthorized = false;
28154
30209
  }
28155
30210
  const req2 = protocolModule.request(options, (res) => {
28156
- resolve20({ res });
30211
+ resolve21({ res });
28157
30212
  });
28158
30213
  req2.on("timeout", () => {
28159
30214
  req2.destroy(new ApiStreamError(`stream connect timeout after ${connectSec}s`, null));
@@ -28291,12 +30346,12 @@ var init_api = __esm({
28291
30346
  * authenticate to.
28292
30347
  */
28293
30348
  performRequest(method, url, headers, data, redirectsLeft) {
28294
- return new Promise((resolve20) => {
30349
+ return new Promise((resolve21) => {
28295
30350
  let parsed;
28296
30351
  try {
28297
30352
  parsed = new URL2(url);
28298
30353
  } catch (err) {
28299
- resolve20({ kind: "error", error: err instanceof Error ? err.message : String(err) });
30354
+ resolve21({ kind: "error", error: err instanceof Error ? err.message : String(err) });
28300
30355
  return;
28301
30356
  }
28302
30357
  const isHttps = parsed.protocol === "https:";
@@ -28321,7 +30376,7 @@ var init_api = __esm({
28321
30376
  try {
28322
30377
  nextUrl = new URL2(location, url).toString();
28323
30378
  } catch {
28324
- resolve20({ kind: "response", res });
30379
+ resolve21({ kind: "response", res });
28325
30380
  return;
28326
30381
  }
28327
30382
  const crossOrigin = !sameOrigin(url, nextUrl);
@@ -28339,17 +30394,17 @@ var init_api = __esm({
28339
30394
  deleteHeaderCaseInsensitive(nextHeaders, name);
28340
30395
  }
28341
30396
  }
28342
- this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve20);
30397
+ this.performRequest(nextMethod, nextUrl, nextHeaders, nextData, redirectsLeft - 1).then(resolve21);
28343
30398
  return;
28344
30399
  }
28345
- resolve20({ kind: "response", res });
30400
+ resolve21({ kind: "response", res });
28346
30401
  });
28347
30402
  req2.on("timeout", () => {
28348
30403
  req2.destroy();
28349
- resolve20({ kind: "error", error: `Request timed out after ${this.timeout}s` });
30404
+ resolve21({ kind: "error", error: `Request timed out after ${this.timeout}s` });
28350
30405
  });
28351
30406
  req2.on("error", (err) => {
28352
- resolve20({ kind: "error", error: err.message });
30407
+ resolve21({ kind: "error", error: err.message });
28353
30408
  });
28354
30409
  if (data) {
28355
30410
  req2.write(data);
@@ -28359,7 +30414,7 @@ var init_api = __esm({
28359
30414
  }
28360
30415
  /** Buffer a response body, parse JSON if possible, and store cookies. */
28361
30416
  readResponse(res) {
28362
- return new Promise((resolve20) => {
30417
+ return new Promise((resolve21) => {
28363
30418
  const chunks = [];
28364
30419
  res.on("data", (chunk) => {
28365
30420
  chunks.push(chunk);
@@ -28374,7 +30429,7 @@ var init_api = __esm({
28374
30429
  } catch {
28375
30430
  parsed = raw;
28376
30431
  }
28377
- resolve20({
30432
+ resolve21({
28378
30433
  http_code: res.statusCode ?? null,
28379
30434
  body: parsed,
28380
30435
  headers: respHeaders,
@@ -28382,7 +30437,7 @@ var init_api = __esm({
28382
30437
  });
28383
30438
  });
28384
30439
  res.on("error", (err) => {
28385
- resolve20({ http_code: null, body: null, headers: {}, error: err.message });
30440
+ resolve21({ http_code: null, body: null, headers: {}, error: err.message });
28386
30441
  });
28387
30442
  });
28388
30443
  }
@@ -28446,7 +30501,7 @@ function parseMailRedirectList(raw) {
28446
30501
  return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
28447
30502
  }
28448
30503
  function readResponse(socket) {
28449
- return new Promise((resolve20, reject) => {
30504
+ return new Promise((resolve21, reject) => {
28450
30505
  let buffer = "";
28451
30506
  const onData = (chunk) => {
28452
30507
  buffer += chunk.toString("utf-8");
@@ -28458,7 +30513,7 @@ function readResponse(socket) {
28458
30513
  if (line.length >= 4 && line[3] === " ") {
28459
30514
  socket.removeListener("data", onData);
28460
30515
  socket.removeListener("error", onError);
28461
- resolve20({ code, text: buffer.trim() });
30516
+ resolve21({ code, text: buffer.trim() });
28462
30517
  return;
28463
30518
  }
28464
30519
  }
@@ -28472,10 +30527,10 @@ function readResponse(socket) {
28472
30527
  });
28473
30528
  }
28474
30529
  function sendCommand(socket, command) {
28475
- return new Promise((resolve20, reject) => {
30530
+ return new Promise((resolve21, reject) => {
28476
30531
  socket.write(command + "\r\n", "utf-8", (err) => {
28477
30532
  if (err) return reject(err);
28478
- readResponse(socket).then(resolve20, reject);
30533
+ readResponse(socket).then(resolve21, reject);
28479
30534
  });
28480
30535
  });
28481
30536
  }
@@ -28575,7 +30630,7 @@ function imapQuote(s) {
28575
30630
  return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
28576
30631
  }
28577
30632
  function imapReadLine(socket) {
28578
- return new Promise((resolve20, reject) => {
30633
+ return new Promise((resolve21, reject) => {
28579
30634
  let buffer = "";
28580
30635
  const onData = (chunk) => {
28581
30636
  buffer += chunk.toString("utf-8");
@@ -28583,7 +30638,7 @@ function imapReadLine(socket) {
28583
30638
  if (nlIndex !== -1) {
28584
30639
  socket.removeListener("data", onData);
28585
30640
  socket.removeListener("error", onError);
28586
- resolve20(buffer);
30641
+ resolve21(buffer);
28587
30642
  }
28588
30643
  };
28589
30644
  const onError = (err) => {
@@ -28595,7 +30650,7 @@ function imapReadLine(socket) {
28595
30650
  });
28596
30651
  }
28597
30652
  function imapCommand(socket, command) {
28598
- return new Promise((resolve20, reject) => {
30653
+ return new Promise((resolve21, reject) => {
28599
30654
  imapTagCounter++;
28600
30655
  const tag = `T${imapTagCounter}`;
28601
30656
  const fullCommand = `${tag} ${command}\r
@@ -28606,7 +30661,7 @@ function imapCommand(socket, command) {
28606
30661
  if (buffer.includes(`${tag} OK`)) {
28607
30662
  socket.removeListener("data", onData);
28608
30663
  socket.removeListener("error", onError);
28609
- resolve20(buffer);
30664
+ resolve21(buffer);
28610
30665
  return;
28611
30666
  }
28612
30667
  if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
@@ -28911,14 +30966,14 @@ var init_messenger = __esm({
28911
30966
  let socket;
28912
30967
  if (this.port === 465) {
28913
30968
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
28914
- await new Promise((resolve20, reject) => {
28915
- socket.once("secureConnect", resolve20);
30969
+ await new Promise((resolve21, reject) => {
30970
+ socket.once("secureConnect", resolve21);
28916
30971
  socket.once("error", reject);
28917
30972
  });
28918
30973
  } else {
28919
30974
  socket = net3.createConnection({ host: this.host, port: this.port });
28920
- await new Promise((resolve20, reject) => {
28921
- socket.once("connect", resolve20);
30975
+ await new Promise((resolve21, reject) => {
30976
+ socket.once("connect", resolve21);
28922
30977
  socket.once("error", reject);
28923
30978
  });
28924
30979
  }
@@ -28942,8 +30997,8 @@ var init_messenger = __esm({
28942
30997
  socket = tls2.connect(
28943
30998
  { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
28944
30999
  );
28945
- await new Promise((resolve20, reject) => {
28946
- socket.once("secureConnect", resolve20);
31000
+ await new Promise((resolve21, reject) => {
31001
+ socket.once("secureConnect", resolve21);
28947
31002
  socket.once("error", reject);
28948
31003
  });
28949
31004
  const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
@@ -29036,14 +31091,14 @@ var init_messenger = __esm({
29036
31091
  let socket;
29037
31092
  if (this.port === 465) {
29038
31093
  socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
29039
- await new Promise((resolve20, reject) => {
29040
- socket.once("secureConnect", resolve20);
31094
+ await new Promise((resolve21, reject) => {
31095
+ socket.once("secureConnect", resolve21);
29041
31096
  socket.once("error", reject);
29042
31097
  });
29043
31098
  } else {
29044
31099
  socket = net3.createConnection({ host: this.host, port: this.port });
29045
- await new Promise((resolve20, reject) => {
29046
- socket.once("connect", resolve20);
31100
+ await new Promise((resolve21, reject) => {
31101
+ socket.once("connect", resolve21);
29047
31102
  socket.once("error", reject);
29048
31103
  });
29049
31104
  }
@@ -29078,14 +31133,14 @@ var init_messenger = __esm({
29078
31133
  const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
29079
31134
  if (useTls) {
29080
31135
  socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
29081
- await new Promise((resolve20, reject) => {
29082
- socket.once("secureConnect", resolve20);
31136
+ await new Promise((resolve21, reject) => {
31137
+ socket.once("secureConnect", resolve21);
29083
31138
  socket.once("error", reject);
29084
31139
  });
29085
31140
  } else {
29086
31141
  socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
29087
- await new Promise((resolve20, reject) => {
29088
- socket.once("connect", resolve20);
31142
+ await new Promise((resolve21, reject) => {
31143
+ socket.once("connect", resolve21);
29089
31144
  socket.once("error", reject);
29090
31145
  });
29091
31146
  }
@@ -29995,16 +32050,16 @@ var init_htmlElement = __esm({
29995
32050
  });
29996
32051
 
29997
32052
  // ../core/src/ai.ts
29998
- import { existsSync as existsSync25, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15, readFileSync as readFileSync24 } from "node:fs";
32053
+ import { existsSync as existsSync26, mkdirSync as mkdirSync17, writeFileSync as writeFileSync16, readFileSync as readFileSync24 } from "node:fs";
29999
32054
  import { homedir } from "node:os";
30000
- import { join as join27, resolve as resolve16, relative as relative9, dirname as dirname12 } from "node:path";
32055
+ import { join as join28, resolve as resolve17, relative as relative10, dirname as dirname12 } from "node:path";
30001
32056
  import { fileURLToPath as fileURLToPath7 } from "node:url";
30002
32057
  import { execSync, execFileSync as execFileSync4 } from "node:child_process";
30003
32058
  import { createInterface } from "node:readline";
30004
32059
  function readVersion() {
30005
32060
  try {
30006
32061
  const thisDir = dirname12(fileURLToPath7(import.meta.url));
30007
- const rootPkg = resolve16(thisDir, "..", "..", "..", "package.json");
32062
+ const rootPkg = resolve17(thisDir, "..", "..", "..", "package.json");
30008
32063
  const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
30009
32064
  return pkg.version ?? "0.0.0";
30010
32065
  } catch {
@@ -30068,8 +32123,8 @@ function downloadSkillsSync(jobs) {
30068
32123
  function installSkills(root = ".", targets) {
30069
32124
  const ref = skillsRef();
30070
32125
  const dests = targets ?? [
30071
- join27(resolve16(root), ".claude", "skills"),
30072
- join27(homedir(), ".claude", "skills")
32126
+ join28(resolve17(root), ".claude", "skills"),
32127
+ join28(homedir(), ".claude", "skills")
30073
32128
  ];
30074
32129
  const jobs = [];
30075
32130
  const index = /* @__PURE__ */ new Map();
@@ -30087,9 +32142,9 @@ function installSkills(root = ".", targets) {
30087
32142
  const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
30088
32143
  skillMdUrl[skill] = `${base}/SKILL.md`;
30089
32144
  for (const dest of dests) {
30090
- add(`${base}/SKILL.md`, join27(dest, skill, "SKILL.md"));
32145
+ add(`${base}/SKILL.md`, join28(dest, skill, "SKILL.md"));
30091
32146
  for (const r of spec.references) {
30092
- add(`${base}/references/${r}`, join27(dest, skill, "references", r));
32147
+ add(`${base}/references/${r}`, join28(dest, skill, "references", r));
30093
32148
  }
30094
32149
  }
30095
32150
  }
@@ -30101,10 +32156,10 @@ function installSkills(root = ".", targets) {
30101
32156
  return installed;
30102
32157
  }
30103
32158
  function isInstalled(root, tool) {
30104
- return existsSync25(join27(resolve16(root), tool.contextFile));
32159
+ return existsSync26(join28(resolve17(root), tool.contextFile));
30105
32160
  }
30106
32161
  function showMenu(root = ".") {
30107
- const r = resolve16(root);
32162
+ const r = resolve17(root);
30108
32163
  console.log("\n Tina4 AI Context Installer\n");
30109
32164
  for (let i = 0; i < AI_TOOLS.length; i++) {
30110
32165
  const tool = AI_TOOLS[i];
@@ -30122,16 +32177,16 @@ function showMenu(root = ".") {
30122
32177
  const tina4AiMarker = tina4AiInstalled ? ` ${GREEN2}[installed]${RESET2}` : "";
30123
32178
  console.log(` 8. Install tina4-ai tools (requires Python)${tina4AiMarker}`);
30124
32179
  console.log();
30125
- return new Promise((resolve20) => {
32180
+ return new Promise((resolve21) => {
30126
32181
  const rl = createInterface({ input: process.stdin, output: process.stdout });
30127
32182
  rl.question(" Select (comma-separated, or 'all'): ", (answer) => {
30128
32183
  rl.close();
30129
- resolve20(answer.trim());
32184
+ resolve21(answer.trim());
30130
32185
  });
30131
32186
  });
30132
32187
  }
30133
32188
  function installSelected(root, selection) {
30134
- const rootPath = resolve16(root);
32189
+ const rootPath = resolve17(root);
30135
32190
  const created = [];
30136
32191
  let indices;
30137
32192
  let doInstallTina4Ai = false;
@@ -30224,35 +32279,35 @@ function looksLikeOldFrameworkInstall(existing) {
30224
32279
  function writeOrMerge(contextPath, contextFile, frameworkGuide) {
30225
32280
  const block = skillBlock(contextFile);
30226
32281
  const [start2, end] = markersFor(contextFile);
30227
- if (!existsSync25(contextPath)) {
30228
- writeFileSync15(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
32282
+ if (!existsSync26(contextPath)) {
32283
+ writeFileSync16(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
30229
32284
  return "Installed";
30230
32285
  }
30231
32286
  const existing = readFileSync24(contextPath, "utf-8");
30232
32287
  if (hasMarkers(existing, start2, end)) {
30233
- writeFileSync15(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
32288
+ writeFileSync16(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
30234
32289
  return "Refreshed skill block in";
30235
32290
  }
30236
32291
  if (looksLikeOldFrameworkInstall(existing)) {
30237
32292
  const head = existing.replace(/^\s+/, "");
30238
32293
  const preamble = existing.slice(0, existing.length - head.length);
30239
32294
  const newContent = (preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") + frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
30240
- writeFileSync15(contextPath, newContent, "utf-8");
32295
+ writeFileSync16(contextPath, newContent, "utf-8");
30241
32296
  return "Migrated (replaced old framework dump in)";
30242
32297
  }
30243
- writeFileSync15(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
32298
+ writeFileSync16(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
30244
32299
  return "Appended skill block to";
30245
32300
  }
30246
32301
  function installForTool(root, tool, context) {
30247
32302
  const created = [];
30248
- const contextPath = join27(root, tool.contextFile);
32303
+ const contextPath = join28(root, tool.contextFile);
30249
32304
  if (tool.configDir) {
30250
- mkdirSync16(join27(root, tool.configDir), { recursive: true });
32305
+ mkdirSync17(join28(root, tool.configDir), { recursive: true });
30251
32306
  }
30252
32307
  const parentDir = dirname12(contextPath);
30253
- mkdirSync16(parentDir, { recursive: true });
32308
+ mkdirSync17(parentDir, { recursive: true });
30254
32309
  const action = writeOrMerge(contextPath, tool.contextFile, context);
30255
- const rel = relative9(root, contextPath);
32310
+ const rel = relative10(root, contextPath);
30256
32311
  created.push(rel);
30257
32312
  console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
30258
32313
  if (tool.name === "claude-code") {
@@ -30285,7 +32340,7 @@ function installTina4Ai() {
30285
32340
  function installClaudeSkills(root) {
30286
32341
  const created = [];
30287
32342
  for (const skill of installSkills(root)) {
30288
- created.push(join27(".claude", "skills", skill));
32343
+ created.push(join28(".claude", "skills", skill));
30289
32344
  console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
30290
32345
  }
30291
32346
  return created;
@@ -30627,9 +32682,9 @@ import { tests, assertEqual, runAll } from "tina4-nodejs";
30627
32682
  function generateClaudeCodeContext() {
30628
32683
  try {
30629
32684
  const thisDir = dirname12(fileURLToPath7(import.meta.url));
30630
- const repoRoot = resolve16(thisDir, "..", "..", "..");
30631
- const claudeMdPath = join27(repoRoot, "CLAUDE.md");
30632
- if (existsSync25(claudeMdPath)) {
32685
+ const repoRoot = resolve17(thisDir, "..", "..", "..");
32686
+ const claudeMdPath = join28(repoRoot, "CLAUDE.md");
32687
+ if (existsSync26(claudeMdPath)) {
30633
32688
  return readFileSync24(claudeMdPath, "utf-8");
30634
32689
  }
30635
32690
  } catch {
@@ -31176,11 +33231,11 @@ var init_aiClient = __esm({
31176
33231
  const payload = JSON.stringify(body);
31177
33232
  const controller = new AbortController();
31178
33233
  const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
31179
- return new Promise((resolve20, reject) => {
33234
+ return new Promise((resolve21, reject) => {
31180
33235
  const client = url.protocol === "https:" ? https2 : http2;
31181
33236
  const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
31182
33237
  clearTimeout(connectTimer);
31183
- resolve20({ response, cleanup: () => {
33238
+ resolve21({ response, cleanup: () => {
31184
33239
  clearTimeout(totalTimer);
31185
33240
  clearTimeout(connectTimer);
31186
33241
  } });
@@ -31209,7 +33264,7 @@ var init_aiClient = __esm({
31209
33264
  const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
31210
33265
  const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
31211
33266
  const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
31212
- return new Promise((resolve20) => setTimeout(resolve20, delay));
33267
+ return new Promise((resolve21) => setTimeout(resolve21, delay));
31213
33268
  }
31214
33269
  static async requestJson(config, headers, body) {
31215
33270
  const deadline = performance.now() + config.totalTimeout * 1e3;
@@ -34153,8 +36208,8 @@ __export(sqlite_exports, {
34153
36208
  SQLiteAdapter: () => SQLiteAdapter
34154
36209
  });
34155
36210
  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";
36211
+ import { mkdirSync as mkdirSync18 } from "node:fs";
36212
+ import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29, resolve as resolve18 } from "node:path";
34158
36213
  function isIdentifier(str) {
34159
36214
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
34160
36215
  }
@@ -34187,13 +36242,13 @@ function resolveSqlitePath(dbPath) {
34187
36242
  if (dbPath === ":memory:") return dbPath;
34188
36243
  let path8 = dbPath;
34189
36244
  if (!isAbsolute5(path8)) {
34190
- path8 = join28(process.cwd(), path8);
34191
- mkdirSync17(dirname13(path8), { recursive: true });
36245
+ path8 = join29(process.cwd(), path8);
36246
+ mkdirSync18(dirname13(path8), { recursive: true });
34192
36247
  } else {
34193
- const cwd = resolve17(process.cwd());
34194
- const abs = resolve17(path8);
36248
+ const cwd = resolve18(process.cwd());
36249
+ const abs = resolve18(path8);
34195
36250
  if (abs.startsWith(cwd + "/") || abs === cwd) {
34196
- mkdirSync17(dirname13(abs), { recursive: true });
36251
+ mkdirSync18(dirname13(abs), { recursive: true });
34197
36252
  }
34198
36253
  }
34199
36254
  return path8;
@@ -34591,7 +36646,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
34591
36646
  const elapsedMs = () => performance.now() - startedAt;
34592
36647
  if (budgetMs === null) return attempt();
34593
36648
  const started = attempt();
34594
- return new Promise((resolve20, reject) => {
36649
+ return new Promise((resolve21, reject) => {
34595
36650
  let expired = false;
34596
36651
  const timer = setTimeout(() => {
34597
36652
  expired = true;
@@ -34601,7 +36656,7 @@ function withConnectTimeout(attempt, budgetMs, host, port, abandon) {
34601
36656
  (arrived) => {
34602
36657
  clearTimeout(timer);
34603
36658
  if (expired) abandon?.(arrived);
34604
- else resolve20(arrived);
36659
+ else resolve21(arrived);
34605
36660
  },
34606
36661
  (failure) => {
34607
36662
  clearTimeout(timer);
@@ -35190,10 +37245,10 @@ var init_mysql = __esm({
35190
37245
  ...timeoutOption
35191
37246
  });
35192
37247
  }
35193
- return new Promise((resolve20, reject) => {
37248
+ return new Promise((resolve21, reject) => {
35194
37249
  this.connection.connect((err) => {
35195
37250
  if (err) reject(err);
35196
- else resolve20();
37251
+ else resolve21();
35197
37252
  });
35198
37253
  });
35199
37254
  },
@@ -35215,10 +37270,10 @@ var init_mysql = __esm({
35215
37270
  }
35216
37271
  }
35217
37272
  queryPromise(sql, params) {
35218
- return new Promise((resolve20, reject) => {
37273
+ return new Promise((resolve21, reject) => {
35219
37274
  this.connection.query(sql, params ?? [], (err, results) => {
35220
37275
  if (err) reject(err);
35221
- else resolve20(results);
37276
+ else resolve21(results);
35222
37277
  });
35223
37278
  });
35224
37279
  }
@@ -35604,11 +37659,11 @@ var init_mssql = __esm({
35604
37659
  };
35605
37660
  }
35606
37661
  await withConnectTimeout(
35607
- () => new Promise((resolve20, reject) => {
37662
+ () => new Promise((resolve21, reject) => {
35608
37663
  this.connection = new Connection(tediousConfig);
35609
37664
  this.connection.on("connect", (err) => {
35610
37665
  if (err) reject(err);
35611
- else resolve20();
37666
+ else resolve21();
35612
37667
  });
35613
37668
  this.connection.connect();
35614
37669
  }),
@@ -35653,11 +37708,11 @@ var init_mssql = __esm({
35653
37708
  const tediousModule = requireTedious();
35654
37709
  const Request = tediousModule.Request;
35655
37710
  const TYPES = tediousModule.TYPES;
35656
- return new Promise((resolve20, reject) => {
37711
+ return new Promise((resolve21, reject) => {
35657
37712
  const rows = [];
35658
37713
  const request = new Request(sql, (err, rowCount) => {
35659
37714
  if (err) reject(err);
35660
- else resolve20({ rows, rowCount });
37715
+ else resolve21({ rows, rowCount });
35661
37716
  });
35662
37717
  if (params) {
35663
37718
  params.forEach((p, i) => {
@@ -35859,8 +37914,8 @@ var init_mssql = __esm({
35859
37914
  throw new Error("Use startTransactionAsync() for MSSQL.");
35860
37915
  }
35861
37916
  async startTransactionAsync() {
35862
- await new Promise((resolve20, reject) => {
35863
- this.connection.beginTransaction((err) => err ? reject(err) : resolve20());
37917
+ await new Promise((resolve21, reject) => {
37918
+ this.connection.beginTransaction((err) => err ? reject(err) : resolve21());
35864
37919
  });
35865
37920
  this._inTransaction = true;
35866
37921
  }
@@ -35868,8 +37923,8 @@ var init_mssql = __esm({
35868
37923
  throw new Error("Use commitAsync() for MSSQL.");
35869
37924
  }
35870
37925
  async commitAsync() {
35871
- await new Promise((resolve20, reject) => {
35872
- this.connection.commitTransaction((err) => err ? reject(err) : resolve20());
37926
+ await new Promise((resolve21, reject) => {
37927
+ this.connection.commitTransaction((err) => err ? reject(err) : resolve21());
35873
37928
  });
35874
37929
  this._inTransaction = false;
35875
37930
  }
@@ -35877,8 +37932,8 @@ var init_mssql = __esm({
35877
37932
  throw new Error("Use rollbackAsync() for MSSQL.");
35878
37933
  }
35879
37934
  async rollbackAsync() {
35880
- await new Promise((resolve20, reject) => {
35881
- this.connection.rollbackTransaction((err) => err ? reject(err) : resolve20());
37935
+ await new Promise((resolve21, reject) => {
37936
+ this.connection.rollbackTransaction((err) => err ? reject(err) : resolve21());
35882
37937
  });
35883
37938
  this._inTransaction = false;
35884
37939
  }
@@ -36203,8 +38258,8 @@ var init_firebird = __esm({
36203
38258
  }
36204
38259
  attachOnce(config) {
36205
38260
  const fb = requireFirebird();
36206
- return new Promise((resolve20, reject) => {
36207
- fb.attach(config, (err, db) => err ? reject(err) : resolve20(db));
38261
+ return new Promise((resolve21, reject) => {
38262
+ fb.attach(config, (err, db) => err ? reject(err) : resolve21(db));
36208
38263
  });
36209
38264
  }
36210
38265
  /**
@@ -36223,7 +38278,7 @@ var init_firebird = __esm({
36223
38278
  } catch (err) {
36224
38279
  lastError = err;
36225
38280
  if (attempt < attempts - 1) {
36226
- await new Promise((resolve20) => setTimeout(resolve20, 100 * (attempt + 1)));
38281
+ await new Promise((resolve21) => setTimeout(resolve21, 100 * (attempt + 1)));
36227
38282
  }
36228
38283
  }
36229
38284
  }
@@ -36306,19 +38361,19 @@ var init_firebird = __esm({
36306
38361
  }
36307
38362
  queryPromise(sql, params) {
36308
38363
  const translated = this.translateSql(sql);
36309
- return this.withReconnect(() => new Promise((resolve20, reject) => {
38364
+ return this.withReconnect(() => new Promise((resolve21, reject) => {
36310
38365
  this.statementHandle().query(translated, params ?? [], (err, result) => {
36311
38366
  if (err) reject(err);
36312
- else resolve20(result ?? []);
38367
+ else resolve21(result ?? []);
36313
38368
  });
36314
38369
  }));
36315
38370
  }
36316
38371
  executePromise(sql, params) {
36317
38372
  const translated = this.translateSql(sql);
36318
- return this.withReconnect(() => new Promise((resolve20, reject) => {
38373
+ return this.withReconnect(() => new Promise((resolve21, reject) => {
36319
38374
  this.statementHandle().execute(translated, params ?? [], (err) => {
36320
38375
  if (err) reject(err);
36321
- else resolve20();
38376
+ else resolve21();
36322
38377
  });
36323
38378
  }));
36324
38379
  }
@@ -36357,13 +38412,13 @@ var init_firebird = __esm({
36357
38412
  * and no bytes round-tripped (FB-BLOB-SRP-UNVERIFIED).
36358
38413
  */
36359
38414
  readBlob(blobFn) {
36360
- return new Promise((resolve20, reject) => {
38415
+ return new Promise((resolve21, reject) => {
36361
38416
  blobFn((err, _name, emitter) => {
36362
38417
  if (err) return reject(err);
36363
- if (!emitter) return resolve20(null);
38418
+ if (!emitter) return resolve21(null);
36364
38419
  const chunks = [];
36365
38420
  emitter.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
36366
- emitter.on("end", () => resolve20(Buffer.concat(chunks)));
38421
+ emitter.on("end", () => resolve21(Buffer.concat(chunks)));
36367
38422
  emitter.on("error", (streamErr) => reject(streamErr));
36368
38423
  });
36369
38424
  });
@@ -36499,12 +38554,12 @@ var init_firebird = __esm({
36499
38554
  }
36500
38555
  async startTransactionAsync() {
36501
38556
  this.ensureConnected();
36502
- await new Promise((resolve20, reject) => {
38557
+ await new Promise((resolve21, reject) => {
36503
38558
  this.db.transaction(0, (err, transaction) => {
36504
38559
  if (err) reject(err);
36505
38560
  else {
36506
38561
  this.transaction = transaction;
36507
- resolve20();
38562
+ resolve21();
36508
38563
  }
36509
38564
  });
36510
38565
  });
@@ -36514,12 +38569,12 @@ var init_firebird = __esm({
36514
38569
  }
36515
38570
  async commitAsync() {
36516
38571
  if (!this.transaction) throw new Error("No active transaction to commit.");
36517
- await new Promise((resolve20, reject) => {
38572
+ await new Promise((resolve21, reject) => {
36518
38573
  this.transaction.commit((err) => {
36519
38574
  if (err) reject(err);
36520
38575
  else {
36521
38576
  this.transaction = null;
36522
- resolve20();
38577
+ resolve21();
36523
38578
  }
36524
38579
  });
36525
38580
  });
@@ -36529,12 +38584,12 @@ var init_firebird = __esm({
36529
38584
  }
36530
38585
  async rollbackAsync() {
36531
38586
  if (!this.transaction) throw new Error("No active transaction to rollback.");
36532
- await new Promise((resolve20, reject) => {
38587
+ await new Promise((resolve21, reject) => {
36533
38588
  this.transaction.rollback((err) => {
36534
38589
  if (err) reject(err);
36535
38590
  else {
36536
38591
  this.transaction = null;
36537
- resolve20();
38592
+ resolve21();
36538
38593
  }
36539
38594
  });
36540
38595
  });
@@ -39014,7 +41069,7 @@ var init_database = __esm({
39014
41069
 
39015
41070
  // src/model.ts
39016
41071
  import { readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
39017
- import { join as join29, extname as extname7 } from "node:path";
41072
+ import { join as join30, extname as extname7 } from "node:path";
39018
41073
  async function discoverModels(modelsDir) {
39019
41074
  const models = [];
39020
41075
  let files;
@@ -39024,7 +41079,7 @@ async function discoverModels(modelsDir) {
39024
41079
  return models;
39025
41080
  }
39026
41081
  for (const file of files) {
39027
- const filePath = join29(modelsDir, file);
41082
+ const filePath = join30(modelsDir, file);
39028
41083
  const stat = statSync17(filePath);
39029
41084
  if (!stat.isFile()) continue;
39030
41085
  const ext = extname7(file);
@@ -39066,8 +41121,8 @@ var init_model = __esm({
39066
41121
  });
39067
41122
 
39068
41123
  // 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";
41124
+ import { existsSync as existsSync27, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync19, writeFileSync as writeFileSync17 } from "node:fs";
41125
+ import { join as join31, resolve as resolve19 } from "node:path";
39071
41126
  function unwrapAdapter(db) {
39072
41127
  let cur = db;
39073
41128
  while (cur && cur.constructor?.name === "CachedDatabaseAdapter" && cur.adapter) {
@@ -39383,15 +41438,15 @@ async function rollback(migrationsDir, delimiter2) {
39383
41438
  }
39384
41439
  return rolledBack2;
39385
41440
  }
39386
- const dir = resolve18(migrationsDir ?? "migrations");
41441
+ const dir = resolve19(migrationsDir ?? "migrations");
39387
41442
  const delim = delimiter2 ?? ";";
39388
41443
  const db = getAdapter();
39389
41444
  const migrations = await getLastBatchMigrations();
39390
41445
  const rolledBack = [];
39391
41446
  for (const migration of migrations) {
39392
41447
  const downFile = `${migration.migration_name}.down.sql`;
39393
- const downPath = join30(dir, downFile);
39394
- if (!existsSync26(downPath)) {
41448
+ const downPath = join31(dir, downFile);
41449
+ if (!existsSync27(downPath)) {
39395
41450
  throw new Error(
39396
41451
  `Cannot rollback ${migration.migration_name}: no .down.sql file found`
39397
41452
  );
@@ -39555,10 +41610,10 @@ function warnUnprefixedMigrations(files) {
39555
41610
  }
39556
41611
  async function migrate(adapter, options) {
39557
41612
  const db = adapter ?? getAdapter();
39558
- const dir = resolve18(options?.migrationsDir ?? "migrations");
41613
+ const dir = resolve19(options?.migrationsDir ?? "migrations");
39559
41614
  const delimiter2 = options?.delimiter ?? ";";
39560
41615
  const result = { applied: [], skipped: [], failed: [] };
39561
- if (!existsSync26(dir)) {
41616
+ if (!existsSync27(dir)) {
39562
41617
  return result;
39563
41618
  }
39564
41619
  await ensureMigrationTableOn(db);
@@ -39594,7 +41649,7 @@ async function migrate(adapter, options) {
39594
41649
  result.skipped.push(file);
39595
41650
  continue;
39596
41651
  }
39597
- const sqlContent = readFileSync25(join30(dir, file), "utf-8").trim();
41652
+ const sqlContent = readFileSync25(join31(dir, file), "utf-8").trim();
39598
41653
  if (!sqlContent) {
39599
41654
  result.skipped.push(file);
39600
41655
  continue;
@@ -39629,9 +41684,9 @@ async function migrate(adapter, options) {
39629
41684
  }
39630
41685
  async function status(adapter, options) {
39631
41686
  const db = adapter ?? getAdapter();
39632
- const dir = resolve18(options?.migrationsDir ?? "migrations");
41687
+ const dir = resolve19(options?.migrationsDir ?? "migrations");
39633
41688
  const result = { completed: [], pending: [] };
39634
- if (!existsSync26(dir)) {
41689
+ if (!existsSync27(dir)) {
39635
41690
  return result;
39636
41691
  }
39637
41692
  if (!await adapterTableExists(db, MIGRATION_TABLE)) {
@@ -39677,13 +41732,13 @@ async function createMigration(description, options) {
39677
41732
  if (kind === "code" || kind === "class") {
39678
41733
  return createClassMigration(description, options);
39679
41734
  }
39680
- const dir = resolve18(options?.migrationsDir ?? "migrations");
39681
- if (!existsSync26(dir)) {
39682
- mkdirSync18(dir, { recursive: true });
41735
+ const dir = resolve19(options?.migrationsDir ?? "migrations");
41736
+ if (!existsSync27(dir)) {
41737
+ mkdirSync19(dir, { recursive: true });
39683
41738
  }
39684
41739
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
39685
41740
  const now = /* @__PURE__ */ new Date();
39686
- const timestamp = [
41741
+ const timestamp2 = [
39687
41742
  now.getFullYear(),
39688
41743
  String(now.getMonth() + 1).padStart(2, "0"),
39689
41744
  String(now.getDate()).padStart(2, "0"),
@@ -39691,10 +41746,10 @@ async function createMigration(description, options) {
39691
41746
  String(now.getMinutes()).padStart(2, "0"),
39692
41747
  String(now.getSeconds()).padStart(2, "0")
39693
41748
  ].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);
41749
+ const upFileName = `${timestamp2}_${safeName}.sql`;
41750
+ const downFileName = `${timestamp2}_${safeName}.down.sql`;
41751
+ const upPath = join31(dir, upFileName);
41752
+ const downPath = join31(dir, downFileName);
39698
41753
  const upTemplate = `-- Migration: ${description}
39699
41754
  -- Created: ${now.toISOString()}
39700
41755
 
@@ -39703,19 +41758,19 @@ async function createMigration(description, options) {
39703
41758
  -- Created: ${now.toISOString()}
39704
41759
 
39705
41760
  `;
39706
- writeFileSync16(upPath, upTemplate, "utf-8");
39707
- writeFileSync16(downPath, downTemplate, "utf-8");
41761
+ writeFileSync17(upPath, upTemplate, "utf-8");
41762
+ writeFileSync17(downPath, downTemplate, "utf-8");
39708
41763
  return { upPath, downPath };
39709
41764
  }
39710
41765
  async function createClassMigration(description, options) {
39711
- const dir = resolve18(options?.migrationsDir ?? "migrations");
39712
- if (!existsSync26(dir)) {
39713
- mkdirSync18(dir, { recursive: true });
41766
+ const dir = resolve19(options?.migrationsDir ?? "migrations");
41767
+ if (!existsSync27(dir)) {
41768
+ mkdirSync19(dir, { recursive: true });
39714
41769
  }
39715
41770
  const safeName = description.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
39716
41771
  const className = description.replace(/[^a-zA-Z0-9 ]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
39717
41772
  const now = /* @__PURE__ */ new Date();
39718
- const timestamp = [
41773
+ const timestamp2 = [
39719
41774
  now.getFullYear(),
39720
41775
  String(now.getMonth() + 1).padStart(2, "0"),
39721
41776
  String(now.getDate()).padStart(2, "0"),
@@ -39723,8 +41778,8 @@ async function createClassMigration(description, options) {
39723
41778
  String(now.getMinutes()).padStart(2, "0"),
39724
41779
  String(now.getSeconds()).padStart(2, "0")
39725
41780
  ].join("");
39726
- const fileName = `${timestamp}_${safeName}.ts`;
39727
- const filePath = join30(dir, fileName);
41781
+ const fileName = `${timestamp2}_${safeName}.ts`;
41782
+ const filePath = join31(dir, fileName);
39728
41783
  const content = `// Migration: ${description}
39729
41784
  // Created: ${now.toISOString()}
39730
41785
 
@@ -39740,7 +41795,7 @@ export class ${className} {
39740
41795
  }
39741
41796
  }
39742
41797
  `;
39743
- writeFileSync16(filePath, content, "utf-8");
41798
+ writeFileSync17(filePath, content, "utf-8");
39744
41799
  return filePath;
39745
41800
  }
39746
41801
  var ALTER_ADD_RE, CREATE_TABLE_RE, MIGRATION_TABLE, SMART_QUOTES, SMART_QUOTE_RE, SET_TERM_RE, Migration;
@@ -39821,8 +41876,8 @@ var init_migration = __esm({
39821
41876
  }
39822
41877
  /** Return sorted list of all migration files on disk (excludes .down.sql). */
39823
41878
  getFiles() {
39824
- const dir = resolve18(this.dir);
39825
- if (!existsSync26(dir)) return [];
41879
+ const dir = resolve19(this.dir);
41880
+ if (!existsSync27(dir)) return [];
39826
41881
  return sortMigrationFiles(
39827
41882
  readdirSync18(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql"))
39828
41883
  );
@@ -42606,8 +44661,8 @@ var init_seeder = __esm({
42606
44661
  // src/docstore.ts
42607
44662
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
42608
44663
  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";
44664
+ import { mkdirSync as mkdirSync20 } from "node:fs";
44665
+ import { dirname as dirname14, isAbsolute as isAbsolute6, join as join32 } from "node:path";
42611
44666
  function iso(d) {
42612
44667
  return d.toISOString();
42613
44668
  }
@@ -42872,8 +44927,8 @@ function resolveStorePath(dbPath) {
42872
44927
  if (dbPath === ":memory:") return dbPath;
42873
44928
  let path8 = dbPath;
42874
44929
  if (!isAbsolute6(path8)) {
42875
- path8 = join31(process.cwd(), path8);
42876
- mkdirSync19(dirname14(path8), { recursive: true });
44930
+ path8 = join32(process.cwd(), path8);
44931
+ mkdirSync20(dirname14(path8), { recursive: true });
42877
44932
  }
42878
44933
  return path8;
42879
44934
  }
@@ -44156,8 +46211,8 @@ var init_attachment = __esm({
44156
46211
 
44157
46212
  // src/realtime/storage.ts
44158
46213
  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";
46214
+ import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync18, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
46215
+ import { resolve as resolve20, sep as sep6 } from "node:path";
44161
46216
  import { createRequire as createRequire8 } from "node:module";
44162
46217
  function storageKey(filename = "") {
44163
46218
  let ext = "";
@@ -44193,19 +46248,19 @@ var init_storage = __esm({
44193
46248
  LocalStorage = class {
44194
46249
  directory;
44195
46250
  constructor(directory) {
44196
- this.directory = resolve19(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
44197
- mkdirSync20(this.directory, { recursive: true });
46251
+ this.directory = resolve20(directory || process.env.TINA4_STORAGE_DIR || "data/rt_storage");
46252
+ mkdirSync21(this.directory, { recursive: true });
44198
46253
  }
44199
46254
  // Resolve inside the root and reject any traversal attempt.
44200
46255
  pathFor(key) {
44201
- const target = resolve19(this.directory, key);
44202
- if (target !== this.directory && !target.startsWith(this.directory + sep5)) {
46256
+ const target = resolve20(this.directory, key);
46257
+ if (target !== this.directory && !target.startsWith(this.directory + sep6)) {
44203
46258
  throw new Error(`unsafe storage key: ${JSON.stringify(key)}`);
44204
46259
  }
44205
46260
  return target;
44206
46261
  }
44207
46262
  put(key, data) {
44208
- writeFileSync17(this.pathFor(key), data);
46263
+ writeFileSync18(this.pathFor(key), data);
44209
46264
  }
44210
46265
  get(key) {
44211
46266
  try {