tina4-nodejs 3.13.100 → 3.13.103

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.
@@ -1462,6 +1462,7 @@ __export(engine_exports, {
1462
1462
  Frond: () => Frond,
1463
1463
  MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
1464
1464
  TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
1465
+ expressionFormCache: () => expressionFormCache,
1465
1466
  filterChainCache: () => filterChainCache,
1466
1467
  pathParseCache: () => pathParseCache,
1467
1468
  setFormTokenSessionId: () => setFormTokenSessionId
@@ -1870,62 +1871,58 @@ function splitOutsideQuotes(expr, sep6) {
1870
1871
  parts.push(expr.slice(currentStart));
1871
1872
  return parts;
1872
1873
  }
1873
- function evalExpr(expr, context) {
1874
- expr = expr.trim();
1875
- if (expr.length >= 2) {
1876
- const q = expr[0];
1877
- if ((q === '"' || q === "'") && expr.endsWith(q) && !expr.slice(1, -1).includes(q)) {
1878
- return expr.slice(1, -1);
1879
- }
1880
- }
1881
- if (expr.length >= 2 && expr[0] === "(" && expr.endsWith(")")) {
1882
- let depth = 0;
1883
- let matched = true;
1884
- for (let pi = 0; pi < expr.length; pi++) {
1885
- if (expr[pi] === "(") depth++;
1886
- else if (expr[pi] === ")") depth--;
1887
- if (depth === 0 && pi < expr.length - 1) {
1888
- matched = false;
1889
- break;
1890
- }
1891
- }
1892
- if (matched) {
1893
- return evalExpr(expr.slice(1, -1), context);
1894
- }
1874
+ function parenthesizedInner(expr) {
1875
+ if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
1876
+ let depth = 0;
1877
+ for (let index = 0; index < expr.length; index++) {
1878
+ if (expr[index] === "(") depth++;
1879
+ else if (expr[index] === ")") depth--;
1880
+ if (depth === 0 && index < expr.length - 1) return null;
1895
1881
  }
1896
- const ternaryIdx = findTernary(expr);
1897
- if (ternaryIdx !== -1) {
1898
- const condPart = expr.slice(0, ternaryIdx).trim();
1899
- const rest = expr.slice(ternaryIdx + 1);
1900
- const colonIdx = findColon(rest);
1901
- if (colonIdx !== -1) {
1902
- const truePart = rest.slice(0, colonIdx).trim();
1903
- const falsePart = rest.slice(colonIdx + 1).trim();
1904
- const cond = evalExpr(condPart, context);
1905
- return cond ? evalExpr(truePart, context) : evalExpr(falsePart, context);
1906
- }
1882
+ return expr.slice(1, -1);
1883
+ }
1884
+ function evalPrimary(expr, context) {
1885
+ const quote = expr[0];
1886
+ if (expr.length >= 2 && (quote === '"' || quote === "'") && expr.endsWith(quote) && !expr.slice(1, -1).includes(quote)) {
1887
+ return expr.slice(1, -1);
1907
1888
  }
1889
+ const inner = parenthesizedInner(expr);
1890
+ if (inner !== null) return evalExpr(inner, context);
1891
+ return EXPR_NOT_MATCHED;
1892
+ }
1893
+ function evalTernaryExpression(expr, context) {
1894
+ const ternaryIdx = findTernary(expr);
1895
+ if (ternaryIdx === -1) return EXPR_NOT_MATCHED;
1896
+ const rest = expr.slice(ternaryIdx + 1);
1897
+ const colonIdx = findColon(rest);
1898
+ if (colonIdx === -1) return EXPR_NOT_MATCHED;
1899
+ const condition = evalExpr(expr.slice(0, ternaryIdx).trim(), context);
1900
+ const branch = condition ? rest.slice(0, colonIdx) : rest.slice(colonIdx + 1);
1901
+ return evalExpr(branch.trim(), context);
1902
+ }
1903
+ function evalInlineIfExpression(expr, context) {
1908
1904
  const ifIdx = findOutsideQuotes(expr, " if ");
1909
- if (ifIdx >= 0) {
1910
- const elseIdx = findOutsideQuotes(expr, " else ");
1911
- if (elseIdx >= 0 && elseIdx > ifIdx) {
1912
- const valuePart = expr.slice(0, ifIdx).trim();
1913
- const condPart = expr.slice(ifIdx + 4, elseIdx).trim();
1914
- const elsePart = expr.slice(elseIdx + 6).trim();
1915
- const cond = evalExpr(condPart, context);
1916
- return cond ? evalExpr(valuePart, context) : evalExpr(elsePart, context);
1917
- }
1918
- }
1905
+ if (ifIdx < 0) return EXPR_NOT_MATCHED;
1906
+ const elseIdx = findOutsideQuotes(expr, " else ");
1907
+ if (elseIdx < 0 || elseIdx <= ifIdx) return EXPR_NOT_MATCHED;
1908
+ const condition = evalExpr(expr.slice(ifIdx + 4, elseIdx).trim(), context);
1909
+ const branch = condition ? expr.slice(0, ifIdx) : expr.slice(elseIdx + 6);
1910
+ return evalExpr(branch.trim(), context);
1911
+ }
1912
+ function evalCoalesceExpression(expr, context) {
1919
1913
  const qqIdx = findOutsideQuotes(expr, "??");
1920
- if (qqIdx !== -1) {
1921
- const left = expr.slice(0, qqIdx).trim();
1922
- const right = expr.slice(qqIdx + 2).trim();
1923
- const val = evalExpr(left, context);
1924
- if (val === null || val === void 0) {
1925
- return evalExpr(right, context);
1926
- }
1927
- return val;
1914
+ if (qqIdx === -1) return EXPR_NOT_MATCHED;
1915
+ const value = evalExpr(expr.slice(0, qqIdx).trim(), context);
1916
+ return value === null || value === void 0 ? evalExpr(expr.slice(qqIdx + 2).trim(), context) : value;
1917
+ }
1918
+ function evalConditional(expr, context) {
1919
+ for (const evaluator of [evalTernaryExpression, evalInlineIfExpression, evalCoalesceExpression]) {
1920
+ const result = evaluator(expr, context);
1921
+ if (result !== EXPR_NOT_MATCHED) return result;
1928
1922
  }
1923
+ return EXPR_NOT_MATCHED;
1924
+ }
1925
+ function evalConcatOrComparison(expr, context) {
1929
1926
  if (findOutsideQuotes(expr, "~") >= 0) {
1930
1927
  const parts = splitOutsideQuotes(expr, "~");
1931
1928
  if (parts.length > 1) {
@@ -1943,6 +1940,9 @@ function evalExpr(expr, context) {
1943
1940
  return evalComparison(expr, context);
1944
1941
  }
1945
1942
  }
1943
+ return EXPR_NOT_MATCHED;
1944
+ }
1945
+ function evalArithmeticExpression(expr, context) {
1946
1946
  for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
1947
1947
  const pos = findOutsideQuotes(expr, op);
1948
1948
  if (pos >= 0) {
@@ -1955,40 +1955,15 @@ function evalExpr(expr, context) {
1955
1955
  let rNum = rVal != null ? Number(rVal) : 0;
1956
1956
  if (isNaN(lNum)) lNum = 0;
1957
1957
  if (isNaN(rNum)) rNum = 0;
1958
- const opS = op.trim();
1959
- const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
1960
- let result;
1961
- switch (opS) {
1962
- case "+":
1963
- result = lNum + rNum;
1964
- break;
1965
- case "-":
1966
- result = lNum - rNum;
1967
- break;
1968
- case "*":
1969
- result = lNum * rNum;
1970
- break;
1971
- case "//":
1972
- result = rNum !== 0 ? Math.floor(lNum / rNum) : 0;
1973
- break;
1974
- case "/":
1975
- result = rNum !== 0 ? lNum / rNum : 0;
1976
- break;
1977
- case "%":
1978
- result = rNum !== 0 ? lNum % rNum : 0;
1979
- break;
1980
- case "**":
1981
- result = lNum ** rNum;
1982
- break;
1983
- default:
1984
- result = 0;
1985
- }
1986
- return bothInt && Number.isInteger(result) ? result : result;
1958
+ return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
1987
1959
  } catch {
1988
1960
  return null;
1989
1961
  }
1990
1962
  }
1991
1963
  }
1964
+ return EXPR_NOT_MATCHED;
1965
+ }
1966
+ function evalFilterExpression(expr, context) {
1992
1967
  if (findOutsideQuotes(expr, "|") >= 0) {
1993
1968
  const [baseExpr, filters] = parseFilterChain(expr);
1994
1969
  if (filters.length > 0) {
@@ -2007,38 +1982,49 @@ function evalExpr(expr, context) {
2007
1982
  return value;
2008
1983
  }
2009
1984
  }
2010
- const fnMatch = expr.match(FN_CALL_RE);
2011
- if (fnMatch) {
2012
- const fnName = fnMatch[1];
2013
- const rawArgs = fnMatch[2] || "";
2014
- if (fnName.includes(".")) {
2015
- const lastDot = fnName.lastIndexOf(".");
2016
- const objPath = fnName.slice(0, lastDot);
2017
- const methodName = fnName.slice(lastDot + 1);
2018
- const obj = resolveVar(objPath, context);
2019
- if (obj && typeof obj === "object" && methodName in obj) {
2020
- const method = obj[methodName];
2021
- if (typeof method === "function") {
2022
- if (rawArgs.trim()) {
2023
- const parts = splitArgs(rawArgs);
2024
- const evalArgs = parts.map((a) => evalExpr(a.trim(), context));
2025
- return method.apply(obj, evalArgs);
2026
- }
2027
- return method.call(obj);
2028
- }
2029
- }
2030
- } else {
2031
- const fn = context[fnName] ?? resolveVar(fnName, context);
2032
- if (typeof fn === "function") {
2033
- if (rawArgs.trim()) {
2034
- const parts = splitArgs(rawArgs);
2035
- const evalArgs = parts.map((a) => evalExpr(a.trim(), context));
2036
- return fn(...evalArgs);
2037
- }
2038
- return fn();
2039
- }
1985
+ return EXPR_NOT_MATCHED;
1986
+ }
1987
+ function evaluateCallArgs(rawArgs, context) {
1988
+ return rawArgs.trim() ? splitArgs(rawArgs).map((arg) => evalExpr(arg.trim(), context)) : [];
1989
+ }
1990
+ function evalDottedFunction(name, rawArgs, context) {
1991
+ const lastDot = name.lastIndexOf(".");
1992
+ const owner = resolveVar(name.slice(0, lastDot), context);
1993
+ const member = name.slice(lastDot + 1);
1994
+ if (!owner || typeof owner !== "object" || !(member in owner)) {
1995
+ return EXPR_NOT_MATCHED;
1996
+ }
1997
+ const method = owner[member];
1998
+ return typeof method === "function" ? method.apply(owner, evaluateCallArgs(rawArgs, context)) : EXPR_NOT_MATCHED;
1999
+ }
2000
+ function evalFunctionExpression(expr, context) {
2001
+ const match = expr.match(FN_CALL_RE);
2002
+ if (!match) return EXPR_NOT_MATCHED;
2003
+ const name = match[1];
2004
+ const rawArgs = match[2] || "";
2005
+ if (name.includes(".")) return evalDottedFunction(name, rawArgs, context);
2006
+ const fn = context[name] ?? resolveVar(name, context);
2007
+ if (typeof fn === "function") return fn(...evaluateCallArgs(rawArgs, context));
2008
+ return EXPR_NOT_MATCHED;
2009
+ }
2010
+ function evalExpr(expr, context) {
2011
+ expr = expr.trim();
2012
+ const cachedForm = expressionFormCache.get(expr);
2013
+ if (cachedForm !== void 0) {
2014
+ if (cachedForm === -1) return resolveVar(expr, context);
2015
+ const result = EXPR_EVALUATORS[cachedForm](expr, context);
2016
+ return result === EXPR_NOT_MATCHED ? resolveVar(expr, context) : result;
2017
+ }
2018
+ for (let index = 0; index < EXPR_EVALUATORS.length; index++) {
2019
+ const result = EXPR_EVALUATORS[index](expr, context);
2020
+ if (result !== EXPR_NOT_MATCHED) {
2021
+ capCache(expressionFormCache, MEMO_CACHE_MAX);
2022
+ expressionFormCache.set(expr, index);
2023
+ return result;
2040
2024
  }
2041
2025
  }
2026
+ capCache(expressionFormCache, MEMO_CACHE_MAX);
2027
+ expressionFormCache.set(expr, FN_CALL_RE.test(expr) ? EXPR_EVALUATORS.length - 1 : -1);
2042
2028
  return resolveVar(expr, context);
2043
2029
  }
2044
2030
  function findTernary(expr) {
@@ -2494,7 +2480,7 @@ function _generateFormToken(descriptor = "") {
2494
2480
  function _generateFormTokenValue(descriptor = "") {
2495
2481
  return new SafeString(_buildFormTokenJwt(descriptor));
2496
2482
  }
2497
- var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
2483
+ var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, EXPR_NOT_MATCHED, ARITHMETIC_OPERATIONS, EXPR_EVALUATORS, expressionFormCache, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
2498
2484
  var init_engine = __esm({
2499
2485
  "../frond/src/engine.ts"() {
2500
2486
  "use strict";
@@ -2596,6 +2582,25 @@ var init_engine = __esm({
2596
2582
  MEMO_CACHE_MAX = 1024;
2597
2583
  TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
2598
2584
  RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
2585
+ EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
2586
+ ARITHMETIC_OPERATIONS = {
2587
+ "+": (left, right) => left + right,
2588
+ "-": (left, right) => left - right,
2589
+ "*": (left, right) => left * right,
2590
+ "//": (left, right) => right !== 0 ? Math.floor(left / right) : 0,
2591
+ "/": (left, right) => right !== 0 ? left / right : 0,
2592
+ "%": (left, right) => right !== 0 ? left % right : 0,
2593
+ "**": (left, right) => left ** right
2594
+ };
2595
+ EXPR_EVALUATORS = [
2596
+ evalPrimary,
2597
+ evalConditional,
2598
+ evalConcatOrComparison,
2599
+ evalArithmeticExpression,
2600
+ evalFilterExpression,
2601
+ evalFunctionExpression
2602
+ ];
2603
+ expressionFormCache = /* @__PURE__ */ new Map();
2599
2604
  VarRef = class {
2600
2605
  constructor(name) {
2601
2606
  this.name = name;
@@ -19566,14 +19571,14 @@ async function discoverRoutes(routesDir) {
19566
19571
  const currentMtime = statSync7(filePath).mtimeMs;
19567
19572
  if (_seenFiles.has(filePath) && _seenMtimes.get(filePath) === currentMtime) continue;
19568
19573
  const method = name.toUpperCase();
19569
- const relativePath3 = relative(routesDir, filePath);
19570
- const pattern = filePathToPattern(relativePath3);
19574
+ const relativePath2 = relative(routesDir, filePath);
19575
+ const pattern = filePathToPattern(relativePath2);
19571
19576
  try {
19572
19577
  const moduleUrl = `file://${filePath}?t=${currentMtime}`;
19573
19578
  const mod = await import(moduleUrl);
19574
19579
  const handler = mod.default ?? mod.handler;
19575
19580
  if (typeof handler !== "function") {
19576
- console.warn(` Warning: ${relativePath3} does not export a handler function, skipping`);
19581
+ console.warn(` Warning: ${relativePath2} does not export a handler function, skipping`);
19577
19582
  continue;
19578
19583
  }
19579
19584
  const meta = mod.meta;
@@ -19585,7 +19590,7 @@ async function discoverRoutes(routesDir) {
19585
19590
  _seenMtimes.set(filePath, currentMtime);
19586
19591
  registeredFromThisScan++;
19587
19592
  } catch (err) {
19588
- console.error(` Error loading route ${relativePath3}:`, err);
19593
+ console.error(` Error loading route ${relativePath2}:`, err);
19589
19594
  recordBrokenImport(filePath, err);
19590
19595
  }
19591
19596
  }
@@ -19619,8 +19624,8 @@ function recordBrokenImport(filePath, error) {
19619
19624
  } catch {
19620
19625
  }
19621
19626
  }
19622
- function filePathToPattern(relativePath3) {
19623
- const parts = relativePath3.replace(/\\/g, "/").split("/").slice(0, -1);
19627
+ function filePathToPattern(relativePath2) {
19628
+ const parts = relativePath2.replace(/\\/g, "/").split("/").slice(0, -1);
19624
19629
  const urlParts = parts.map((part) => {
19625
19630
  if (part.startsWith("[...") && part.endsWith("]")) {
19626
19631
  const name = part.slice(4, -1);
@@ -22857,497 +22862,135 @@ import * as fs3 from "node:fs";
22857
22862
  import * as path2 from "node:path";
22858
22863
  import { spawnSync } from "node:child_process";
22859
22864
  import { fileURLToPath } from "node:url";
22860
- function walkFiles(dir, extensions, exclude = ["node_modules", ".git", "dist", "build"]) {
22861
- const results = [];
22862
- if (!fs3.existsSync(dir)) return results;
22863
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
22864
- for (const entry of entries) {
22865
- const fullPath = path2.join(dir, entry.name);
22866
- if (entry.isDirectory()) {
22867
- if (!exclude.includes(entry.name)) {
22868
- results.push(...walkFiles(fullPath, extensions, exclude));
22869
- }
22870
- } else if (entry.isFile()) {
22871
- const ext = path2.extname(entry.name);
22872
- if (extensions.includes(ext) && !entry.name.endsWith(".d.ts")) {
22873
- results.push(fullPath);
22874
- }
22875
- }
22876
- }
22877
- return results;
22878
- }
22879
- function readFileSafe(filePath) {
22880
- try {
22881
- return fs3.readFileSync(filePath, "utf-8");
22882
- } catch {
22883
- return null;
22884
- }
22885
- }
22886
- function relativePath(filePath, root = ".") {
22887
- return path2.relative(root, filePath);
22888
- }
22889
- function countLines(source) {
22890
- const lines = source.split("\n");
22891
- let loc = 0;
22892
- let blank = 0;
22893
- let comment = 0;
22894
- let inBlockComment = false;
22895
- for (const line of lines) {
22896
- const stripped = line.trim();
22897
- if (!stripped) {
22898
- blank++;
22899
- continue;
22900
- }
22901
- if (inBlockComment) {
22902
- comment++;
22903
- if (stripped.includes("*/")) {
22904
- inBlockComment = false;
22905
- }
22906
- continue;
22907
- }
22908
- if (stripped.startsWith("/*")) {
22909
- comment++;
22910
- if (!stripped.includes("*/") || stripped.endsWith("/*")) {
22911
- inBlockComment = true;
22912
- }
22913
- continue;
22914
- }
22915
- if (stripped.startsWith("//")) {
22916
- comment++;
22917
- continue;
22918
- }
22919
- loc++;
22920
- }
22921
- return { loc, blank, comment };
22922
- }
22923
- function stripLiterals(source) {
22924
- const out = [];
22925
- const n = source.length;
22926
- let i = 0;
22927
- let prevSignificant = "";
22928
- let prevWord = "";
22929
- const regexKeywords = /* @__PURE__ */ new Set([
22930
- "return",
22931
- "typeof",
22932
- "instanceof",
22933
- "in",
22934
- "of",
22935
- "new",
22936
- "delete",
22937
- "void",
22938
- "throw",
22939
- "case",
22940
- "do",
22941
- "else",
22942
- "yield",
22943
- "await"
22944
- ]);
22945
- function prevEndsExpression() {
22946
- if (prevSignificant === "") return false;
22947
- if (/[A-Za-z0-9_$]/.test(prevSignificant)) {
22948
- return !regexKeywords.has(prevWord);
22949
- }
22950
- if (prevSignificant === ")" || prevSignificant === "]") return true;
22951
- if (prevSignificant === ".") return true;
22952
- return false;
22953
- }
22954
- while (i < n) {
22955
- const ch = source[i];
22956
- const next = i + 1 < n ? source[i + 1] : "";
22957
- if (ch === "/" && next === "/") {
22958
- out.push("//");
22959
- i += 2;
22960
- while (i < n && source[i] !== "\n") {
22961
- out.push(" ");
22962
- i++;
22963
- }
22964
- continue;
22965
- }
22966
- if (ch === "/" && next === "*") {
22967
- out.push("/*");
22968
- i += 2;
22969
- while (i < n && !(source[i] === "*" && source[i + 1] === "/")) {
22970
- out.push(source[i] === "\n" ? "\n" : " ");
22971
- i++;
22972
- }
22973
- if (i < n) {
22974
- out.push("*/");
22975
- i += 2;
22976
- }
22977
- continue;
22978
- }
22979
- if (ch === '"' || ch === "'") {
22980
- const quote = ch;
22981
- out.push(quote);
22982
- i++;
22983
- while (i < n && source[i] !== quote) {
22984
- if (source[i] === "\\" && i + 1 < n) {
22985
- out.push(" ");
22986
- i += 2;
22987
- continue;
22988
- }
22989
- if (source[i] === "\n") {
22990
- out.push("\n");
22991
- i++;
22992
- break;
22993
- }
22994
- out.push(" ");
22995
- i++;
22996
- }
22997
- if (i < n && source[i] === quote) {
22998
- out.push(quote);
22999
- i++;
23000
- }
23001
- prevSignificant = quote;
23002
- prevWord = "";
23003
- continue;
23004
- }
23005
- if (ch === "`") {
23006
- out.push("`");
23007
- i++;
23008
- while (i < n && source[i] !== "`") {
23009
- if (source[i] === "\\" && i + 1 < n) {
23010
- out.push(source[i + 1] === "\n" ? " \n" : " ");
23011
- i += 2;
23012
- continue;
23013
- }
23014
- if (source[i] === "$" && source[i + 1] === "{") {
23015
- out.push("${");
23016
- i += 2;
23017
- let depth = 1;
23018
- const exprStart = i;
23019
- while (i < n && depth > 0) {
23020
- if (source[i] === "{") depth++;
23021
- else if (source[i] === "}") depth--;
23022
- if (depth === 0) break;
23023
- i++;
23024
- }
23025
- out.push(stripLiterals(source.slice(exprStart, i)));
23026
- if (i < n && source[i] === "}") {
23027
- out.push("}");
23028
- i++;
23029
- }
23030
- continue;
23031
- }
23032
- out.push(source[i] === "\n" ? "\n" : " ");
23033
- i++;
23034
- }
23035
- if (i < n && source[i] === "`") {
23036
- out.push("`");
23037
- i++;
23038
- }
23039
- prevSignificant = "`";
23040
- prevWord = "";
23041
- continue;
23042
- }
23043
- if (ch === "/" && !prevEndsExpression()) {
23044
- let j = i + 1;
23045
- let ok = false;
23046
- let inClass = false;
23047
- while (j < n) {
23048
- const c = source[j];
23049
- if (c === "\\") {
23050
- j += 2;
23051
- continue;
23052
- }
23053
- if (c === "\n") break;
23054
- if (c === "[") inClass = true;
23055
- else if (c === "]") inClass = false;
23056
- else if (c === "/" && !inClass) {
23057
- ok = true;
23058
- break;
23059
- }
23060
- j++;
23061
- }
23062
- if (ok) {
23063
- out.push("/");
23064
- for (let k = i + 1; k < j; k++) out.push(" ");
23065
- out.push("/");
23066
- i = j + 1;
23067
- while (i < n && /[a-z]/i.test(source[i])) {
23068
- out.push(source[i]);
23069
- i++;
23070
- }
23071
- prevSignificant = "/";
23072
- prevWord = "";
23073
- continue;
23074
- }
23075
- }
23076
- out.push(ch);
23077
- if (!/\s/.test(ch)) {
23078
- prevSignificant = ch;
23079
- if (/[A-Za-z0-9_$]/.test(ch)) {
23080
- prevWord = /[A-Za-z0-9_$]/.test(source[i - 1] ?? "") ? prevWord + ch : ch;
23081
- } else {
23082
- prevWord = "";
23083
- }
23084
- }
23085
- i++;
23086
- }
23087
- return out.join("");
23088
- }
23089
- function countClassesQuick(source) {
23090
- const matches = source.match(
23091
- /(?:^|\n)\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/g
23092
- );
23093
- return matches ? matches.length : 0;
23094
- }
23095
- function countFunctionsQuick(source) {
23096
- const clean = stripLiterals(source);
23097
- let count = 0;
23098
- const funcDecls = clean.match(
23099
- /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(/g
23100
- );
23101
- if (funcDecls) count += funcDecls.length;
23102
- const methods = clean.match(
23103
- /(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*\([^)]*\)\s*(?::\s*\S+)?\s*\{/g
23104
- );
23105
- if (methods) count += methods.length;
23106
- const arrows = clean.match(
23107
- /(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\(/g
23108
- );
23109
- if (arrows) count += arrows.length;
23110
- return count;
23111
- }
23112
- function resolveRoot(root = "src") {
23113
- const rootPath = path2.resolve(root);
23114
- if (fs3.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
23115
- _lastScanRoot = rootPath;
23116
- return root;
23117
- }
23118
- const fwDir = path2.resolve(path2.dirname(new URL(import.meta.url).pathname));
23119
- _lastScanRoot = fwDir;
23120
- return fwDir;
23121
- }
23122
- function quickMetrics(root = "src") {
23123
- root = resolveRoot(root);
23124
- const rootPath = path2.resolve(root);
23125
- if (!fs3.existsSync(rootPath)) {
23126
- return { error: `Directory not found: ${root}` };
23127
- }
23128
- const tsFiles = walkFiles(rootPath, [".ts", ".js"]);
23129
- const twigFiles = walkFiles(rootPath, [".twig", ".html"]);
23130
- const migrationsDir = path2.resolve("migrations");
23131
- const migrationFiles = [
23132
- ...walkFiles(migrationsDir, [".sql"]),
23133
- ...walkFiles(migrationsDir, [".ts"])
23134
- ];
23135
- const scssFiles = walkFiles(rootPath, [".scss", ".css"]);
23136
- let totalLoc = 0;
23137
- let totalBlank = 0;
23138
- let totalComment = 0;
23139
- let totalClasses = 0;
23140
- let totalFunctions = 0;
23141
- const fileDetails = [];
23142
- for (const f of tsFiles) {
23143
- const source = readFileSafe(f);
23144
- if (source === null) continue;
23145
- const counts = countLines(source);
23146
- const classes = countClassesQuick(source);
23147
- const functions = countFunctionsQuick(source);
23148
- totalLoc += counts.loc;
23149
- totalBlank += counts.blank;
23150
- totalComment += counts.comment;
23151
- totalClasses += classes;
23152
- totalFunctions += functions;
23153
- fileDetails.push({
23154
- path: relativePath(f, rootPath),
23155
- loc: counts.loc,
23156
- blank: counts.blank,
23157
- comment: counts.comment,
23158
- classes,
23159
- functions
23160
- });
22865
+ function containsTypeScript(directory) {
22866
+ if (!fs3.existsSync(directory) || !fs3.statSync(directory).isDirectory()) return false;
22867
+ for (const entry of fs3.readdirSync(directory, { withFileTypes: true })) {
22868
+ if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
22869
+ const target = path2.join(directory, entry.name);
22870
+ if (entry.isDirectory() ? containsTypeScript(target) : /\.[cm]?[jt]sx?$/.test(entry.name)) return true;
23161
22871
  }
23162
- fileDetails.sort((a, b) => b.loc - a.loc);
23163
- let routeCount = 0;
23164
- let ormCount = 0;
23165
- for (const f of tsFiles) {
23166
- const source = readFileSafe(f);
23167
- if (source === null) continue;
23168
- const routes = source.match(
23169
- /(?:router\s*\.\s*(?:get|post|put|delete|patch|any)\s*\(|@(?:get|post|put|delete|patch)\s*\()/g
23170
- );
23171
- if (routes) routeCount += routes.length;
23172
- const orms = source.match(
23173
- /class\s+\w+\s+extends\s+(?:ORM|Model)\b/g
23174
- );
23175
- if (orms) ormCount += orms.length;
23176
- }
23177
- const breakdown = {
23178
- typescript: tsFiles.filter((f) => f.endsWith(".ts")).length,
23179
- javascript: tsFiles.filter((f) => f.endsWith(".js")).length,
23180
- templates: twigFiles.length,
23181
- migrations: migrationFiles.length,
23182
- stylesheets: scssFiles.length
23183
- };
23184
- return {
23185
- file_count: tsFiles.length,
23186
- total_loc: totalLoc,
23187
- total_blank: totalBlank,
23188
- total_comment: totalComment,
23189
- lloc: totalLoc,
23190
- classes: totalClasses,
23191
- functions: totalFunctions,
23192
- route_count: routeCount,
23193
- orm_count: ormCount,
23194
- template_count: twigFiles.length,
23195
- migration_count: migrationFiles.length,
23196
- avg_file_size: tsFiles.length > 0 ? Math.round(totalLoc / tsFiles.length * 10) / 10 : 0,
23197
- largest_files: fileDetails.slice(0, 10),
23198
- breakdown
23199
- };
22872
+ return false;
23200
22873
  }
23201
- function resolveScanTarget(root = "src") {
23202
- const resolved = resolveRoot(root);
23203
- const frameworkDir = path2.dirname(fileURLToPath(import.meta.url));
23204
- const real = path2.resolve(resolved);
23205
- const scanningFramework = real === frameworkDir || real.startsWith(frameworkDir);
23206
- return [resolved, scanningFramework ? "framework" : "project"];
22874
+ function resolveTarget(root = "src") {
22875
+ const resolved = containsTypeScript(root) ? path2.resolve(root) : path2.dirname(fileURLToPath(import.meta.url));
22876
+ const mode = containsTypeScript(root) ? "project" : "framework";
22877
+ lastScanRoot = resolved;
22878
+ return [resolved, mode];
23207
22879
  }
23208
22880
  function enginePath() {
23209
- const names = process.platform === "win32" ? ["tina4.exe", "tina4.cmd", "tina4"] : ["tina4"];
23210
- for (const dir of (process.env.PATH || "").split(path2.delimiter)) {
23211
- if (!dir) continue;
22881
+ const names = process.platform === "win32" ? ["tina4.exe", "tina4"] : ["tina4"];
22882
+ for (const directory of (process.env.PATH || "").split(path2.delimiter)) {
23212
22883
  for (const name of names) {
23213
- const candidate = path2.join(dir, name);
22884
+ const candidate = path2.join(directory, name);
23214
22885
  try {
23215
- if (!fs3.statSync(candidate).isFile()) continue;
23216
22886
  fs3.accessSync(candidate, fs3.constants.X_OK);
22887
+ if (!fs3.statSync(candidate).isFile()) continue;
22888
+ const descriptor = fs3.openSync(candidate, "r");
22889
+ const header = Buffer.alloc(2);
22890
+ fs3.readSync(descriptor, header, 0, 2, 0);
22891
+ fs3.closeSync(descriptor);
22892
+ if (header.toString("latin1") !== "#!") return candidate;
23217
22893
  } catch {
23218
22894
  continue;
23219
22895
  }
23220
- try {
23221
- const fd = fs3.openSync(candidate, "r");
23222
- const buf = Buffer.alloc(2);
23223
- fs3.readSync(fd, buf, 0, 2, 0);
23224
- fs3.closeSync(fd);
23225
- if (buf.toString("latin1") === "#!") continue;
23226
- } catch {
23227
- }
23228
- return candidate;
23229
22896
  }
23230
22897
  }
23231
22898
  return null;
23232
22899
  }
23233
22900
  function runEngine(target) {
23234
22901
  const binary = enginePath();
23235
- if (binary === null) {
23236
- throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
23237
- }
23238
- const proc = spawnSync(binary, ["metrics", "--path", target, "--json"], {
22902
+ if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
22903
+ const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
23239
22904
  encoding: "utf8",
23240
- timeout: TIMEOUT_MS,
22905
+ timeout: 6e4,
23241
22906
  maxBuffer: 64 * 1024 * 1024
23242
22907
  });
23243
- if (proc.error) {
23244
- const err = proc.error;
23245
- if (err.code === "ETIMEDOUT") {
23246
- throw new MetricsEngineError(`tina4 metrics timed out after ${TIMEOUT_MS / 1e3}s on ${target}`);
23247
- }
23248
- throw new MetricsEngineError(`could not run ${binary}: ${err.message}`);
23249
- }
23250
- if (proc.status !== 0) {
23251
- const detail = (proc.stderr || proc.stdout || "").trim().split("\n")[0];
23252
- throw new MetricsEngineError(
23253
- `tina4 metrics failed on ${target}: ${detail || `exit code ${proc.status}`}`
23254
- );
22908
+ if (processResult.error) {
22909
+ throw new MetricsEngineError(`could not run ${binary}: ${processResult.error.message}`);
23255
22910
  }
23256
- if (!proc.stdout || !proc.stdout.trim()) {
23257
- throw new MetricsEngineError(`tina4 metrics produced no output for ${target}`);
22911
+ if (processResult.status !== 0) {
22912
+ const detail = (processResult.stderr || processResult.stdout || "").trim().split("\n")[0];
22913
+ throw new MetricsEngineError(`tina4 metrics failed on ${target}: ${detail || processResult.status}`);
23258
22914
  }
23259
- let payload;
23260
22915
  try {
23261
- payload = JSON.parse(proc.stdout);
23262
- } catch (e) {
23263
- throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${e.message}`);
23264
- }
23265
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
23266
- throw new MetricsEngineError("tina4 metrics returned a non-object payload");
22916
+ const payload = JSON.parse(processResult.stdout);
22917
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
22918
+ throw new Error("non-object payload");
22919
+ }
22920
+ return payload;
22921
+ } catch (error) {
22922
+ throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${error.message}`);
23267
22923
  }
23268
- return payload;
23269
22924
  }
23270
- function requireKey(payload, key, isArray) {
23271
- const value = payload[key];
23272
- const ok = isArray ? Array.isArray(value) : value !== null && typeof value === "object" && !Array.isArray(value);
23273
- if (!ok) {
23274
- throw new MetricsEngineError(
23275
- `engine payload has no usable '${key}' - the installed tina4 CLI predates a field the dashboard renders. Update it: ${INSTALL_HINT}`
23276
- );
22925
+ function requireArray(payload, key) {
22926
+ if (!Array.isArray(payload[key])) {
22927
+ throw new MetricsEngineError(`engine payload has no usable '${key}' - ${INSTALL_HINT}`);
23277
22928
  }
23278
- return value;
22929
+ return payload[key];
23279
22930
  }
23280
22931
  function fullAnalysis(root = "src") {
23281
- const [resolved, scanMode] = resolveScanTarget(root);
22932
+ const [resolved, scanMode] = resolveTarget(root);
23282
22933
  const payload = runEngine(resolved);
23283
- const summary = requireKey(payload, "summary", false);
23284
- const fileMetrics = requireKey(payload, "file_metrics", true);
23285
- const functions = requireKey(payload, "most_complex_functions", true);
23286
- const missing = SUMMARY_KEYS.filter((k) => !(k in summary));
23287
- if (missing.length) {
23288
- throw new MetricsEngineError(
23289
- `engine summary is missing ${missing.join(", ")} - update the CLI: ${INSTALL_HINT}`
23290
- );
23291
- }
23292
- if (fileMetrics.length) {
23293
- const absent = FILE_KEYS.filter((k) => !(k in fileMetrics[0]));
23294
- if (absent.length) throw new MetricsEngineError(`engine file_metrics is missing ${absent.join(", ")}`);
22934
+ const summary = payload.summary;
22935
+ if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
22936
+ throw new MetricsEngineError(`engine payload has no usable 'summary' - ${INSTALL_HINT}`);
22937
+ }
22938
+ const fileMetrics = requireArray(payload, "file_metrics");
22939
+ const functions = requireArray(payload, "most_complex_functions");
22940
+ const missingSummary = SUMMARY_KEYS.filter((key) => !(key in summary));
22941
+ if (missingSummary.length) throw new MetricsEngineError(`engine summary is missing ${missingSummary.join(", ")}`);
22942
+ const missingFile = fileMetrics.length ? FILE_KEYS.filter((key) => !(key in fileMetrics[0])) : [];
22943
+ if (missingFile.length) throw new MetricsEngineError(`engine file_metrics is missing ${missingFile.join(", ")}`);
22944
+ const missingFunction = functions.length ? FUNCTION_KEYS.filter((key) => !(key in functions[0])) : [];
22945
+ if (missingFunction.length) {
22946
+ throw new MetricsEngineError(`engine function metrics are missing ${missingFunction.join(", ")}`);
23295
22947
  }
23296
- if (functions.length) {
23297
- const absent = FUNCTION_KEYS.filter((k) => !(k in functions[0]));
23298
- if (absent.length) throw new MetricsEngineError(`engine function metrics are missing ${absent.join(", ")}`);
23299
- }
23300
- const result = {};
23301
- for (const key of SUMMARY_KEYS) result[key] = summary[key];
23302
- result.file_metrics = fileMetrics;
23303
- result.most_complex_functions = functions.slice(0, 15);
23304
- result.dependency_graph = payload.dependency_graph || {};
23305
- result.scan_mode = scanMode;
23306
- result.scan_root = path2.resolve(resolved);
23307
- result.engine = "tina4-cli";
23308
- return result;
22948
+ return {
22949
+ ...Object.fromEntries(SUMMARY_KEYS.map((key) => [key, summary[key]])),
22950
+ file_metrics: fileMetrics,
22951
+ most_complex_functions: functions.slice(0, 15),
22952
+ dependency_graph: payload.dependency_graph || {},
22953
+ scan_mode: scanMode,
22954
+ scan_root: resolved,
22955
+ engine: "tina4-cli"
22956
+ };
23309
22957
  }
23310
22958
  function fileDetail(filePath) {
23311
22959
  if (!filePath) throw new MetricsEngineError("fileDetail needs a path");
23312
22960
  let target = filePath;
23313
- if (!fs3.existsSync(target) && _lastScanRoot) {
23314
- const candidate = path2.join(_lastScanRoot, filePath);
23315
- if (fs3.existsSync(candidate)) target = candidate;
23316
- }
22961
+ if (!fs3.existsSync(target) && lastScanRoot) target = path2.join(lastScanRoot, filePath);
23317
22962
  if (!fs3.existsSync(target)) throw new MetricsEngineError(`no such file: ${filePath}`);
23318
22963
  if (fs3.statSync(target).isDirectory()) throw new MetricsEngineError(`not a file: ${filePath}`);
23319
22964
  const payload = runEngine(target);
23320
- const fileMetrics = requireKey(payload, "file_metrics", true);
23321
- if (!fileMetrics.length) {
23322
- throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
23323
- }
23324
- return { ...fileMetrics[0], engine: "tina4-cli" };
22965
+ const files = requireArray(payload, "file_metrics");
22966
+ if (!files.length) throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
22967
+ return {
22968
+ ...files[0],
22969
+ function_count: files[0].functions || 0,
22970
+ functions: requireArray(payload, "most_complex_functions"),
22971
+ engine: "tina4-cli"
22972
+ };
23325
22973
  }
23326
- var _lastScanRoot, MetricsEngineError, TIMEOUT_MS, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
22974
+ var lastScanRoot, MetricsEngineError, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
23327
22975
  var init_metrics = __esm({
23328
22976
  "src/metrics.ts"() {
23329
22977
  "use strict";
23330
- _lastScanRoot = "";
22978
+ lastScanRoot = "";
23331
22979
  MetricsEngineError = class extends Error {
23332
22980
  constructor(message) {
23333
22981
  super(message);
23334
22982
  this.name = "MetricsEngineError";
23335
22983
  }
23336
22984
  };
23337
- TIMEOUT_MS = 6e4;
23338
- INSTALL_HINT = [
23339
- "the tina4 CLI provides the metrics engine (ADR-0002). Install it with",
23340
- " curl -fsSL https://tina4.com/install.sh | sh",
23341
- "or see https://tina4.com/cli"
23342
- ].join("\n");
22985
+ INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
23343
22986
  SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
23344
- FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_tests"];
22987
+ FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_referencing_test"];
23345
22988
  FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
23346
22989
  }
23347
22990
  });
23348
22991
 
23349
22992
  // src/feedback.ts
23350
- import { readFileSync as readFileSync13, existsSync as existsSync13 } from "node:fs";
22993
+ import { readFileSync as readFileSync12, existsSync as existsSync13 } from "node:fs";
23351
22994
  import { dirname as dirname7, join as join18, resolve as resolve9 } from "node:path";
23352
22995
  import { fileURLToPath as fileURLToPath2 } from "node:url";
23353
22996
  function feedbackEnabled() {
@@ -23488,7 +23131,7 @@ var init_feedback = __esm({
23488
23131
  handleFeedbackWidgetJs = (_req, res) => {
23489
23132
  let body;
23490
23133
  if (existsSync13(WIDGET_BUNDLE_PATH)) {
23491
- body = readFileSync13(WIDGET_BUNDLE_PATH);
23134
+ body = readFileSync12(WIDGET_BUNDLE_PATH);
23492
23135
  } else {
23493
23136
  body = "console.warn('tina4-feedback-widget bundle not built yet');";
23494
23137
  }
@@ -23503,7 +23146,7 @@ var init_feedback = __esm({
23503
23146
  });
23504
23147
 
23505
23148
  // src/version.ts
23506
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
23149
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
23507
23150
  import { dirname as dirname8, join as join19 } from "node:path";
23508
23151
  import { fileURLToPath as fileURLToPath3 } from "node:url";
23509
23152
  function resolveFrameworkVersion() {
@@ -23512,7 +23155,7 @@ function resolveFrameworkVersion() {
23512
23155
  const pkgPath = join19(dir, "package.json");
23513
23156
  if (existsSync14(pkgPath)) {
23514
23157
  try {
23515
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
23158
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
23516
23159
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
23517
23160
  } catch {
23518
23161
  }
@@ -26300,8 +25943,8 @@ __export(context_exports, {
26300
25943
  fts5Supported: () => fts5Supported
26301
25944
  });
26302
25945
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
26303
- import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync16, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
26304
- import { basename as basename4, dirname as dirname10, extname as extname6, isAbsolute as isAbsolute6, join as join21, relative as relative4, resolve as resolve11 } from "node:path";
25946
+ import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync15, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
25947
+ import { basename as basename4, dirname as dirname10, extname as extname5, isAbsolute as isAbsolute6, join as join21, relative as relative3, resolve as resolve11 } from "node:path";
26305
25948
  function fts5Supported() {
26306
25949
  try {
26307
25950
  const conn = new DatabaseSync4(":memory:");
@@ -26436,7 +26079,7 @@ var init_context = __esm({
26436
26079
  }
26437
26080
  // ── indexing ───────────────────────────────────────────────
26438
26081
  static chunksFor(label, text) {
26439
- const ext = extname6(label).toLowerCase();
26082
+ const ext = extname5(label).toLowerCase();
26440
26083
  const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
26441
26084
  if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
26442
26085
  return chunkCode(text, label);
@@ -26454,7 +26097,7 @@ var init_context = __esm({
26454
26097
  const stored = label != null ? String(label) : String(file);
26455
26098
  let text;
26456
26099
  try {
26457
- text = readFileSync16(file, "utf-8");
26100
+ text = readFileSync15(file, "utf-8");
26458
26101
  } catch {
26459
26102
  return 0;
26460
26103
  }
@@ -26475,7 +26118,7 @@ var init_context = __esm({
26475
26118
  static eligible(filename) {
26476
26119
  const fn = filename.toLowerCase();
26477
26120
  if (fn.endsWith(".min.js")) return false;
26478
- const ext = extname6(fn);
26121
+ const ext = extname5(fn);
26479
26122
  return CODE_EXTS.has(ext) || DOC_EXTS.has(ext) || CONFIG_EXTS.has(ext) || SPECIAL_FILES.has(fn);
26480
26123
  }
26481
26124
  /**
@@ -26504,7 +26147,7 @@ var init_context = __esm({
26504
26147
  for (const fn of files) {
26505
26148
  if (!_Context.eligible(fn)) continue;
26506
26149
  const full = join21(dir, fn);
26507
- const rel = relative4(rootAbs, full);
26150
+ const rel = relative3(rootAbs, full);
26508
26151
  total += this.indexPath(full, rel);
26509
26152
  }
26510
26153
  for (const d of subdirs) walk2(join21(dir, d));
@@ -26525,7 +26168,7 @@ var init_context = __esm({
26525
26168
  const raw = String(changedPath);
26526
26169
  const abs = isAbsolute6(raw) ? raw : join21(process.cwd(), raw);
26527
26170
  const resolved = realResolve(resolve11(abs));
26528
- const rel = relative4(this.root, resolved);
26171
+ const rel = relative3(this.root, resolved);
26529
26172
  if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) {
26530
26173
  return -1;
26531
26174
  }
@@ -28447,7 +28090,7 @@ var init_job = __esm({
28447
28090
  });
28448
28091
 
28449
28092
  // src/queueBackends/liteBackend.ts
28450
- import { mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7, existsSync as existsSync17 } from "node:fs";
28093
+ import { mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7, existsSync as existsSync17 } from "node:fs";
28451
28094
  import { join as join22 } from "node:path";
28452
28095
  import { randomUUID as randomUUID6 } from "node:crypto";
28453
28096
  var LiteBackend;
@@ -28547,7 +28190,7 @@ var init_liteBackend = __esm({
28547
28190
  const filePath = join22(dir, filename);
28548
28191
  let job;
28549
28192
  try {
28550
- job = JSON.parse(readFileSync17(filePath, "utf-8"));
28193
+ job = JSON.parse(readFileSync16(filePath, "utf-8"));
28551
28194
  } catch {
28552
28195
  continue;
28553
28196
  }
@@ -28611,7 +28254,7 @@ var init_liteBackend = __esm({
28611
28254
  const filePath = join22(reservedDir, filename);
28612
28255
  let record;
28613
28256
  try {
28614
- record = JSON.parse(readFileSync17(filePath, "utf-8"));
28257
+ record = JSON.parse(readFileSync16(filePath, "utf-8"));
28615
28258
  } catch {
28616
28259
  continue;
28617
28260
  }
@@ -28724,7 +28367,7 @@ var init_liteBackend = __esm({
28724
28367
  let count = 0;
28725
28368
  for (const file of files) {
28726
28369
  try {
28727
- const job = JSON.parse(readFileSync17(join22(scanDir, file), "utf-8"));
28370
+ const job = JSON.parse(readFileSync16(join22(scanDir, file), "utf-8"));
28728
28371
  if (job.status === status2) count++;
28729
28372
  } catch {
28730
28373
  }
@@ -28781,7 +28424,7 @@ var init_liteBackend = __esm({
28781
28424
  const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data")).sort();
28782
28425
  for (const file of files) {
28783
28426
  try {
28784
- const job = JSON.parse(readFileSync17(join22(dir, file), "utf-8"));
28427
+ const job = JSON.parse(readFileSync16(join22(dir, file), "utf-8"));
28785
28428
  const attempts = job.attempts || 0;
28786
28429
  if (attempts > 0 && attempts < maxRetries) {
28787
28430
  results.push(job);
@@ -28807,7 +28450,7 @@ var init_liteBackend = __esm({
28807
28450
  const failedDir = join22(this.basePath, q, "failed");
28808
28451
  const filePath = join22(failedDir, `${jobId}.queue-data`);
28809
28452
  if (existsSync17(filePath)) {
28810
- const job = JSON.parse(readFileSync17(filePath, "utf-8"));
28453
+ const job = JSON.parse(readFileSync16(filePath, "utf-8"));
28811
28454
  job.status = "pending";
28812
28455
  job.attempts = (job.attempts || 0) + 1;
28813
28456
  job.error = void 0;
@@ -28831,7 +28474,7 @@ var init_liteBackend = __esm({
28831
28474
  const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
28832
28475
  for (const file of files) {
28833
28476
  try {
28834
- const job = JSON.parse(readFileSync17(join22(failedDir, file), "utf-8"));
28477
+ const job = JSON.parse(readFileSync16(join22(failedDir, file), "utf-8"));
28835
28478
  if ((job.attempts || 0) >= maxRetries) {
28836
28479
  job.status = "dead";
28837
28480
  results.push(job);
@@ -28865,7 +28508,7 @@ var init_liteBackend = __esm({
28865
28508
  const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
28866
28509
  for (const file of files) {
28867
28510
  try {
28868
- const job = JSON.parse(readFileSync17(join22(dir, file), "utf-8"));
28511
+ const job = JSON.parse(readFileSync16(join22(dir, file), "utf-8"));
28869
28512
  if (job.status === status2) {
28870
28513
  unlinkSync7(join22(dir, file));
28871
28514
  count++;
@@ -28892,7 +28535,7 @@ var init_liteBackend = __esm({
28892
28535
  for (const file of files) {
28893
28536
  try {
28894
28537
  const filePath = join22(failedDir, file);
28895
- const job = JSON.parse(readFileSync17(filePath, "utf-8"));
28538
+ const job = JSON.parse(readFileSync16(filePath, "utf-8"));
28896
28539
  if ((job.attempts || 0) >= maxRetries) {
28897
28540
  continue;
28898
28541
  }
@@ -28924,7 +28567,7 @@ var init_liteBackend = __esm({
28924
28567
  const filePath = join22(dir, file);
28925
28568
  let job;
28926
28569
  try {
28927
- job = JSON.parse(readFileSync17(filePath, "utf-8"));
28570
+ job = JSON.parse(readFileSync16(filePath, "utf-8"));
28928
28571
  } catch {
28929
28572
  continue;
28930
28573
  }
@@ -30387,7 +30030,7 @@ function detectVersion(projectRoot3) {
30387
30030
  }
30388
30031
  return "0.0.0";
30389
30032
  }
30390
- function relativePath2(absPath, projectRoot3, frameworkRoots) {
30033
+ function relativePath(absPath, projectRoot3, frameworkRoots) {
30391
30034
  const norm = path6.resolve(absPath);
30392
30035
  for (const fw of frameworkRoots) {
30393
30036
  const parent = path6.dirname(fw);
@@ -30852,7 +30495,7 @@ function buildEntriesForFile(absPath, source, fwRoots, projectRoot3, version, ou
30852
30495
  } catch {
30853
30496
  return;
30854
30497
  }
30855
- const rel = relativePath2(absPath, projectRoot3, fwRoots);
30498
+ const rel = relativePath(absPath, projectRoot3, fwRoots);
30856
30499
  for (const cls of parsed.classes) {
30857
30500
  if (!cls.exported && source === "framework") {
30858
30501
  continue;
@@ -31488,8 +31131,8 @@ ${end}
31488
31131
 
31489
31132
  // src/devAdmin.ts
31490
31133
  import { cpus as osCpus } from "node:os";
31491
- import { readFileSync as readFileSync21, writeFileSync as writeFileSync14, existsSync as existsSync21, readdirSync as readdirSync15, mkdirSync as mkdirSync17, copyFileSync, statSync as statSync16 } from "node:fs";
31492
- import { join as join26, dirname as dirname12, resolve as resolve15, relative as relative8 } from "node:path";
31134
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, existsSync as existsSync21, readdirSync as readdirSync15, mkdirSync as mkdirSync17, copyFileSync, statSync as statSync16 } from "node:fs";
31135
+ import { join as join26, dirname as dirname12, resolve as resolve15, relative as relative7 } from "node:path";
31493
31136
  import { fileURLToPath as fileURLToPath5 } from "node:url";
31494
31137
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
31495
31138
  function escapeHtml(value) {
@@ -31608,7 +31251,7 @@ function readQueueDir(dir, topic, status2) {
31608
31251
  for (const filename of readdirSync15(dir).sort()) {
31609
31252
  if (!filename.endsWith(".queue-data")) continue;
31610
31253
  try {
31611
- jobs.push(mapQueueJob(JSON.parse(readFileSync21(join26(dir, filename), "utf-8")), topic, status2));
31254
+ jobs.push(mapQueueJob(JSON.parse(readFileSync20(join26(dir, filename), "utf-8")), topic, status2));
31612
31255
  } catch {
31613
31256
  }
31614
31257
  }
@@ -31727,7 +31370,7 @@ function resolveDevEnvVar(key) {
31727
31370
  if (live !== void 0 && live !== "") return live;
31728
31371
  const envPath = join26(process.cwd(), ".env");
31729
31372
  if (!existsSync21(envPath)) return "";
31730
- for (const line of readFileSync21(envPath, "utf-8").split("\n")) {
31373
+ for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
31731
31374
  const t = line.trim();
31732
31375
  if (!t || t.startsWith("#") || !t.includes("=")) continue;
31733
31376
  const eq = t.indexOf("=");
@@ -31737,7 +31380,7 @@ function resolveDevEnvVar(key) {
31737
31380
  }
31738
31381
  function upsertDevEnvVar(key, value) {
31739
31382
  const envPath = join26(process.cwd(), ".env");
31740
- const lines = existsSync21(envPath) ? readFileSync21(envPath, "utf-8").split("\n") : [];
31383
+ const lines = existsSync21(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
31741
31384
  let found = false;
31742
31385
  const out = [];
31743
31386
  for (const line of lines) {
@@ -31770,7 +31413,7 @@ function parseEnvFile() {
31770
31413
  const envPath = join26(process.cwd(), ".env");
31771
31414
  const result = {};
31772
31415
  if (!existsSync21(envPath)) return result;
31773
- const lines = readFileSync21(envPath, "utf-8").split("\n");
31416
+ const lines = readFileSync20(envPath, "utf-8").split("\n");
31774
31417
  for (const line of lines) {
31775
31418
  const trimmed = line.trim();
31776
31419
  if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
@@ -31810,7 +31453,7 @@ function handleGalleryDeploy(router) {
31810
31453
  const copied = [];
31811
31454
  const allFiles = walkDirRecursive(gallerySrc);
31812
31455
  for (const srcFile of allFiles) {
31813
- const rel = relative8(gallerySrc, srcFile);
31456
+ const rel = relative7(gallerySrc, srcFile);
31814
31457
  const dest = join26(projectSrc, rel);
31815
31458
  mkdirSync17(dirname12(dest), { recursive: true });
31816
31459
  copyFileSync(srcFile, dest);
@@ -32472,9 +32115,6 @@ var init_devAdmin = __esm({
32472
32115
  { method: "GET", pattern: "/__dev/api/gallery", handler: handleGalleryList },
32473
32116
  { method: "POST", pattern: "/__dev/api/gallery/deploy", handler: handleGalleryDeploy(router) },
32474
32117
  // Metrics
32475
- { method: "GET", pattern: "/__dev/api/metrics", handler: (_req, res) => {
32476
- res.json(quickMetrics());
32477
- } },
32478
32118
  // No fallback (ADR-0002): a missing or stale CLI is a 503 naming the
32479
32119
  // install command, never zeros that read as a healthy codebase.
32480
32120
  { method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req, res) => {
@@ -33283,7 +32923,7 @@ var init_devAdmin = __esm({
33283
32923
  }
33284
32924
  try {
33285
32925
  const envPath = join26(process.cwd(), ".env");
33286
- const lines = existsSync21(envPath) ? readFileSync21(envPath, "utf-8").split("\n") : [];
32926
+ const lines = existsSync21(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
33287
32927
  const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
33288
32928
  const newLines = [];
33289
32929
  for (const line of lines) {
@@ -33329,12 +32969,12 @@ var init_devAdmin = __esm({
33329
32969
  const metaFile = join26(entryPath, "meta.json");
33330
32970
  if (statSync16(entryPath).isDirectory() && existsSync21(metaFile)) {
33331
32971
  try {
33332
- const meta = JSON.parse(readFileSync21(metaFile, "utf-8"));
32972
+ const meta = JSON.parse(readFileSync20(metaFile, "utf-8"));
33333
32973
  meta.id = entry;
33334
32974
  const srcDir = join26(entryPath, "src");
33335
32975
  if (existsSync21(srcDir)) {
33336
32976
  const allFiles = walkDirRecursive(srcDir);
33337
- meta.files = allFiles.map((f) => relative8(srcDir, f));
32977
+ meta.files = allFiles.map((f) => relative7(srcDir, f));
33338
32978
  }
33339
32979
  const projectSrc = resolve15(process.cwd(), "src");
33340
32980
  if (existsSync21(srcDir) && meta.files) {
@@ -33452,7 +33092,7 @@ var init_devAdmin = __esm({
33452
33092
  for (const name of readdirSync15(target).sort()) {
33453
33093
  if (devFilesHidden(name)) continue;
33454
33094
  const full = join26(target, name);
33455
- const entryRel = relative8(root, full).replace(/\\/g, "/");
33095
+ const entryRel = relative7(root, full).replace(/\\/g, "/");
33456
33096
  if (isSecretPath(entryRel)) continue;
33457
33097
  let isDir = false;
33458
33098
  let size = null;
@@ -33497,7 +33137,7 @@ var init_devAdmin = __esm({
33497
33137
  size
33498
33138
  });
33499
33139
  }
33500
- res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
33140
+ res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
33501
33141
  };
33502
33142
  DEV_ADMIN_LANG_MAP = {
33503
33143
  ".py": "python",
@@ -33549,8 +33189,8 @@ var init_devAdmin = __esm({
33549
33189
  return;
33550
33190
  }
33551
33191
  try {
33552
- const content = readFileSync21(target, "utf-8");
33553
- const path8 = relative8(root, target);
33192
+ const content = readFileSync20(target, "utf-8");
33193
+ const path8 = relative7(root, target);
33554
33194
  res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
33555
33195
  } catch (e) {
33556
33196
  res.json({ error: e.message }, 500);
@@ -33572,10 +33212,10 @@ var init_devAdmin = __esm({
33572
33212
  writeFileSync14(target, content, "utf-8");
33573
33213
  try {
33574
33214
  const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
33575
- Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
33215
+ Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
33576
33216
  } catch {
33577
33217
  }
33578
- res.json({ ok: true, path: relative8(root, target), bytes: Buffer.byteLength(content, "utf-8") });
33218
+ res.json({ ok: true, path: relative7(root, target), bytes: Buffer.byteLength(content, "utf-8") });
33579
33219
  } catch (e) {
33580
33220
  res.json({ error: e.message }, 500);
33581
33221
  }
@@ -33595,7 +33235,7 @@ var init_devAdmin = __esm({
33595
33235
  return;
33596
33236
  }
33597
33237
  try {
33598
- const buf = readFileSync21(target);
33238
+ const buf = readFileSync20(target);
33599
33239
  const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
33600
33240
  const mime = {
33601
33241
  js: "application/javascript",
@@ -33637,7 +33277,7 @@ var init_devAdmin = __esm({
33637
33277
  const { renameSync: renameSync3 } = await import("node:fs");
33638
33278
  mkdirSync17(dirname12(dst), { recursive: true });
33639
33279
  renameSync3(src, dst);
33640
- res.json({ ok: true, from: relative8(root, src), to: relative8(root, dst) });
33280
+ res.json({ ok: true, from: relative7(root, src), to: relative7(root, dst) });
33641
33281
  } catch (e) {
33642
33282
  res.json({ error: e.message }, 500);
33643
33283
  }
@@ -33658,7 +33298,7 @@ var init_devAdmin = __esm({
33658
33298
  try {
33659
33299
  const { rmSync } = await import("node:fs");
33660
33300
  rmSync(target, { recursive: true, force: true });
33661
- res.json({ ok: true, deleted: relative8(root, target) });
33301
+ res.json({ ok: true, deleted: relative7(root, target) });
33662
33302
  } catch (e) {
33663
33303
  res.json({ error: e.message }, 500);
33664
33304
  }
@@ -33992,7 +33632,7 @@ var init_devAdmin = __esm({
33992
33632
  });
33993
33633
  };
33994
33634
  handleDevAdminJs = async (_req, res) => {
33995
- const { readFileSync: readFileSync28, existsSync: existsSync27 } = await import("node:fs");
33635
+ const { readFileSync: readFileSync27, existsSync: existsSync27 } = await import("node:fs");
33996
33636
  const { dirname: dirname15, join: join32, resolve: resolve20 } = await import("node:path");
33997
33637
  const { fileURLToPath: fileURLToPath8 } = await import("node:url");
33998
33638
  const dir = dirname15(fileURLToPath8(import.meta.url));
@@ -34008,7 +33648,7 @@ var init_devAdmin = __esm({
34008
33648
  for (const jsPath of candidates) {
34009
33649
  if (existsSync27(jsPath)) {
34010
33650
  try {
34011
- const content = readFileSync28(jsPath, "utf-8");
33651
+ const content = readFileSync27(jsPath, "utf-8");
34012
33652
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
34013
33653
  res.raw.end(content);
34014
33654
  return;
@@ -34023,7 +33663,7 @@ var init_devAdmin = __esm({
34023
33663
  });
34024
33664
 
34025
33665
  // src/i18n.ts
34026
- import { readFileSync as readFileSync22, readdirSync as readdirSync16, existsSync as existsSync22 } from "node:fs";
33666
+ import { readFileSync as readFileSync21, readdirSync as readdirSync16, existsSync as existsSync22 } from "node:fs";
34027
33667
  import { join as join27, resolve as resolve16 } from "node:path";
34028
33668
  var I18n;
34029
33669
  var init_i18n = __esm({
@@ -34120,7 +33760,7 @@ var init_i18n = __esm({
34120
33760
  const filePath = join27(this._localeDir, `${locale}.json`);
34121
33761
  if (existsSync22(filePath)) {
34122
33762
  try {
34123
- const raw = readFileSync22(filePath, "utf-8");
33763
+ const raw = readFileSync21(filePath, "utf-8");
34124
33764
  const data = JSON.parse(raw);
34125
33765
  this._translations.set(locale, _I18n._flatten(data));
34126
33766
  return;
@@ -34133,7 +33773,7 @@ var init_i18n = __esm({
34133
33773
  const yamlPath = join27(this._localeDir, `${locale}${ext}`);
34134
33774
  if (existsSync22(yamlPath)) {
34135
33775
  try {
34136
- const raw = readFileSync22(yamlPath, "utf-8");
33776
+ const raw = readFileSync21(yamlPath, "utf-8");
34137
33777
  const data = _I18n._parseSimpleYaml(raw);
34138
33778
  this._translations.set(locale, _I18n._flatten(data));
34139
33779
  return;
@@ -34996,8 +34636,8 @@ var init_docsAutoDiscovery = __esm({
34996
34636
  // src/server.ts
34997
34637
  import { createServer as createServer2 } from "node:http";
34998
34638
  import { randomBytes as randomBytes7 } from "node:crypto";
34999
- import { resolve as resolve18, dirname as dirname13, join as join29, relative as relative9 } from "node:path";
35000
- import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync24, statSync as statSync17 } from "node:fs";
34639
+ import { resolve as resolve18, dirname as dirname13, join as join29, relative as relative8 } from "node:path";
34640
+ import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
35001
34641
  import { isatty } from "node:tty";
35002
34642
  import { fileURLToPath as fileURLToPath6 } from "node:url";
35003
34643
  import { execFileSync as execFileSync3, exec } from "node:child_process";
@@ -35221,7 +34861,7 @@ function getGalleryDeployedState() {
35221
34861
  if (existsSync24(srcDir)) {
35222
34862
  const files = walkGalleryFiles(srcDir);
35223
34863
  const projectSrc = resolve18(process.cwd(), "src");
35224
- state[entry] = files.every((f) => existsSync24(join29(projectSrc, relative9(srcDir, f))));
34864
+ state[entry] = files.every((f) => existsSync24(join29(projectSrc, relative8(srcDir, f))));
35225
34865
  } else {
35226
34866
  state[entry] = false;
35227
34867
  }
@@ -35658,7 +35298,7 @@ function serveTemplateFallback(ctx) {
35658
35298
  if ((ctx.req.method ?? "GET") !== "GET") return false;
35659
35299
  const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
35660
35300
  if (!tplFile) return false;
35661
- const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync24(resolve18(ctx.templatesDir, tplFile), "utf-8");
35301
+ const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync23(resolve18(ctx.templatesDir, tplFile), "utf-8");
35662
35302
  ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
35663
35303
  ctx.res.raw.end(html);
35664
35304
  return true;
@@ -36485,7 +36125,7 @@ var init_mqttMessage = __esm({
36485
36125
  import net2 from "node:net";
36486
36126
  import tls from "node:tls";
36487
36127
  import { randomBytes as randomBytes8 } from "node:crypto";
36488
- import { existsSync as existsSync25, readFileSync as readFileSync25 } from "node:fs";
36128
+ import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
36489
36129
  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;
36490
36130
  var init_mqtt = __esm({
36491
36131
  "src/mqtt.ts"() {
@@ -36950,7 +36590,7 @@ var init_mqtt = __esm({
36950
36590
  servername: this.host,
36951
36591
  rejectUnauthorized: this.tlsVerify
36952
36592
  };
36953
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync25(this.caFile);
36593
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
36954
36594
  sock = tls.connect(opts, () => settle(() => resolve20(sock)));
36955
36595
  } else {
36956
36596
  sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
@@ -37171,7 +36811,7 @@ var init_mqtt = __esm({
37171
36811
 
37172
36812
  // src/service.ts
37173
36813
  import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
37174
- import { join as join30, extname as extname8 } from "node:path";
36814
+ import { join as join30, extname as extname7 } from "node:path";
37175
36815
  import { pathToFileURL } from "node:url";
37176
36816
  function matchCronField(field, value) {
37177
36817
  if (field === "*") return true;
@@ -37342,7 +36982,7 @@ var init_service = __esm({
37342
36982
  return discovered;
37343
36983
  }
37344
36984
  for (const entry of entries) {
37345
- const ext = extname8(entry);
36985
+ const ext = extname7(entry);
37346
36986
  if (ext !== ".ts" && ext !== ".js") continue;
37347
36987
  const fullPath = join30(dir, entry);
37348
36988
  const stat = statSync18(fullPath);
@@ -37458,7 +37098,7 @@ var init_service = __esm({
37458
37098
  return;
37459
37099
  }
37460
37100
  for (const entry of entries) {
37461
- const ext = extname8(entry);
37101
+ const ext = extname7(entry);
37462
37102
  if (ext !== ".ts" && ext !== ".js") continue;
37463
37103
  const fullPath = join30(dir, entry);
37464
37104
  if (watchedFiles.has(fullPath)) continue;
@@ -38161,7 +37801,7 @@ var init_api = __esm({
38161
37801
  // src/messenger.ts
38162
37802
  import net3 from "node:net";
38163
37803
  import tls2 from "node:tls";
38164
- import { readFileSync as readFileSync26 } from "node:fs";
37804
+ import { readFileSync as readFileSync25 } from "node:fs";
38165
37805
  import { basename as basename6 } from "node:path";
38166
37806
  import { randomUUID as randomUUID7 } from "node:crypto";
38167
37807
  function tlsRejectUnauthorized() {
@@ -38258,7 +37898,7 @@ function buildMimeMessage(options) {
38258
37898
  }
38259
37899
  for (const filePath of options.attachments) {
38260
37900
  const fileName = basename6(filePath);
38261
- const fileData = readFileSync26(filePath);
37901
+ const fileData = readFileSync25(filePath);
38262
37902
  const base64Data = fileData.toString("base64");
38263
37903
  lines.push("");
38264
37904
  lines.push(`--${boundary}`);
@@ -39721,9 +39361,9 @@ var init_htmlElement = __esm({
39721
39361
  });
39722
39362
 
39723
39363
  // src/ai.ts
39724
- import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync17, readFileSync as readFileSync27 } from "node:fs";
39364
+ import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync17, readFileSync as readFileSync26 } from "node:fs";
39725
39365
  import { homedir } from "node:os";
39726
- import { join as join31, resolve as resolve19, relative as relative10, dirname as dirname14 } from "node:path";
39366
+ import { join as join31, resolve as resolve19, relative as relative9, dirname as dirname14 } from "node:path";
39727
39367
  import { fileURLToPath as fileURLToPath7 } from "node:url";
39728
39368
  import { execSync, execFileSync as execFileSync4 } from "node:child_process";
39729
39369
  import { createInterface } from "node:readline";
@@ -39731,7 +39371,7 @@ function readVersion() {
39731
39371
  try {
39732
39372
  const thisDir = dirname14(fileURLToPath7(import.meta.url));
39733
39373
  const rootPkg = resolve19(thisDir, "..", "..", "..", "package.json");
39734
- const pkg = JSON.parse(readFileSync27(rootPkg, "utf-8"));
39374
+ const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
39735
39375
  return pkg.version ?? "0.0.0";
39736
39376
  } catch {
39737
39377
  return "0.0.0";
@@ -39954,7 +39594,7 @@ function writeOrMerge(contextPath, contextFile, frameworkGuide) {
39954
39594
  writeFileSync17(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
39955
39595
  return "Installed";
39956
39596
  }
39957
- const existing = readFileSync27(contextPath, "utf-8");
39597
+ const existing = readFileSync26(contextPath, "utf-8");
39958
39598
  if (hasMarkers(existing, start2, end)) {
39959
39599
  writeFileSync17(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
39960
39600
  return "Refreshed skill block in";
@@ -39978,7 +39618,7 @@ function installForTool(root, tool, context) {
39978
39618
  const parentDir = dirname14(contextPath);
39979
39619
  mkdirSync20(parentDir, { recursive: true });
39980
39620
  const action = writeOrMerge(contextPath, tool.contextFile, context);
39981
- const rel = relative10(root, contextPath);
39621
+ const rel = relative9(root, contextPath);
39982
39622
  created.push(rel);
39983
39623
  console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
39984
39624
  if (tool.name === "claude-code") {
@@ -40356,7 +39996,7 @@ function generateClaudeCodeContext() {
40356
39996
  const repoRoot = resolve19(thisDir, "..", "..", "..");
40357
39997
  const claudeMdPath = join31(repoRoot, "CLAUDE.md");
40358
39998
  if (existsSync26(claudeMdPath)) {
40359
- return readFileSync27(claudeMdPath, "utf-8");
39999
+ return readFileSync26(claudeMdPath, "utf-8");
40360
40000
  }
40361
40001
  } catch {
40362
40002
  }
@@ -40550,6 +40190,292 @@ export default class User {
40550
40190
  }
40551
40191
  });
40552
40192
 
40193
+ // src/aiClient.ts
40194
+ import http2 from "node:http";
40195
+ import https2 from "node:https";
40196
+ var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai;
40197
+ var init_aiClient = __esm({
40198
+ "src/aiClient.ts"() {
40199
+ "use strict";
40200
+ AiError = class extends Error {
40201
+ };
40202
+ AiConfigError = class extends AiError {
40203
+ };
40204
+ AiTimeoutError = class extends AiError {
40205
+ };
40206
+ AiParseError = class extends AiError {
40207
+ };
40208
+ AiHTTPError = class extends AiError {
40209
+ constructor(message, status2 = null) {
40210
+ super(message);
40211
+ this.status = status2;
40212
+ }
40213
+ };
40214
+ Ai = class {
40215
+ static chat(messages, options = {}) {
40216
+ this.validateMessages(messages);
40217
+ const config = this.config("chat", options);
40218
+ const body = this.chatBody(config, messages, options);
40219
+ const headers = this.headers(config);
40220
+ return options.stream ? this.streamRequest(config, headers, body) : this.chatResponse(config, headers, body);
40221
+ }
40222
+ static async complete(prompt, options = {}) {
40223
+ if (typeof prompt !== "string") throw new AiConfigError("AI prompt must be a string");
40224
+ return (await this.chat([{ role: "user", content: prompt }], { ...options, stream: false })).text;
40225
+ }
40226
+ static async embed(textOrTexts, options = {}) {
40227
+ const single = typeof textOrTexts === "string";
40228
+ if (!single && (!Array.isArray(textOrTexts) || textOrTexts.length === 0 || !textOrTexts.every((item) => typeof item === "string"))) {
40229
+ throw new AiConfigError("AI embedding input must be a string or a non-empty list of strings");
40230
+ }
40231
+ const config = this.config("embed", options);
40232
+ if (config.provider === "anthropic") throw new AiConfigError("Anthropic does not provide the embedding endpoint in this contract");
40233
+ const raw = await this.requestJson(config, this.headers(config), { model: config.model, input: textOrTexts });
40234
+ try {
40235
+ const data = raw.data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
40236
+ const vectors = data.map((item) => item.embedding);
40237
+ const expected = single ? 1 : textOrTexts.length;
40238
+ if (vectors.length !== expected || !vectors.every((vector) => Array.isArray(vector) && vector.length > 0 && vector.every((value) => typeof value === "number" && Number.isFinite(value)))) throw new Error();
40239
+ return single ? vectors[0] : vectors;
40240
+ } catch {
40241
+ throw new AiParseError("AI provider returned a malformed embedding response");
40242
+ }
40243
+ }
40244
+ static validateMessages(messages) {
40245
+ if (!Array.isArray(messages) || messages.length === 0 || !messages.every((message) => message && ["system", "user", "assistant"].includes(message.role) && typeof message.content === "string")) {
40246
+ throw new AiConfigError("AI messages must contain supported roles and string content");
40247
+ }
40248
+ }
40249
+ static number(name, fallback, minimum) {
40250
+ const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
40251
+ if (!Number.isFinite(value) || value < minimum) throw new AiConfigError(`${name} must be numeric and at least ${minimum}`);
40252
+ return value;
40253
+ }
40254
+ static config(capability, options) {
40255
+ const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
40256
+ if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
40257
+ const key = process.env.TINA4_AI_KEY || null;
40258
+ if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
40259
+ const defaults = {
40260
+ local: ["http://localhost:11437", "llama3.2"],
40261
+ openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
40262
+ anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
40263
+ };
40264
+ const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
40265
+ const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
40266
+ if (!model) throw new AiConfigError("AI model must be a non-empty string");
40267
+ const totalTimeout = options.timeout === void 0 ? this.number("TINA4_AI_TIMEOUT", 60, 1e-3) : Number(options.timeout);
40268
+ if (!Number.isFinite(totalTimeout) || totalTimeout <= 0) throw new AiConfigError("AI timeout must be greater than zero");
40269
+ return { provider, url: this.endpoint(rawUrl, capability, provider), model, key, totalTimeout, connectTimeout: this.number("TINA4_AI_CONNECT_TIMEOUT", 10, 1e-3), maxRetries: Math.trunc(this.number("TINA4_AI_MAX_RETRIES", 2, 0)) };
40270
+ }
40271
+ static endpoint(value, capability, provider) {
40272
+ let url;
40273
+ try {
40274
+ url = new URL(value);
40275
+ } catch {
40276
+ throw new AiConfigError("AI URL must be an http or https URL");
40277
+ }
40278
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
40279
+ const path8 = url.pathname.replace(/\/+$/, "");
40280
+ if (path8 === "" || path8 === "/v1" || path8 === "/api") {
40281
+ const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
40282
+ url.pathname = (path8 || "/v1") + suffix;
40283
+ }
40284
+ return url.toString();
40285
+ }
40286
+ static headers(config) {
40287
+ const headers = { "content-type": "application/json", accept: "application/json" };
40288
+ if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
40289
+ if (config.provider === "anthropic") {
40290
+ headers["x-api-key"] = config.key;
40291
+ headers["anthropic-version"] = "2023-06-01";
40292
+ }
40293
+ return headers;
40294
+ }
40295
+ static chatBody(config, messages, options) {
40296
+ const body = { model: config.model, messages, stream: options.stream ?? false };
40297
+ if (options.temperature !== void 0) body.temperature = options.temperature;
40298
+ if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
40299
+ if (config.provider === "anthropic") {
40300
+ const system = messages.filter((message) => message.role === "system").map((message) => message.content);
40301
+ body.messages = messages.filter((message) => message.role !== "system");
40302
+ body.max_tokens = options.maxTokens ?? 1024;
40303
+ if (system.length) body.system = system.join("\n\n");
40304
+ }
40305
+ return body;
40306
+ }
40307
+ static open(config, deadline, headers, body) {
40308
+ const remainingMs = deadline - performance.now();
40309
+ if (remainingMs <= 0) return Promise.reject(new AiTimeoutError("AI total request timeout expired"));
40310
+ const url = new URL(config.url);
40311
+ const payload = JSON.stringify(body);
40312
+ const controller = new AbortController();
40313
+ const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
40314
+ return new Promise((resolve20, reject) => {
40315
+ const client = url.protocol === "https:" ? https2 : http2;
40316
+ const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
40317
+ clearTimeout(connectTimer);
40318
+ resolve20({ response, cleanup: () => {
40319
+ clearTimeout(totalTimer);
40320
+ clearTimeout(connectTimer);
40321
+ } });
40322
+ });
40323
+ const connectTimer = setTimeout(() => request.destroy(new AiTimeoutError("AI connection timeout expired")), Math.min(config.connectTimeout * 1e3, remainingMs));
40324
+ request.on("socket", (socket) => {
40325
+ if (!socket.connecting) clearTimeout(connectTimer);
40326
+ socket.once(url.protocol === "https:" ? "secureConnect" : "connect", () => clearTimeout(connectTimer));
40327
+ });
40328
+ request.once("error", (error) => {
40329
+ clearTimeout(totalTimer);
40330
+ clearTimeout(connectTimer);
40331
+ if (error instanceof AiError) reject(error);
40332
+ else if (controller.signal.aborted) reject(new AiTimeoutError("AI total request timeout expired"));
40333
+ else reject(new AiHTTPError(`AI transport failed (${error.name})`));
40334
+ });
40335
+ request.end(payload);
40336
+ });
40337
+ }
40338
+ static async readBody(response) {
40339
+ const chunks = [];
40340
+ for await (const chunk of response) chunks.push(Buffer.from(chunk));
40341
+ return Buffer.concat(chunks).toString("utf8");
40342
+ }
40343
+ static retryDelay(headers, deadline) {
40344
+ const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
40345
+ const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
40346
+ const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
40347
+ return new Promise((resolve20) => setTimeout(resolve20, delay));
40348
+ }
40349
+ static async requestJson(config, headers, body) {
40350
+ const deadline = performance.now() + config.totalTimeout * 1e3;
40351
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
40352
+ let opened = null;
40353
+ try {
40354
+ opened = await this.open(config, deadline, headers, body);
40355
+ const status2 = opened.response.statusCode ?? 0;
40356
+ const responseHeaders = opened.response.headers;
40357
+ const raw = await this.readBody(opened.response);
40358
+ opened.cleanup();
40359
+ opened = null;
40360
+ if (status2 < 200 || status2 >= 300) {
40361
+ if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
40362
+ await this.retryDelay(responseHeaders, deadline);
40363
+ continue;
40364
+ }
40365
+ throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
40366
+ }
40367
+ let parsed;
40368
+ try {
40369
+ parsed = JSON.parse(raw);
40370
+ } catch {
40371
+ throw new AiParseError("AI provider returned malformed JSON");
40372
+ }
40373
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new AiParseError("AI provider returned a non-object JSON response");
40374
+ return parsed;
40375
+ } catch (error) {
40376
+ opened?.cleanup();
40377
+ if (error instanceof AiParseError || error instanceof AiHTTPError && error.status !== null) throw error;
40378
+ if (attempt >= config.maxRetries) throw error;
40379
+ }
40380
+ }
40381
+ throw new AiHTTPError("AI request failed");
40382
+ }
40383
+ static normalizeChat(provider, raw) {
40384
+ try {
40385
+ if (provider === "anthropic") {
40386
+ const content = raw.content;
40387
+ const parts = content.filter((item) => (item.type ?? "text") === "text").map((item) => item.text);
40388
+ if (!parts.length || !parts.every((part) => typeof part === "string")) throw new Error();
40389
+ const usage2 = raw.usage ?? {};
40390
+ const promptTokens = Number(usage2.input_tokens ?? 0);
40391
+ const completionTokens = Number(usage2.output_tokens ?? 0);
40392
+ return { text: parts.join(""), model: String(raw.model ?? ""), usage: { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens }, finishReason: raw.stop_reason == null ? null : String(raw.stop_reason), raw };
40393
+ }
40394
+ const choice = raw.choices[0];
40395
+ const text = choice.message.content;
40396
+ if (typeof text !== "string") throw new Error();
40397
+ const usage = raw.usage ?? {};
40398
+ return { text, model: String(raw.model ?? ""), usage: { promptTokens: Number(usage.prompt_tokens ?? 0), completionTokens: Number(usage.completion_tokens ?? 0), totalTokens: Number(usage.total_tokens ?? 0) }, finishReason: choice.finish_reason == null ? null : String(choice.finish_reason), raw };
40399
+ } catch {
40400
+ throw new AiParseError("AI provider returned a malformed chat response");
40401
+ }
40402
+ }
40403
+ static async chatResponse(config, headers, body) {
40404
+ return this.normalizeChat(config.provider, await this.requestJson(config, headers, body));
40405
+ }
40406
+ static streamDelta(provider, data) {
40407
+ if (data === "[DONE]") return { completed: true };
40408
+ let event;
40409
+ try {
40410
+ event = JSON.parse(data);
40411
+ } catch {
40412
+ throw new AiParseError("AI provider returned malformed stream data");
40413
+ }
40414
+ const text = provider === "anthropic" ? event.type === "content_block_delta" ? event.delta?.text : void 0 : event.choices?.[0]?.delta?.content;
40415
+ if (text !== void 0 && text !== null && typeof text !== "string") throw new AiParseError("AI provider returned malformed stream data");
40416
+ return { completed: false, text };
40417
+ }
40418
+ static async *streamData(response) {
40419
+ let buffer = "";
40420
+ for await (const chunk of response) {
40421
+ buffer += Buffer.from(chunk).toString("utf8");
40422
+ let newline;
40423
+ while ((newline = buffer.indexOf("\n")) >= 0) {
40424
+ const line = buffer.slice(0, newline).trim();
40425
+ buffer = buffer.slice(newline + 1);
40426
+ if (line.startsWith("data:")) yield line.slice(5).trim();
40427
+ }
40428
+ }
40429
+ }
40430
+ static streamError(error) {
40431
+ if (error instanceof AiError) return error;
40432
+ if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
40433
+ return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
40434
+ }
40435
+ static async *streamRequest(config, headers, body) {
40436
+ const deadline = performance.now() + config.totalTimeout * 1e3;
40437
+ let yielded = false;
40438
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
40439
+ let opened = null;
40440
+ try {
40441
+ opened = await this.open(config, deadline, { ...headers, accept: "text/event-stream" }, body);
40442
+ const status2 = opened.response.statusCode ?? 0;
40443
+ if (status2 < 200 || status2 >= 300) {
40444
+ await this.readBody(opened.response);
40445
+ if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
40446
+ await this.retryDelay(opened.response.headers, deadline);
40447
+ opened.cleanup();
40448
+ opened = null;
40449
+ continue;
40450
+ }
40451
+ throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
40452
+ }
40453
+ let completed = false;
40454
+ for await (const data of this.streamData(opened.response)) {
40455
+ const delta = this.streamDelta(config.provider, data);
40456
+ if (delta.completed) {
40457
+ completed = true;
40458
+ break;
40459
+ }
40460
+ if (delta.text === void 0) continue;
40461
+ yielded = true;
40462
+ yield delta.text;
40463
+ }
40464
+ opened.cleanup();
40465
+ opened = null;
40466
+ if (completed) return;
40467
+ throw new AiParseError("AI provider stream ended before [DONE]");
40468
+ } catch (error) {
40469
+ opened?.cleanup();
40470
+ const failure = this.streamError(error);
40471
+ if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || yielded || attempt >= config.maxRetries) throw failure;
40472
+ }
40473
+ }
40474
+ }
40475
+ };
40476
+ }
40477
+ });
40478
+
40553
40479
  // src/queueBackends/rabbitmqBackend.ts
40554
40480
  import { execFileSync as execFileSync5 } from "node:child_process";
40555
40481
  import { randomUUID as randomUUID8 } from "node:crypto";
@@ -42253,6 +42179,12 @@ __export(index_exports, {
42253
42179
  APPLICATION_JSON: () => APPLICATION_JSON,
42254
42180
  APPLICATION_OCTET: () => APPLICATION_OCTET,
42255
42181
  APPLICATION_XML: () => APPLICATION_XML,
42182
+ Ai: () => Ai,
42183
+ AiConfigError: () => AiConfigError,
42184
+ AiError: () => AiError,
42185
+ AiHTTPError: () => AiHTTPError,
42186
+ AiParseError: () => AiParseError,
42187
+ AiTimeoutError: () => AiTimeoutError,
42256
42188
  Api: () => Api,
42257
42189
  Auth: () => Auth,
42258
42190
  CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
@@ -42579,6 +42511,7 @@ var init_index = __esm({
42579
42511
  init_htmlElement();
42580
42512
  init_errorOverlay();
42581
42513
  init_ai();
42514
+ init_aiClient();
42582
42515
  init_liteBackend();
42583
42516
  init_rabbitmqBackend();
42584
42517
  init_kafkaBackend();
@@ -42608,6 +42541,12 @@ export {
42608
42541
  APPLICATION_JSON,
42609
42542
  APPLICATION_OCTET,
42610
42543
  APPLICATION_XML,
42544
+ Ai,
42545
+ AiConfigError,
42546
+ AiError,
42547
+ AiHTTPError,
42548
+ AiParseError,
42549
+ AiTimeoutError,
42611
42550
  Api,
42612
42551
  Auth,
42613
42552
  CANONICAL_SESSION_BACKENDS,