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.
- package/CLAUDE.md +3 -3
- package/README.md +16 -0
- package/package.json +2 -2
- package/packages/cli/dist/bin.js +572 -764
- package/packages/cli/src/bin.ts +0 -6
- package/packages/core/dist/index.js +561 -622
- package/packages/core/public/js/tina4-dev-admin.min.js +2 -2
- package/packages/core/src/aiClient.ts +288 -0
- package/packages/core/src/devAdmin.ts +1 -2
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/metrics.ts +80 -632
- package/packages/frond/dist/index.js +113 -109
- package/packages/frond/src/engine.ts +128 -130
- package/packages/orm/dist/index.js +562 -629
- package/types/core/src/aiClient.d.ts +66 -0
- package/types/core/src/index.d.ts +2 -0
- package/types/core/src/metrics.d.ts +0 -35
- package/types/frond/src/engine.d.ts +4 -2
- package/packages/cli/src/commands/metrics.ts +0 -160
- package/types/cli/src/commands/metrics.d.ts +0 -6
|
@@ -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
|
|
1874
|
-
expr
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
if (
|
|
1878
|
-
|
|
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
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
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
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
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
|
|
19570
|
-
const pattern = filePathToPattern(
|
|
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: ${
|
|
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 ${
|
|
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(
|
|
19623
|
-
const parts =
|
|
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
|
|
22861
|
-
|
|
22862
|
-
|
|
22863
|
-
|
|
22864
|
-
|
|
22865
|
-
|
|
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
|
-
|
|
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
|
|
23202
|
-
const resolved =
|
|
23203
|
-
const
|
|
23204
|
-
|
|
23205
|
-
|
|
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
|
|
23210
|
-
for (const
|
|
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(
|
|
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
|
|
23236
|
-
|
|
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:
|
|
22905
|
+
timeout: 6e4,
|
|
23241
22906
|
maxBuffer: 64 * 1024 * 1024
|
|
23242
22907
|
});
|
|
23243
|
-
if (
|
|
23244
|
-
|
|
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 (
|
|
23257
|
-
|
|
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(
|
|
23262
|
-
|
|
23263
|
-
|
|
23264
|
-
|
|
23265
|
-
|
|
23266
|
-
|
|
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
|
|
23271
|
-
|
|
23272
|
-
|
|
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
|
|
22929
|
+
return payload[key];
|
|
23279
22930
|
}
|
|
23280
22931
|
function fullAnalysis(root = "src") {
|
|
23281
|
-
const [resolved, scanMode] =
|
|
22932
|
+
const [resolved, scanMode] = resolveTarget(root);
|
|
23282
22933
|
const payload = runEngine(resolved);
|
|
23283
|
-
const summary =
|
|
23284
|
-
|
|
23285
|
-
|
|
23286
|
-
|
|
23287
|
-
|
|
23288
|
-
|
|
23289
|
-
|
|
23290
|
-
|
|
23291
|
-
|
|
23292
|
-
if (
|
|
23293
|
-
|
|
23294
|
-
|
|
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
|
-
|
|
23297
|
-
|
|
23298
|
-
|
|
23299
|
-
|
|
23300
|
-
|
|
23301
|
-
|
|
23302
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
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) &&
|
|
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
|
|
23321
|
-
if (!
|
|
23322
|
-
|
|
23323
|
-
|
|
23324
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
26304
|
-
import { basename as basename4, dirname as dirname10, extname as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
31492
|
-
import { join as join26, dirname as dirname12, resolve as resolve15, relative as
|
|
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(
|
|
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
|
|
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) ?
|
|
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 =
|
|
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 =
|
|
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) ?
|
|
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(
|
|
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) =>
|
|
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 =
|
|
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:
|
|
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 =
|
|
33553
|
-
const path8 =
|
|
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",
|
|
33215
|
+
Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
|
|
33576
33216
|
} catch {
|
|
33577
33217
|
}
|
|
33578
|
-
res.json({ ok: true, path:
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
35000
|
-
import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as
|
|
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,
|
|
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, {}) :
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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,
|