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
|
@@ -2622,6 +2622,7 @@ __export(engine_exports, {
|
|
|
2622
2622
|
Frond: () => Frond,
|
|
2623
2623
|
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
2624
2624
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
2625
|
+
expressionFormCache: () => expressionFormCache,
|
|
2625
2626
|
filterChainCache: () => filterChainCache,
|
|
2626
2627
|
pathParseCache: () => pathParseCache,
|
|
2627
2628
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
@@ -3030,62 +3031,58 @@ function splitOutsideQuotes(expr, sep6) {
|
|
|
3030
3031
|
parts.push(expr.slice(currentStart));
|
|
3031
3032
|
return parts;
|
|
3032
3033
|
}
|
|
3033
|
-
function
|
|
3034
|
-
expr
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
if (
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
}
|
|
3041
|
-
if (expr.length >= 2 && expr[0] === "(" && expr.endsWith(")")) {
|
|
3042
|
-
let depth = 0;
|
|
3043
|
-
let matched = true;
|
|
3044
|
-
for (let pi = 0; pi < expr.length; pi++) {
|
|
3045
|
-
if (expr[pi] === "(") depth++;
|
|
3046
|
-
else if (expr[pi] === ")") depth--;
|
|
3047
|
-
if (depth === 0 && pi < expr.length - 1) {
|
|
3048
|
-
matched = false;
|
|
3049
|
-
break;
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
if (matched) {
|
|
3053
|
-
return evalExpr(expr.slice(1, -1), context);
|
|
3054
|
-
}
|
|
3034
|
+
function parenthesizedInner(expr) {
|
|
3035
|
+
if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
|
|
3036
|
+
let depth = 0;
|
|
3037
|
+
for (let index = 0; index < expr.length; index++) {
|
|
3038
|
+
if (expr[index] === "(") depth++;
|
|
3039
|
+
else if (expr[index] === ")") depth--;
|
|
3040
|
+
if (depth === 0 && index < expr.length - 1) return null;
|
|
3055
3041
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
const truePart = rest.slice(0, colonIdx).trim();
|
|
3063
|
-
const falsePart = rest.slice(colonIdx + 1).trim();
|
|
3064
|
-
const cond = evalExpr(condPart, context);
|
|
3065
|
-
return cond ? evalExpr(truePart, context) : evalExpr(falsePart, context);
|
|
3066
|
-
}
|
|
3042
|
+
return expr.slice(1, -1);
|
|
3043
|
+
}
|
|
3044
|
+
function evalPrimary(expr, context) {
|
|
3045
|
+
const quote = expr[0];
|
|
3046
|
+
if (expr.length >= 2 && (quote === '"' || quote === "'") && expr.endsWith(quote) && !expr.slice(1, -1).includes(quote)) {
|
|
3047
|
+
return expr.slice(1, -1);
|
|
3067
3048
|
}
|
|
3049
|
+
const inner = parenthesizedInner(expr);
|
|
3050
|
+
if (inner !== null) return evalExpr(inner, context);
|
|
3051
|
+
return EXPR_NOT_MATCHED;
|
|
3052
|
+
}
|
|
3053
|
+
function evalTernaryExpression(expr, context) {
|
|
3054
|
+
const ternaryIdx = findTernary(expr);
|
|
3055
|
+
if (ternaryIdx === -1) return EXPR_NOT_MATCHED;
|
|
3056
|
+
const rest = expr.slice(ternaryIdx + 1);
|
|
3057
|
+
const colonIdx = findColon(rest);
|
|
3058
|
+
if (colonIdx === -1) return EXPR_NOT_MATCHED;
|
|
3059
|
+
const condition = evalExpr(expr.slice(0, ternaryIdx).trim(), context);
|
|
3060
|
+
const branch = condition ? rest.slice(0, colonIdx) : rest.slice(colonIdx + 1);
|
|
3061
|
+
return evalExpr(branch.trim(), context);
|
|
3062
|
+
}
|
|
3063
|
+
function evalInlineIfExpression(expr, context) {
|
|
3068
3064
|
const ifIdx = findOutsideQuotes(expr, " if ");
|
|
3069
|
-
if (ifIdx
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3065
|
+
if (ifIdx < 0) return EXPR_NOT_MATCHED;
|
|
3066
|
+
const elseIdx = findOutsideQuotes(expr, " else ");
|
|
3067
|
+
if (elseIdx < 0 || elseIdx <= ifIdx) return EXPR_NOT_MATCHED;
|
|
3068
|
+
const condition = evalExpr(expr.slice(ifIdx + 4, elseIdx).trim(), context);
|
|
3069
|
+
const branch = condition ? expr.slice(0, ifIdx) : expr.slice(elseIdx + 6);
|
|
3070
|
+
return evalExpr(branch.trim(), context);
|
|
3071
|
+
}
|
|
3072
|
+
function evalCoalesceExpression(expr, context) {
|
|
3079
3073
|
const qqIdx = findOutsideQuotes(expr, "??");
|
|
3080
|
-
if (qqIdx
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
return
|
|
3074
|
+
if (qqIdx === -1) return EXPR_NOT_MATCHED;
|
|
3075
|
+
const value = evalExpr(expr.slice(0, qqIdx).trim(), context);
|
|
3076
|
+
return value === null || value === void 0 ? evalExpr(expr.slice(qqIdx + 2).trim(), context) : value;
|
|
3077
|
+
}
|
|
3078
|
+
function evalConditional(expr, context) {
|
|
3079
|
+
for (const evaluator of [evalTernaryExpression, evalInlineIfExpression, evalCoalesceExpression]) {
|
|
3080
|
+
const result = evaluator(expr, context);
|
|
3081
|
+
if (result !== EXPR_NOT_MATCHED) return result;
|
|
3088
3082
|
}
|
|
3083
|
+
return EXPR_NOT_MATCHED;
|
|
3084
|
+
}
|
|
3085
|
+
function evalConcatOrComparison(expr, context) {
|
|
3089
3086
|
if (findOutsideQuotes(expr, "~") >= 0) {
|
|
3090
3087
|
const parts = splitOutsideQuotes(expr, "~");
|
|
3091
3088
|
if (parts.length > 1) {
|
|
@@ -3103,6 +3100,9 @@ function evalExpr(expr, context) {
|
|
|
3103
3100
|
return evalComparison(expr, context);
|
|
3104
3101
|
}
|
|
3105
3102
|
}
|
|
3103
|
+
return EXPR_NOT_MATCHED;
|
|
3104
|
+
}
|
|
3105
|
+
function evalArithmeticExpression(expr, context) {
|
|
3106
3106
|
for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
|
|
3107
3107
|
const pos = findOutsideQuotes(expr, op);
|
|
3108
3108
|
if (pos >= 0) {
|
|
@@ -3115,40 +3115,15 @@ function evalExpr(expr, context) {
|
|
|
3115
3115
|
let rNum = rVal != null ? Number(rVal) : 0;
|
|
3116
3116
|
if (isNaN(lNum)) lNum = 0;
|
|
3117
3117
|
if (isNaN(rNum)) rNum = 0;
|
|
3118
|
-
|
|
3119
|
-
const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
|
|
3120
|
-
let result;
|
|
3121
|
-
switch (opS) {
|
|
3122
|
-
case "+":
|
|
3123
|
-
result = lNum + rNum;
|
|
3124
|
-
break;
|
|
3125
|
-
case "-":
|
|
3126
|
-
result = lNum - rNum;
|
|
3127
|
-
break;
|
|
3128
|
-
case "*":
|
|
3129
|
-
result = lNum * rNum;
|
|
3130
|
-
break;
|
|
3131
|
-
case "//":
|
|
3132
|
-
result = rNum !== 0 ? Math.floor(lNum / rNum) : 0;
|
|
3133
|
-
break;
|
|
3134
|
-
case "/":
|
|
3135
|
-
result = rNum !== 0 ? lNum / rNum : 0;
|
|
3136
|
-
break;
|
|
3137
|
-
case "%":
|
|
3138
|
-
result = rNum !== 0 ? lNum % rNum : 0;
|
|
3139
|
-
break;
|
|
3140
|
-
case "**":
|
|
3141
|
-
result = lNum ** rNum;
|
|
3142
|
-
break;
|
|
3143
|
-
default:
|
|
3144
|
-
result = 0;
|
|
3145
|
-
}
|
|
3146
|
-
return bothInt && Number.isInteger(result) ? result : result;
|
|
3118
|
+
return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
|
|
3147
3119
|
} catch {
|
|
3148
3120
|
return null;
|
|
3149
3121
|
}
|
|
3150
3122
|
}
|
|
3151
3123
|
}
|
|
3124
|
+
return EXPR_NOT_MATCHED;
|
|
3125
|
+
}
|
|
3126
|
+
function evalFilterExpression(expr, context) {
|
|
3152
3127
|
if (findOutsideQuotes(expr, "|") >= 0) {
|
|
3153
3128
|
const [baseExpr, filters] = parseFilterChain(expr);
|
|
3154
3129
|
if (filters.length > 0) {
|
|
@@ -3167,38 +3142,49 @@ function evalExpr(expr, context) {
|
|
|
3167
3142
|
return value;
|
|
3168
3143
|
}
|
|
3169
3144
|
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3145
|
+
return EXPR_NOT_MATCHED;
|
|
3146
|
+
}
|
|
3147
|
+
function evaluateCallArgs(rawArgs, context) {
|
|
3148
|
+
return rawArgs.trim() ? splitArgs(rawArgs).map((arg) => evalExpr(arg.trim(), context)) : [];
|
|
3149
|
+
}
|
|
3150
|
+
function evalDottedFunction(name, rawArgs, context) {
|
|
3151
|
+
const lastDot = name.lastIndexOf(".");
|
|
3152
|
+
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
3153
|
+
const member = name.slice(lastDot + 1);
|
|
3154
|
+
if (!owner || typeof owner !== "object" || !(member in owner)) {
|
|
3155
|
+
return EXPR_NOT_MATCHED;
|
|
3156
|
+
}
|
|
3157
|
+
const method = owner[member];
|
|
3158
|
+
return typeof method === "function" ? method.apply(owner, evaluateCallArgs(rawArgs, context)) : EXPR_NOT_MATCHED;
|
|
3159
|
+
}
|
|
3160
|
+
function evalFunctionExpression(expr, context) {
|
|
3161
|
+
const match = expr.match(FN_CALL_RE);
|
|
3162
|
+
if (!match) return EXPR_NOT_MATCHED;
|
|
3163
|
+
const name = match[1];
|
|
3164
|
+
const rawArgs = match[2] || "";
|
|
3165
|
+
if (name.includes(".")) return evalDottedFunction(name, rawArgs, context);
|
|
3166
|
+
const fn = context[name] ?? resolveVar(name, context);
|
|
3167
|
+
if (typeof fn === "function") return fn(...evaluateCallArgs(rawArgs, context));
|
|
3168
|
+
return EXPR_NOT_MATCHED;
|
|
3169
|
+
}
|
|
3170
|
+
function evalExpr(expr, context) {
|
|
3171
|
+
expr = expr.trim();
|
|
3172
|
+
const cachedForm = expressionFormCache.get(expr);
|
|
3173
|
+
if (cachedForm !== void 0) {
|
|
3174
|
+
if (cachedForm === -1) return resolveVar(expr, context);
|
|
3175
|
+
const result = EXPR_EVALUATORS[cachedForm](expr, context);
|
|
3176
|
+
return result === EXPR_NOT_MATCHED ? resolveVar(expr, context) : result;
|
|
3177
|
+
}
|
|
3178
|
+
for (let index = 0; index < EXPR_EVALUATORS.length; index++) {
|
|
3179
|
+
const result = EXPR_EVALUATORS[index](expr, context);
|
|
3180
|
+
if (result !== EXPR_NOT_MATCHED) {
|
|
3181
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
3182
|
+
expressionFormCache.set(expr, index);
|
|
3183
|
+
return result;
|
|
3200
3184
|
}
|
|
3201
3185
|
}
|
|
3186
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
3187
|
+
expressionFormCache.set(expr, FN_CALL_RE.test(expr) ? EXPR_EVALUATORS.length - 1 : -1);
|
|
3202
3188
|
return resolveVar(expr, context);
|
|
3203
3189
|
}
|
|
3204
3190
|
function findTernary(expr) {
|
|
@@ -3654,7 +3640,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
3654
3640
|
function _generateFormTokenValue(descriptor = "") {
|
|
3655
3641
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
3656
3642
|
}
|
|
3657
|
-
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;
|
|
3643
|
+
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;
|
|
3658
3644
|
var init_engine = __esm({
|
|
3659
3645
|
"../frond/src/engine.ts"() {
|
|
3660
3646
|
"use strict";
|
|
@@ -3756,6 +3742,25 @@ var init_engine = __esm({
|
|
|
3756
3742
|
MEMO_CACHE_MAX = 1024;
|
|
3757
3743
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
3758
3744
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
3745
|
+
EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
|
|
3746
|
+
ARITHMETIC_OPERATIONS = {
|
|
3747
|
+
"+": (left, right) => left + right,
|
|
3748
|
+
"-": (left, right) => left - right,
|
|
3749
|
+
"*": (left, right) => left * right,
|
|
3750
|
+
"//": (left, right) => right !== 0 ? Math.floor(left / right) : 0,
|
|
3751
|
+
"/": (left, right) => right !== 0 ? left / right : 0,
|
|
3752
|
+
"%": (left, right) => right !== 0 ? left % right : 0,
|
|
3753
|
+
"**": (left, right) => left ** right
|
|
3754
|
+
};
|
|
3755
|
+
EXPR_EVALUATORS = [
|
|
3756
|
+
evalPrimary,
|
|
3757
|
+
evalConditional,
|
|
3758
|
+
evalConcatOrComparison,
|
|
3759
|
+
evalArithmeticExpression,
|
|
3760
|
+
evalFilterExpression,
|
|
3761
|
+
evalFunctionExpression
|
|
3762
|
+
];
|
|
3763
|
+
expressionFormCache = /* @__PURE__ */ new Map();
|
|
3759
3764
|
VarRef = class {
|
|
3760
3765
|
constructor(name) {
|
|
3761
3766
|
this.name = name;
|
|
@@ -8819,14 +8824,14 @@ async function discoverRoutes(routesDir) {
|
|
|
8819
8824
|
const currentMtime = statSync5(filePath).mtimeMs;
|
|
8820
8825
|
if (_seenFiles.has(filePath) && _seenMtimes.get(filePath) === currentMtime) continue;
|
|
8821
8826
|
const method = name.toUpperCase();
|
|
8822
|
-
const
|
|
8823
|
-
const pattern = filePathToPattern(
|
|
8827
|
+
const relativePath2 = relative(routesDir, filePath);
|
|
8828
|
+
const pattern = filePathToPattern(relativePath2);
|
|
8824
8829
|
try {
|
|
8825
8830
|
const moduleUrl = `file://${filePath}?t=${currentMtime}`;
|
|
8826
8831
|
const mod = await import(moduleUrl);
|
|
8827
8832
|
const handler = mod.default ?? mod.handler;
|
|
8828
8833
|
if (typeof handler !== "function") {
|
|
8829
|
-
console.warn(` Warning: ${
|
|
8834
|
+
console.warn(` Warning: ${relativePath2} does not export a handler function, skipping`);
|
|
8830
8835
|
continue;
|
|
8831
8836
|
}
|
|
8832
8837
|
const meta = mod.meta;
|
|
@@ -8838,7 +8843,7 @@ async function discoverRoutes(routesDir) {
|
|
|
8838
8843
|
_seenMtimes.set(filePath, currentMtime);
|
|
8839
8844
|
registeredFromThisScan++;
|
|
8840
8845
|
} catch (err) {
|
|
8841
|
-
console.error(` Error loading route ${
|
|
8846
|
+
console.error(` Error loading route ${relativePath2}:`, err);
|
|
8842
8847
|
recordBrokenImport(filePath, err);
|
|
8843
8848
|
}
|
|
8844
8849
|
}
|
|
@@ -8872,8 +8877,8 @@ function recordBrokenImport(filePath, error) {
|
|
|
8872
8877
|
} catch {
|
|
8873
8878
|
}
|
|
8874
8879
|
}
|
|
8875
|
-
function filePathToPattern(
|
|
8876
|
-
const parts =
|
|
8880
|
+
function filePathToPattern(relativePath2) {
|
|
8881
|
+
const parts = relativePath2.replace(/\\/g, "/").split("/").slice(0, -1);
|
|
8877
8882
|
const urlParts = parts.map((part) => {
|
|
8878
8883
|
if (part.startsWith("[...") && part.endsWith("]")) {
|
|
8879
8884
|
const name = part.slice(4, -1);
|
|
@@ -12110,497 +12115,135 @@ import * as fs3 from "node:fs";
|
|
|
12110
12115
|
import * as path2 from "node:path";
|
|
12111
12116
|
import { spawnSync } from "node:child_process";
|
|
12112
12117
|
import { fileURLToPath } from "node:url";
|
|
12113
|
-
function
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
if (entry.isDirectory()) {
|
|
12120
|
-
if (!exclude.includes(entry.name)) {
|
|
12121
|
-
results.push(...walkFiles(fullPath, extensions, exclude));
|
|
12122
|
-
}
|
|
12123
|
-
} else if (entry.isFile()) {
|
|
12124
|
-
const ext = path2.extname(entry.name);
|
|
12125
|
-
if (extensions.includes(ext) && !entry.name.endsWith(".d.ts")) {
|
|
12126
|
-
results.push(fullPath);
|
|
12127
|
-
}
|
|
12128
|
-
}
|
|
12129
|
-
}
|
|
12130
|
-
return results;
|
|
12131
|
-
}
|
|
12132
|
-
function readFileSafe(filePath) {
|
|
12133
|
-
try {
|
|
12134
|
-
return fs3.readFileSync(filePath, "utf-8");
|
|
12135
|
-
} catch {
|
|
12136
|
-
return null;
|
|
12137
|
-
}
|
|
12138
|
-
}
|
|
12139
|
-
function relativePath(filePath, root = ".") {
|
|
12140
|
-
return path2.relative(root, filePath);
|
|
12141
|
-
}
|
|
12142
|
-
function countLines(source) {
|
|
12143
|
-
const lines = source.split("\n");
|
|
12144
|
-
let loc = 0;
|
|
12145
|
-
let blank = 0;
|
|
12146
|
-
let comment = 0;
|
|
12147
|
-
let inBlockComment = false;
|
|
12148
|
-
for (const line of lines) {
|
|
12149
|
-
const stripped = line.trim();
|
|
12150
|
-
if (!stripped) {
|
|
12151
|
-
blank++;
|
|
12152
|
-
continue;
|
|
12153
|
-
}
|
|
12154
|
-
if (inBlockComment) {
|
|
12155
|
-
comment++;
|
|
12156
|
-
if (stripped.includes("*/")) {
|
|
12157
|
-
inBlockComment = false;
|
|
12158
|
-
}
|
|
12159
|
-
continue;
|
|
12160
|
-
}
|
|
12161
|
-
if (stripped.startsWith("/*")) {
|
|
12162
|
-
comment++;
|
|
12163
|
-
if (!stripped.includes("*/") || stripped.endsWith("/*")) {
|
|
12164
|
-
inBlockComment = true;
|
|
12165
|
-
}
|
|
12166
|
-
continue;
|
|
12167
|
-
}
|
|
12168
|
-
if (stripped.startsWith("//")) {
|
|
12169
|
-
comment++;
|
|
12170
|
-
continue;
|
|
12171
|
-
}
|
|
12172
|
-
loc++;
|
|
12173
|
-
}
|
|
12174
|
-
return { loc, blank, comment };
|
|
12175
|
-
}
|
|
12176
|
-
function stripLiterals(source) {
|
|
12177
|
-
const out = [];
|
|
12178
|
-
const n = source.length;
|
|
12179
|
-
let i = 0;
|
|
12180
|
-
let prevSignificant = "";
|
|
12181
|
-
let prevWord = "";
|
|
12182
|
-
const regexKeywords = /* @__PURE__ */ new Set([
|
|
12183
|
-
"return",
|
|
12184
|
-
"typeof",
|
|
12185
|
-
"instanceof",
|
|
12186
|
-
"in",
|
|
12187
|
-
"of",
|
|
12188
|
-
"new",
|
|
12189
|
-
"delete",
|
|
12190
|
-
"void",
|
|
12191
|
-
"throw",
|
|
12192
|
-
"case",
|
|
12193
|
-
"do",
|
|
12194
|
-
"else",
|
|
12195
|
-
"yield",
|
|
12196
|
-
"await"
|
|
12197
|
-
]);
|
|
12198
|
-
function prevEndsExpression() {
|
|
12199
|
-
if (prevSignificant === "") return false;
|
|
12200
|
-
if (/[A-Za-z0-9_$]/.test(prevSignificant)) {
|
|
12201
|
-
return !regexKeywords.has(prevWord);
|
|
12202
|
-
}
|
|
12203
|
-
if (prevSignificant === ")" || prevSignificant === "]") return true;
|
|
12204
|
-
if (prevSignificant === ".") return true;
|
|
12205
|
-
return false;
|
|
12206
|
-
}
|
|
12207
|
-
while (i < n) {
|
|
12208
|
-
const ch = source[i];
|
|
12209
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
12210
|
-
if (ch === "/" && next === "/") {
|
|
12211
|
-
out.push("//");
|
|
12212
|
-
i += 2;
|
|
12213
|
-
while (i < n && source[i] !== "\n") {
|
|
12214
|
-
out.push(" ");
|
|
12215
|
-
i++;
|
|
12216
|
-
}
|
|
12217
|
-
continue;
|
|
12218
|
-
}
|
|
12219
|
-
if (ch === "/" && next === "*") {
|
|
12220
|
-
out.push("/*");
|
|
12221
|
-
i += 2;
|
|
12222
|
-
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) {
|
|
12223
|
-
out.push(source[i] === "\n" ? "\n" : " ");
|
|
12224
|
-
i++;
|
|
12225
|
-
}
|
|
12226
|
-
if (i < n) {
|
|
12227
|
-
out.push("*/");
|
|
12228
|
-
i += 2;
|
|
12229
|
-
}
|
|
12230
|
-
continue;
|
|
12231
|
-
}
|
|
12232
|
-
if (ch === '"' || ch === "'") {
|
|
12233
|
-
const quote = ch;
|
|
12234
|
-
out.push(quote);
|
|
12235
|
-
i++;
|
|
12236
|
-
while (i < n && source[i] !== quote) {
|
|
12237
|
-
if (source[i] === "\\" && i + 1 < n) {
|
|
12238
|
-
out.push(" ");
|
|
12239
|
-
i += 2;
|
|
12240
|
-
continue;
|
|
12241
|
-
}
|
|
12242
|
-
if (source[i] === "\n") {
|
|
12243
|
-
out.push("\n");
|
|
12244
|
-
i++;
|
|
12245
|
-
break;
|
|
12246
|
-
}
|
|
12247
|
-
out.push(" ");
|
|
12248
|
-
i++;
|
|
12249
|
-
}
|
|
12250
|
-
if (i < n && source[i] === quote) {
|
|
12251
|
-
out.push(quote);
|
|
12252
|
-
i++;
|
|
12253
|
-
}
|
|
12254
|
-
prevSignificant = quote;
|
|
12255
|
-
prevWord = "";
|
|
12256
|
-
continue;
|
|
12257
|
-
}
|
|
12258
|
-
if (ch === "`") {
|
|
12259
|
-
out.push("`");
|
|
12260
|
-
i++;
|
|
12261
|
-
while (i < n && source[i] !== "`") {
|
|
12262
|
-
if (source[i] === "\\" && i + 1 < n) {
|
|
12263
|
-
out.push(source[i + 1] === "\n" ? " \n" : " ");
|
|
12264
|
-
i += 2;
|
|
12265
|
-
continue;
|
|
12266
|
-
}
|
|
12267
|
-
if (source[i] === "$" && source[i + 1] === "{") {
|
|
12268
|
-
out.push("${");
|
|
12269
|
-
i += 2;
|
|
12270
|
-
let depth = 1;
|
|
12271
|
-
const exprStart = i;
|
|
12272
|
-
while (i < n && depth > 0) {
|
|
12273
|
-
if (source[i] === "{") depth++;
|
|
12274
|
-
else if (source[i] === "}") depth--;
|
|
12275
|
-
if (depth === 0) break;
|
|
12276
|
-
i++;
|
|
12277
|
-
}
|
|
12278
|
-
out.push(stripLiterals(source.slice(exprStart, i)));
|
|
12279
|
-
if (i < n && source[i] === "}") {
|
|
12280
|
-
out.push("}");
|
|
12281
|
-
i++;
|
|
12282
|
-
}
|
|
12283
|
-
continue;
|
|
12284
|
-
}
|
|
12285
|
-
out.push(source[i] === "\n" ? "\n" : " ");
|
|
12286
|
-
i++;
|
|
12287
|
-
}
|
|
12288
|
-
if (i < n && source[i] === "`") {
|
|
12289
|
-
out.push("`");
|
|
12290
|
-
i++;
|
|
12291
|
-
}
|
|
12292
|
-
prevSignificant = "`";
|
|
12293
|
-
prevWord = "";
|
|
12294
|
-
continue;
|
|
12295
|
-
}
|
|
12296
|
-
if (ch === "/" && !prevEndsExpression()) {
|
|
12297
|
-
let j = i + 1;
|
|
12298
|
-
let ok = false;
|
|
12299
|
-
let inClass = false;
|
|
12300
|
-
while (j < n) {
|
|
12301
|
-
const c = source[j];
|
|
12302
|
-
if (c === "\\") {
|
|
12303
|
-
j += 2;
|
|
12304
|
-
continue;
|
|
12305
|
-
}
|
|
12306
|
-
if (c === "\n") break;
|
|
12307
|
-
if (c === "[") inClass = true;
|
|
12308
|
-
else if (c === "]") inClass = false;
|
|
12309
|
-
else if (c === "/" && !inClass) {
|
|
12310
|
-
ok = true;
|
|
12311
|
-
break;
|
|
12312
|
-
}
|
|
12313
|
-
j++;
|
|
12314
|
-
}
|
|
12315
|
-
if (ok) {
|
|
12316
|
-
out.push("/");
|
|
12317
|
-
for (let k = i + 1; k < j; k++) out.push(" ");
|
|
12318
|
-
out.push("/");
|
|
12319
|
-
i = j + 1;
|
|
12320
|
-
while (i < n && /[a-z]/i.test(source[i])) {
|
|
12321
|
-
out.push(source[i]);
|
|
12322
|
-
i++;
|
|
12323
|
-
}
|
|
12324
|
-
prevSignificant = "/";
|
|
12325
|
-
prevWord = "";
|
|
12326
|
-
continue;
|
|
12327
|
-
}
|
|
12328
|
-
}
|
|
12329
|
-
out.push(ch);
|
|
12330
|
-
if (!/\s/.test(ch)) {
|
|
12331
|
-
prevSignificant = ch;
|
|
12332
|
-
if (/[A-Za-z0-9_$]/.test(ch)) {
|
|
12333
|
-
prevWord = /[A-Za-z0-9_$]/.test(source[i - 1] ?? "") ? prevWord + ch : ch;
|
|
12334
|
-
} else {
|
|
12335
|
-
prevWord = "";
|
|
12336
|
-
}
|
|
12337
|
-
}
|
|
12338
|
-
i++;
|
|
12339
|
-
}
|
|
12340
|
-
return out.join("");
|
|
12341
|
-
}
|
|
12342
|
-
function countClassesQuick(source) {
|
|
12343
|
-
const matches = source.match(
|
|
12344
|
-
/(?:^|\n)\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/g
|
|
12345
|
-
);
|
|
12346
|
-
return matches ? matches.length : 0;
|
|
12347
|
-
}
|
|
12348
|
-
function countFunctionsQuick(source) {
|
|
12349
|
-
const clean = stripLiterals(source);
|
|
12350
|
-
let count = 0;
|
|
12351
|
-
const funcDecls = clean.match(
|
|
12352
|
-
/(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(/g
|
|
12353
|
-
);
|
|
12354
|
-
if (funcDecls) count += funcDecls.length;
|
|
12355
|
-
const methods = clean.match(
|
|
12356
|
-
/(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*\([^)]*\)\s*(?::\s*\S+)?\s*\{/g
|
|
12357
|
-
);
|
|
12358
|
-
if (methods) count += methods.length;
|
|
12359
|
-
const arrows = clean.match(
|
|
12360
|
-
/(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\(/g
|
|
12361
|
-
);
|
|
12362
|
-
if (arrows) count += arrows.length;
|
|
12363
|
-
return count;
|
|
12364
|
-
}
|
|
12365
|
-
function resolveRoot(root = "src") {
|
|
12366
|
-
const rootPath = path2.resolve(root);
|
|
12367
|
-
if (fs3.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
|
|
12368
|
-
_lastScanRoot = rootPath;
|
|
12369
|
-
return root;
|
|
12370
|
-
}
|
|
12371
|
-
const fwDir = path2.resolve(path2.dirname(new URL(import.meta.url).pathname));
|
|
12372
|
-
_lastScanRoot = fwDir;
|
|
12373
|
-
return fwDir;
|
|
12374
|
-
}
|
|
12375
|
-
function quickMetrics(root = "src") {
|
|
12376
|
-
root = resolveRoot(root);
|
|
12377
|
-
const rootPath = path2.resolve(root);
|
|
12378
|
-
if (!fs3.existsSync(rootPath)) {
|
|
12379
|
-
return { error: `Directory not found: ${root}` };
|
|
12380
|
-
}
|
|
12381
|
-
const tsFiles = walkFiles(rootPath, [".ts", ".js"]);
|
|
12382
|
-
const twigFiles = walkFiles(rootPath, [".twig", ".html"]);
|
|
12383
|
-
const migrationsDir = path2.resolve("migrations");
|
|
12384
|
-
const migrationFiles = [
|
|
12385
|
-
...walkFiles(migrationsDir, [".sql"]),
|
|
12386
|
-
...walkFiles(migrationsDir, [".ts"])
|
|
12387
|
-
];
|
|
12388
|
-
const scssFiles = walkFiles(rootPath, [".scss", ".css"]);
|
|
12389
|
-
let totalLoc = 0;
|
|
12390
|
-
let totalBlank = 0;
|
|
12391
|
-
let totalComment = 0;
|
|
12392
|
-
let totalClasses = 0;
|
|
12393
|
-
let totalFunctions = 0;
|
|
12394
|
-
const fileDetails = [];
|
|
12395
|
-
for (const f of tsFiles) {
|
|
12396
|
-
const source = readFileSafe(f);
|
|
12397
|
-
if (source === null) continue;
|
|
12398
|
-
const counts = countLines(source);
|
|
12399
|
-
const classes = countClassesQuick(source);
|
|
12400
|
-
const functions = countFunctionsQuick(source);
|
|
12401
|
-
totalLoc += counts.loc;
|
|
12402
|
-
totalBlank += counts.blank;
|
|
12403
|
-
totalComment += counts.comment;
|
|
12404
|
-
totalClasses += classes;
|
|
12405
|
-
totalFunctions += functions;
|
|
12406
|
-
fileDetails.push({
|
|
12407
|
-
path: relativePath(f, rootPath),
|
|
12408
|
-
loc: counts.loc,
|
|
12409
|
-
blank: counts.blank,
|
|
12410
|
-
comment: counts.comment,
|
|
12411
|
-
classes,
|
|
12412
|
-
functions
|
|
12413
|
-
});
|
|
12118
|
+
function containsTypeScript(directory) {
|
|
12119
|
+
if (!fs3.existsSync(directory) || !fs3.statSync(directory).isDirectory()) return false;
|
|
12120
|
+
for (const entry of fs3.readdirSync(directory, { withFileTypes: true })) {
|
|
12121
|
+
if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
|
|
12122
|
+
const target = path2.join(directory, entry.name);
|
|
12123
|
+
if (entry.isDirectory() ? containsTypeScript(target) : /\.[cm]?[jt]sx?$/.test(entry.name)) return true;
|
|
12414
12124
|
}
|
|
12415
|
-
|
|
12416
|
-
let routeCount = 0;
|
|
12417
|
-
let ormCount = 0;
|
|
12418
|
-
for (const f of tsFiles) {
|
|
12419
|
-
const source = readFileSafe(f);
|
|
12420
|
-
if (source === null) continue;
|
|
12421
|
-
const routes = source.match(
|
|
12422
|
-
/(?:router\s*\.\s*(?:get|post|put|delete|patch|any)\s*\(|@(?:get|post|put|delete|patch)\s*\()/g
|
|
12423
|
-
);
|
|
12424
|
-
if (routes) routeCount += routes.length;
|
|
12425
|
-
const orms = source.match(
|
|
12426
|
-
/class\s+\w+\s+extends\s+(?:ORM|Model)\b/g
|
|
12427
|
-
);
|
|
12428
|
-
if (orms) ormCount += orms.length;
|
|
12429
|
-
}
|
|
12430
|
-
const breakdown = {
|
|
12431
|
-
typescript: tsFiles.filter((f) => f.endsWith(".ts")).length,
|
|
12432
|
-
javascript: tsFiles.filter((f) => f.endsWith(".js")).length,
|
|
12433
|
-
templates: twigFiles.length,
|
|
12434
|
-
migrations: migrationFiles.length,
|
|
12435
|
-
stylesheets: scssFiles.length
|
|
12436
|
-
};
|
|
12437
|
-
return {
|
|
12438
|
-
file_count: tsFiles.length,
|
|
12439
|
-
total_loc: totalLoc,
|
|
12440
|
-
total_blank: totalBlank,
|
|
12441
|
-
total_comment: totalComment,
|
|
12442
|
-
lloc: totalLoc,
|
|
12443
|
-
classes: totalClasses,
|
|
12444
|
-
functions: totalFunctions,
|
|
12445
|
-
route_count: routeCount,
|
|
12446
|
-
orm_count: ormCount,
|
|
12447
|
-
template_count: twigFiles.length,
|
|
12448
|
-
migration_count: migrationFiles.length,
|
|
12449
|
-
avg_file_size: tsFiles.length > 0 ? Math.round(totalLoc / tsFiles.length * 10) / 10 : 0,
|
|
12450
|
-
largest_files: fileDetails.slice(0, 10),
|
|
12451
|
-
breakdown
|
|
12452
|
-
};
|
|
12125
|
+
return false;
|
|
12453
12126
|
}
|
|
12454
|
-
function
|
|
12455
|
-
const resolved =
|
|
12456
|
-
const
|
|
12457
|
-
|
|
12458
|
-
|
|
12459
|
-
return [resolved, scanningFramework ? "framework" : "project"];
|
|
12127
|
+
function resolveTarget(root = "src") {
|
|
12128
|
+
const resolved = containsTypeScript(root) ? path2.resolve(root) : path2.dirname(fileURLToPath(import.meta.url));
|
|
12129
|
+
const mode = containsTypeScript(root) ? "project" : "framework";
|
|
12130
|
+
lastScanRoot = resolved;
|
|
12131
|
+
return [resolved, mode];
|
|
12460
12132
|
}
|
|
12461
12133
|
function enginePath() {
|
|
12462
|
-
const names = process.platform === "win32" ? ["tina4.exe", "tina4
|
|
12463
|
-
for (const
|
|
12464
|
-
if (!dir) continue;
|
|
12134
|
+
const names = process.platform === "win32" ? ["tina4.exe", "tina4"] : ["tina4"];
|
|
12135
|
+
for (const directory of (process.env.PATH || "").split(path2.delimiter)) {
|
|
12465
12136
|
for (const name of names) {
|
|
12466
|
-
const candidate = path2.join(
|
|
12137
|
+
const candidate = path2.join(directory, name);
|
|
12467
12138
|
try {
|
|
12468
|
-
if (!fs3.statSync(candidate).isFile()) continue;
|
|
12469
12139
|
fs3.accessSync(candidate, fs3.constants.X_OK);
|
|
12140
|
+
if (!fs3.statSync(candidate).isFile()) continue;
|
|
12141
|
+
const descriptor = fs3.openSync(candidate, "r");
|
|
12142
|
+
const header = Buffer.alloc(2);
|
|
12143
|
+
fs3.readSync(descriptor, header, 0, 2, 0);
|
|
12144
|
+
fs3.closeSync(descriptor);
|
|
12145
|
+
if (header.toString("latin1") !== "#!") return candidate;
|
|
12470
12146
|
} catch {
|
|
12471
12147
|
continue;
|
|
12472
12148
|
}
|
|
12473
|
-
try {
|
|
12474
|
-
const fd = fs3.openSync(candidate, "r");
|
|
12475
|
-
const buf = Buffer.alloc(2);
|
|
12476
|
-
fs3.readSync(fd, buf, 0, 2, 0);
|
|
12477
|
-
fs3.closeSync(fd);
|
|
12478
|
-
if (buf.toString("latin1") === "#!") continue;
|
|
12479
|
-
} catch {
|
|
12480
|
-
}
|
|
12481
|
-
return candidate;
|
|
12482
12149
|
}
|
|
12483
12150
|
}
|
|
12484
12151
|
return null;
|
|
12485
12152
|
}
|
|
12486
12153
|
function runEngine(target) {
|
|
12487
12154
|
const binary = enginePath();
|
|
12488
|
-
if (binary
|
|
12489
|
-
|
|
12490
|
-
}
|
|
12491
|
-
const proc = spawnSync(binary, ["metrics", "--path", target, "--json"], {
|
|
12155
|
+
if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
|
|
12156
|
+
const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
|
|
12492
12157
|
encoding: "utf8",
|
|
12493
|
-
timeout:
|
|
12158
|
+
timeout: 6e4,
|
|
12494
12159
|
maxBuffer: 64 * 1024 * 1024
|
|
12495
12160
|
});
|
|
12496
|
-
if (
|
|
12497
|
-
|
|
12498
|
-
if (err.code === "ETIMEDOUT") {
|
|
12499
|
-
throw new MetricsEngineError(`tina4 metrics timed out after ${TIMEOUT_MS / 1e3}s on ${target}`);
|
|
12500
|
-
}
|
|
12501
|
-
throw new MetricsEngineError(`could not run ${binary}: ${err.message}`);
|
|
12502
|
-
}
|
|
12503
|
-
if (proc.status !== 0) {
|
|
12504
|
-
const detail = (proc.stderr || proc.stdout || "").trim().split("\n")[0];
|
|
12505
|
-
throw new MetricsEngineError(
|
|
12506
|
-
`tina4 metrics failed on ${target}: ${detail || `exit code ${proc.status}`}`
|
|
12507
|
-
);
|
|
12161
|
+
if (processResult.error) {
|
|
12162
|
+
throw new MetricsEngineError(`could not run ${binary}: ${processResult.error.message}`);
|
|
12508
12163
|
}
|
|
12509
|
-
if (
|
|
12510
|
-
|
|
12164
|
+
if (processResult.status !== 0) {
|
|
12165
|
+
const detail = (processResult.stderr || processResult.stdout || "").trim().split("\n")[0];
|
|
12166
|
+
throw new MetricsEngineError(`tina4 metrics failed on ${target}: ${detail || processResult.status}`);
|
|
12511
12167
|
}
|
|
12512
|
-
let payload;
|
|
12513
12168
|
try {
|
|
12514
|
-
payload = JSON.parse(
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
|
|
12518
|
-
|
|
12519
|
-
|
|
12169
|
+
const payload = JSON.parse(processResult.stdout);
|
|
12170
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
12171
|
+
throw new Error("non-object payload");
|
|
12172
|
+
}
|
|
12173
|
+
return payload;
|
|
12174
|
+
} catch (error) {
|
|
12175
|
+
throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${error.message}`);
|
|
12520
12176
|
}
|
|
12521
|
-
return payload;
|
|
12522
12177
|
}
|
|
12523
|
-
function
|
|
12524
|
-
|
|
12525
|
-
|
|
12526
|
-
if (!ok) {
|
|
12527
|
-
throw new MetricsEngineError(
|
|
12528
|
-
`engine payload has no usable '${key}' - the installed tina4 CLI predates a field the dashboard renders. Update it: ${INSTALL_HINT}`
|
|
12529
|
-
);
|
|
12178
|
+
function requireArray(payload, key) {
|
|
12179
|
+
if (!Array.isArray(payload[key])) {
|
|
12180
|
+
throw new MetricsEngineError(`engine payload has no usable '${key}' - ${INSTALL_HINT}`);
|
|
12530
12181
|
}
|
|
12531
|
-
return
|
|
12182
|
+
return payload[key];
|
|
12532
12183
|
}
|
|
12533
12184
|
function fullAnalysis(root = "src") {
|
|
12534
|
-
const [resolved, scanMode] =
|
|
12185
|
+
const [resolved, scanMode] = resolveTarget(root);
|
|
12535
12186
|
const payload = runEngine(resolved);
|
|
12536
|
-
const summary =
|
|
12537
|
-
|
|
12538
|
-
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
if (
|
|
12546
|
-
|
|
12547
|
-
|
|
12187
|
+
const summary = payload.summary;
|
|
12188
|
+
if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
|
|
12189
|
+
throw new MetricsEngineError(`engine payload has no usable 'summary' - ${INSTALL_HINT}`);
|
|
12190
|
+
}
|
|
12191
|
+
const fileMetrics = requireArray(payload, "file_metrics");
|
|
12192
|
+
const functions = requireArray(payload, "most_complex_functions");
|
|
12193
|
+
const missingSummary = SUMMARY_KEYS.filter((key) => !(key in summary));
|
|
12194
|
+
if (missingSummary.length) throw new MetricsEngineError(`engine summary is missing ${missingSummary.join(", ")}`);
|
|
12195
|
+
const missingFile = fileMetrics.length ? FILE_KEYS.filter((key) => !(key in fileMetrics[0])) : [];
|
|
12196
|
+
if (missingFile.length) throw new MetricsEngineError(`engine file_metrics is missing ${missingFile.join(", ")}`);
|
|
12197
|
+
const missingFunction = functions.length ? FUNCTION_KEYS.filter((key) => !(key in functions[0])) : [];
|
|
12198
|
+
if (missingFunction.length) {
|
|
12199
|
+
throw new MetricsEngineError(`engine function metrics are missing ${missingFunction.join(", ")}`);
|
|
12548
12200
|
}
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
result.scan_mode = scanMode;
|
|
12559
|
-
result.scan_root = path2.resolve(resolved);
|
|
12560
|
-
result.engine = "tina4-cli";
|
|
12561
|
-
return result;
|
|
12201
|
+
return {
|
|
12202
|
+
...Object.fromEntries(SUMMARY_KEYS.map((key) => [key, summary[key]])),
|
|
12203
|
+
file_metrics: fileMetrics,
|
|
12204
|
+
most_complex_functions: functions.slice(0, 15),
|
|
12205
|
+
dependency_graph: payload.dependency_graph || {},
|
|
12206
|
+
scan_mode: scanMode,
|
|
12207
|
+
scan_root: resolved,
|
|
12208
|
+
engine: "tina4-cli"
|
|
12209
|
+
};
|
|
12562
12210
|
}
|
|
12563
12211
|
function fileDetail(filePath) {
|
|
12564
12212
|
if (!filePath) throw new MetricsEngineError("fileDetail needs a path");
|
|
12565
12213
|
let target = filePath;
|
|
12566
|
-
if (!fs3.existsSync(target) &&
|
|
12567
|
-
const candidate = path2.join(_lastScanRoot, filePath);
|
|
12568
|
-
if (fs3.existsSync(candidate)) target = candidate;
|
|
12569
|
-
}
|
|
12214
|
+
if (!fs3.existsSync(target) && lastScanRoot) target = path2.join(lastScanRoot, filePath);
|
|
12570
12215
|
if (!fs3.existsSync(target)) throw new MetricsEngineError(`no such file: ${filePath}`);
|
|
12571
12216
|
if (fs3.statSync(target).isDirectory()) throw new MetricsEngineError(`not a file: ${filePath}`);
|
|
12572
12217
|
const payload = runEngine(target);
|
|
12573
|
-
const
|
|
12574
|
-
if (!
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12218
|
+
const files = requireArray(payload, "file_metrics");
|
|
12219
|
+
if (!files.length) throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
|
|
12220
|
+
return {
|
|
12221
|
+
...files[0],
|
|
12222
|
+
function_count: files[0].functions || 0,
|
|
12223
|
+
functions: requireArray(payload, "most_complex_functions"),
|
|
12224
|
+
engine: "tina4-cli"
|
|
12225
|
+
};
|
|
12578
12226
|
}
|
|
12579
|
-
var
|
|
12227
|
+
var lastScanRoot, MetricsEngineError, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
|
|
12580
12228
|
var init_metrics = __esm({
|
|
12581
12229
|
"../core/src/metrics.ts"() {
|
|
12582
12230
|
"use strict";
|
|
12583
|
-
|
|
12231
|
+
lastScanRoot = "";
|
|
12584
12232
|
MetricsEngineError = class extends Error {
|
|
12585
12233
|
constructor(message) {
|
|
12586
12234
|
super(message);
|
|
12587
12235
|
this.name = "MetricsEngineError";
|
|
12588
12236
|
}
|
|
12589
12237
|
};
|
|
12590
|
-
|
|
12591
|
-
INSTALL_HINT = [
|
|
12592
|
-
"the tina4 CLI provides the metrics engine (ADR-0002). Install it with",
|
|
12593
|
-
" curl -fsSL https://tina4.com/install.sh | sh",
|
|
12594
|
-
"or see https://tina4.com/cli"
|
|
12595
|
-
].join("\n");
|
|
12238
|
+
INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
|
|
12596
12239
|
SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
|
|
12597
|
-
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "
|
|
12240
|
+
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_referencing_test"];
|
|
12598
12241
|
FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
|
|
12599
12242
|
}
|
|
12600
12243
|
});
|
|
12601
12244
|
|
|
12602
12245
|
// ../core/src/feedback.ts
|
|
12603
|
-
import { readFileSync as
|
|
12246
|
+
import { readFileSync as readFileSync10, existsSync as existsSync11 } from "node:fs";
|
|
12604
12247
|
import { dirname as dirname5, join as join13, resolve as resolve5 } from "node:path";
|
|
12605
12248
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
12606
12249
|
function feedbackEnabled() {
|
|
@@ -12741,7 +12384,7 @@ var init_feedback = __esm({
|
|
|
12741
12384
|
handleFeedbackWidgetJs = (_req, res) => {
|
|
12742
12385
|
let body;
|
|
12743
12386
|
if (existsSync11(WIDGET_BUNDLE_PATH)) {
|
|
12744
|
-
body =
|
|
12387
|
+
body = readFileSync10(WIDGET_BUNDLE_PATH);
|
|
12745
12388
|
} else {
|
|
12746
12389
|
body = "console.warn('tina4-feedback-widget bundle not built yet');";
|
|
12747
12390
|
}
|
|
@@ -12756,7 +12399,7 @@ var init_feedback = __esm({
|
|
|
12756
12399
|
});
|
|
12757
12400
|
|
|
12758
12401
|
// ../core/src/version.ts
|
|
12759
|
-
import { existsSync as existsSync12, readFileSync as
|
|
12402
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
|
|
12760
12403
|
import { dirname as dirname6, join as join14 } from "node:path";
|
|
12761
12404
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
12762
12405
|
function resolveFrameworkVersion() {
|
|
@@ -12765,7 +12408,7 @@ function resolveFrameworkVersion() {
|
|
|
12765
12408
|
const pkgPath = join14(dir, "package.json");
|
|
12766
12409
|
if (existsSync12(pkgPath)) {
|
|
12767
12410
|
try {
|
|
12768
|
-
const pkg = JSON.parse(
|
|
12411
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
12769
12412
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
12770
12413
|
} catch {
|
|
12771
12414
|
}
|
|
@@ -15553,8 +15196,8 @@ __export(context_exports, {
|
|
|
15553
15196
|
fts5Supported: () => fts5Supported
|
|
15554
15197
|
});
|
|
15555
15198
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
15556
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as
|
|
15557
|
-
import { basename as basename4, dirname as dirname8, extname as
|
|
15199
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
|
|
15200
|
+
import { basename as basename4, dirname as dirname8, extname as extname4, isAbsolute as isAbsolute4, join as join16, relative as relative3, resolve as resolve7 } from "node:path";
|
|
15558
15201
|
function fts5Supported() {
|
|
15559
15202
|
try {
|
|
15560
15203
|
const conn = new DatabaseSync2(":memory:");
|
|
@@ -15689,7 +15332,7 @@ var init_context = __esm({
|
|
|
15689
15332
|
}
|
|
15690
15333
|
// ── indexing ───────────────────────────────────────────────
|
|
15691
15334
|
static chunksFor(label, text) {
|
|
15692
|
-
const ext =
|
|
15335
|
+
const ext = extname4(label).toLowerCase();
|
|
15693
15336
|
const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
|
|
15694
15337
|
if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
|
|
15695
15338
|
return chunkCode(text, label);
|
|
@@ -15707,7 +15350,7 @@ var init_context = __esm({
|
|
|
15707
15350
|
const stored = label != null ? String(label) : String(file);
|
|
15708
15351
|
let text;
|
|
15709
15352
|
try {
|
|
15710
|
-
text =
|
|
15353
|
+
text = readFileSync13(file, "utf-8");
|
|
15711
15354
|
} catch {
|
|
15712
15355
|
return 0;
|
|
15713
15356
|
}
|
|
@@ -15728,7 +15371,7 @@ var init_context = __esm({
|
|
|
15728
15371
|
static eligible(filename) {
|
|
15729
15372
|
const fn = filename.toLowerCase();
|
|
15730
15373
|
if (fn.endsWith(".min.js")) return false;
|
|
15731
|
-
const ext =
|
|
15374
|
+
const ext = extname4(fn);
|
|
15732
15375
|
return CODE_EXTS.has(ext) || DOC_EXTS.has(ext) || CONFIG_EXTS.has(ext) || SPECIAL_FILES.has(fn);
|
|
15733
15376
|
}
|
|
15734
15377
|
/**
|
|
@@ -15757,7 +15400,7 @@ var init_context = __esm({
|
|
|
15757
15400
|
for (const fn of files) {
|
|
15758
15401
|
if (!_Context.eligible(fn)) continue;
|
|
15759
15402
|
const full = join16(dir, fn);
|
|
15760
|
-
const rel =
|
|
15403
|
+
const rel = relative3(rootAbs, full);
|
|
15761
15404
|
total += this.indexPath(full, rel);
|
|
15762
15405
|
}
|
|
15763
15406
|
for (const d of subdirs) walk2(join16(dir, d));
|
|
@@ -15778,7 +15421,7 @@ var init_context = __esm({
|
|
|
15778
15421
|
const raw = String(changedPath);
|
|
15779
15422
|
const abs = isAbsolute4(raw) ? raw : join16(process.cwd(), raw);
|
|
15780
15423
|
const resolved = realResolve(resolve7(abs));
|
|
15781
|
-
const rel =
|
|
15424
|
+
const rel = relative3(this.root, resolved);
|
|
15782
15425
|
if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) {
|
|
15783
15426
|
return -1;
|
|
15784
15427
|
}
|
|
@@ -17700,7 +17343,7 @@ var init_job = __esm({
|
|
|
17700
17343
|
});
|
|
17701
17344
|
|
|
17702
17345
|
// ../core/src/queueBackends/liteBackend.ts
|
|
17703
|
-
import { mkdirSync as mkdirSync10, readdirSync as readdirSync8, readFileSync as
|
|
17346
|
+
import { mkdirSync as mkdirSync10, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8, unlinkSync as unlinkSync6, existsSync as existsSync15 } from "node:fs";
|
|
17704
17347
|
import { join as join17 } from "node:path";
|
|
17705
17348
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
17706
17349
|
var LiteBackend;
|
|
@@ -17800,7 +17443,7 @@ var init_liteBackend = __esm({
|
|
|
17800
17443
|
const filePath = join17(dir, filename);
|
|
17801
17444
|
let job;
|
|
17802
17445
|
try {
|
|
17803
|
-
job = JSON.parse(
|
|
17446
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
17804
17447
|
} catch {
|
|
17805
17448
|
continue;
|
|
17806
17449
|
}
|
|
@@ -17864,7 +17507,7 @@ var init_liteBackend = __esm({
|
|
|
17864
17507
|
const filePath = join17(reservedDir, filename);
|
|
17865
17508
|
let record;
|
|
17866
17509
|
try {
|
|
17867
|
-
record = JSON.parse(
|
|
17510
|
+
record = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
17868
17511
|
} catch {
|
|
17869
17512
|
continue;
|
|
17870
17513
|
}
|
|
@@ -17977,7 +17620,7 @@ var init_liteBackend = __esm({
|
|
|
17977
17620
|
let count = 0;
|
|
17978
17621
|
for (const file of files) {
|
|
17979
17622
|
try {
|
|
17980
|
-
const job = JSON.parse(
|
|
17623
|
+
const job = JSON.parse(readFileSync14(join17(scanDir, file), "utf-8"));
|
|
17981
17624
|
if (job.status === status2) count++;
|
|
17982
17625
|
} catch {
|
|
17983
17626
|
}
|
|
@@ -18034,7 +17677,7 @@ var init_liteBackend = __esm({
|
|
|
18034
17677
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
18035
17678
|
for (const file of files) {
|
|
18036
17679
|
try {
|
|
18037
|
-
const job = JSON.parse(
|
|
17680
|
+
const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
|
|
18038
17681
|
const attempts = job.attempts || 0;
|
|
18039
17682
|
if (attempts > 0 && attempts < maxRetries) {
|
|
18040
17683
|
results.push(job);
|
|
@@ -18060,7 +17703,7 @@ var init_liteBackend = __esm({
|
|
|
18060
17703
|
const failedDir = join17(this.basePath, q, "failed");
|
|
18061
17704
|
const filePath = join17(failedDir, `${jobId}.queue-data`);
|
|
18062
17705
|
if (existsSync15(filePath)) {
|
|
18063
|
-
const job = JSON.parse(
|
|
17706
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
18064
17707
|
job.status = "pending";
|
|
18065
17708
|
job.attempts = (job.attempts || 0) + 1;
|
|
18066
17709
|
job.error = void 0;
|
|
@@ -18084,7 +17727,7 @@ var init_liteBackend = __esm({
|
|
|
18084
17727
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
18085
17728
|
for (const file of files) {
|
|
18086
17729
|
try {
|
|
18087
|
-
const job = JSON.parse(
|
|
17730
|
+
const job = JSON.parse(readFileSync14(join17(failedDir, file), "utf-8"));
|
|
18088
17731
|
if ((job.attempts || 0) >= maxRetries) {
|
|
18089
17732
|
job.status = "dead";
|
|
18090
17733
|
results.push(job);
|
|
@@ -18118,7 +17761,7 @@ var init_liteBackend = __esm({
|
|
|
18118
17761
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
|
|
18119
17762
|
for (const file of files) {
|
|
18120
17763
|
try {
|
|
18121
|
-
const job = JSON.parse(
|
|
17764
|
+
const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
|
|
18122
17765
|
if (job.status === status2) {
|
|
18123
17766
|
unlinkSync6(join17(dir, file));
|
|
18124
17767
|
count++;
|
|
@@ -18145,7 +17788,7 @@ var init_liteBackend = __esm({
|
|
|
18145
17788
|
for (const file of files) {
|
|
18146
17789
|
try {
|
|
18147
17790
|
const filePath = join17(failedDir, file);
|
|
18148
|
-
const job = JSON.parse(
|
|
17791
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
18149
17792
|
if ((job.attempts || 0) >= maxRetries) {
|
|
18150
17793
|
continue;
|
|
18151
17794
|
}
|
|
@@ -18177,7 +17820,7 @@ var init_liteBackend = __esm({
|
|
|
18177
17820
|
const filePath = join17(dir, file);
|
|
18178
17821
|
let job;
|
|
18179
17822
|
try {
|
|
18180
|
-
job = JSON.parse(
|
|
17823
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
18181
17824
|
} catch {
|
|
18182
17825
|
continue;
|
|
18183
17826
|
}
|
|
@@ -19640,7 +19283,7 @@ function detectVersion(projectRoot3) {
|
|
|
19640
19283
|
}
|
|
19641
19284
|
return "0.0.0";
|
|
19642
19285
|
}
|
|
19643
|
-
function
|
|
19286
|
+
function relativePath(absPath, projectRoot3, frameworkRoots) {
|
|
19644
19287
|
const norm = path6.resolve(absPath);
|
|
19645
19288
|
for (const fw of frameworkRoots) {
|
|
19646
19289
|
const parent = path6.dirname(fw);
|
|
@@ -20105,7 +19748,7 @@ function buildEntriesForFile(absPath, source, fwRoots, projectRoot3, version, ou
|
|
|
20105
19748
|
} catch {
|
|
20106
19749
|
return;
|
|
20107
19750
|
}
|
|
20108
|
-
const rel =
|
|
19751
|
+
const rel = relativePath(absPath, projectRoot3, fwRoots);
|
|
20109
19752
|
for (const cls of parsed.classes) {
|
|
20110
19753
|
if (!cls.exported && source === "framework") {
|
|
20111
19754
|
continue;
|
|
@@ -20741,8 +20384,8 @@ ${end}
|
|
|
20741
20384
|
|
|
20742
20385
|
// ../core/src/devAdmin.ts
|
|
20743
20386
|
import { cpus as osCpus } from "node:os";
|
|
20744
|
-
import { readFileSync as
|
|
20745
|
-
import { join as join21, dirname as dirname10, resolve as resolve11, relative as
|
|
20387
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, existsSync as existsSync19, readdirSync as readdirSync12, mkdirSync as mkdirSync13, copyFileSync, statSync as statSync14 } from "node:fs";
|
|
20388
|
+
import { join as join21, dirname as dirname10, resolve as resolve11, relative as relative7 } from "node:path";
|
|
20746
20389
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
20747
20390
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
20748
20391
|
function escapeHtml(value) {
|
|
@@ -20861,7 +20504,7 @@ function readQueueDir(dir, topic, status2) {
|
|
|
20861
20504
|
for (const filename of readdirSync12(dir).sort()) {
|
|
20862
20505
|
if (!filename.endsWith(".queue-data")) continue;
|
|
20863
20506
|
try {
|
|
20864
|
-
jobs.push(mapQueueJob(JSON.parse(
|
|
20507
|
+
jobs.push(mapQueueJob(JSON.parse(readFileSync18(join21(dir, filename), "utf-8")), topic, status2));
|
|
20865
20508
|
} catch {
|
|
20866
20509
|
}
|
|
20867
20510
|
}
|
|
@@ -20980,7 +20623,7 @@ function resolveDevEnvVar(key) {
|
|
|
20980
20623
|
if (live !== void 0 && live !== "") return live;
|
|
20981
20624
|
const envPath = join21(process.cwd(), ".env");
|
|
20982
20625
|
if (!existsSync19(envPath)) return "";
|
|
20983
|
-
for (const line of
|
|
20626
|
+
for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
|
|
20984
20627
|
const t = line.trim();
|
|
20985
20628
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
20986
20629
|
const eq = t.indexOf("=");
|
|
@@ -20990,7 +20633,7 @@ function resolveDevEnvVar(key) {
|
|
|
20990
20633
|
}
|
|
20991
20634
|
function upsertDevEnvVar(key, value) {
|
|
20992
20635
|
const envPath = join21(process.cwd(), ".env");
|
|
20993
|
-
const lines = existsSync19(envPath) ?
|
|
20636
|
+
const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
20994
20637
|
let found = false;
|
|
20995
20638
|
const out = [];
|
|
20996
20639
|
for (const line of lines) {
|
|
@@ -21023,7 +20666,7 @@ function parseEnvFile() {
|
|
|
21023
20666
|
const envPath = join21(process.cwd(), ".env");
|
|
21024
20667
|
const result = {};
|
|
21025
20668
|
if (!existsSync19(envPath)) return result;
|
|
21026
|
-
const lines =
|
|
20669
|
+
const lines = readFileSync18(envPath, "utf-8").split("\n");
|
|
21027
20670
|
for (const line of lines) {
|
|
21028
20671
|
const trimmed = line.trim();
|
|
21029
20672
|
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -21063,7 +20706,7 @@ function handleGalleryDeploy(router) {
|
|
|
21063
20706
|
const copied = [];
|
|
21064
20707
|
const allFiles = walkDirRecursive(gallerySrc);
|
|
21065
20708
|
for (const srcFile of allFiles) {
|
|
21066
|
-
const rel =
|
|
20709
|
+
const rel = relative7(gallerySrc, srcFile);
|
|
21067
20710
|
const dest = join21(projectSrc, rel);
|
|
21068
20711
|
mkdirSync13(dirname10(dest), { recursive: true });
|
|
21069
20712
|
copyFileSync(srcFile, dest);
|
|
@@ -21725,9 +21368,6 @@ var init_devAdmin = __esm({
|
|
|
21725
21368
|
{ method: "GET", pattern: "/__dev/api/gallery", handler: handleGalleryList },
|
|
21726
21369
|
{ method: "POST", pattern: "/__dev/api/gallery/deploy", handler: handleGalleryDeploy(router) },
|
|
21727
21370
|
// Metrics
|
|
21728
|
-
{ method: "GET", pattern: "/__dev/api/metrics", handler: (_req, res) => {
|
|
21729
|
-
res.json(quickMetrics());
|
|
21730
|
-
} },
|
|
21731
21371
|
// No fallback (ADR-0002): a missing or stale CLI is a 503 naming the
|
|
21732
21372
|
// install command, never zeros that read as a healthy codebase.
|
|
21733
21373
|
{ method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req, res) => {
|
|
@@ -22536,7 +22176,7 @@ var init_devAdmin = __esm({
|
|
|
22536
22176
|
}
|
|
22537
22177
|
try {
|
|
22538
22178
|
const envPath = join21(process.cwd(), ".env");
|
|
22539
|
-
const lines = existsSync19(envPath) ?
|
|
22179
|
+
const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
22540
22180
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
22541
22181
|
const newLines = [];
|
|
22542
22182
|
for (const line of lines) {
|
|
@@ -22582,12 +22222,12 @@ var init_devAdmin = __esm({
|
|
|
22582
22222
|
const metaFile = join21(entryPath, "meta.json");
|
|
22583
22223
|
if (statSync14(entryPath).isDirectory() && existsSync19(metaFile)) {
|
|
22584
22224
|
try {
|
|
22585
|
-
const meta = JSON.parse(
|
|
22225
|
+
const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
|
|
22586
22226
|
meta.id = entry;
|
|
22587
22227
|
const srcDir = join21(entryPath, "src");
|
|
22588
22228
|
if (existsSync19(srcDir)) {
|
|
22589
22229
|
const allFiles = walkDirRecursive(srcDir);
|
|
22590
|
-
meta.files = allFiles.map((f) =>
|
|
22230
|
+
meta.files = allFiles.map((f) => relative7(srcDir, f));
|
|
22591
22231
|
}
|
|
22592
22232
|
const projectSrc = resolve11(process.cwd(), "src");
|
|
22593
22233
|
if (existsSync19(srcDir) && meta.files) {
|
|
@@ -22705,7 +22345,7 @@ var init_devAdmin = __esm({
|
|
|
22705
22345
|
for (const name of readdirSync12(target).sort()) {
|
|
22706
22346
|
if (devFilesHidden(name)) continue;
|
|
22707
22347
|
const full = join21(target, name);
|
|
22708
|
-
const entryRel =
|
|
22348
|
+
const entryRel = relative7(root, full).replace(/\\/g, "/");
|
|
22709
22349
|
if (isSecretPath(entryRel)) continue;
|
|
22710
22350
|
let isDir = false;
|
|
22711
22351
|
let size = null;
|
|
@@ -22750,7 +22390,7 @@ var init_devAdmin = __esm({
|
|
|
22750
22390
|
size
|
|
22751
22391
|
});
|
|
22752
22392
|
}
|
|
22753
|
-
res.json({ path:
|
|
22393
|
+
res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
|
|
22754
22394
|
};
|
|
22755
22395
|
DEV_ADMIN_LANG_MAP = {
|
|
22756
22396
|
".py": "python",
|
|
@@ -22802,8 +22442,8 @@ var init_devAdmin = __esm({
|
|
|
22802
22442
|
return;
|
|
22803
22443
|
}
|
|
22804
22444
|
try {
|
|
22805
|
-
const content =
|
|
22806
|
-
const path8 =
|
|
22445
|
+
const content = readFileSync18(target, "utf-8");
|
|
22446
|
+
const path8 = relative7(root, target);
|
|
22807
22447
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
22808
22448
|
} catch (e) {
|
|
22809
22449
|
res.json({ error: e.message }, 500);
|
|
@@ -22825,10 +22465,10 @@ var init_devAdmin = __esm({
|
|
|
22825
22465
|
writeFileSync12(target, content, "utf-8");
|
|
22826
22466
|
try {
|
|
22827
22467
|
const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
22828
|
-
Plan2.recordAction(existed ? "patched" : "created",
|
|
22468
|
+
Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
|
|
22829
22469
|
} catch {
|
|
22830
22470
|
}
|
|
22831
|
-
res.json({ ok: true, path:
|
|
22471
|
+
res.json({ ok: true, path: relative7(root, target), bytes: Buffer.byteLength(content, "utf-8") });
|
|
22832
22472
|
} catch (e) {
|
|
22833
22473
|
res.json({ error: e.message }, 500);
|
|
22834
22474
|
}
|
|
@@ -22848,7 +22488,7 @@ var init_devAdmin = __esm({
|
|
|
22848
22488
|
return;
|
|
22849
22489
|
}
|
|
22850
22490
|
try {
|
|
22851
|
-
const buf =
|
|
22491
|
+
const buf = readFileSync18(target);
|
|
22852
22492
|
const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
22853
22493
|
const mime = {
|
|
22854
22494
|
js: "application/javascript",
|
|
@@ -22890,7 +22530,7 @@ var init_devAdmin = __esm({
|
|
|
22890
22530
|
const { renameSync: renameSync3 } = await import("node:fs");
|
|
22891
22531
|
mkdirSync13(dirname10(dst), { recursive: true });
|
|
22892
22532
|
renameSync3(src, dst);
|
|
22893
|
-
res.json({ ok: true, from:
|
|
22533
|
+
res.json({ ok: true, from: relative7(root, src), to: relative7(root, dst) });
|
|
22894
22534
|
} catch (e) {
|
|
22895
22535
|
res.json({ error: e.message }, 500);
|
|
22896
22536
|
}
|
|
@@ -22911,7 +22551,7 @@ var init_devAdmin = __esm({
|
|
|
22911
22551
|
try {
|
|
22912
22552
|
const { rmSync } = await import("node:fs");
|
|
22913
22553
|
rmSync(target, { recursive: true, force: true });
|
|
22914
|
-
res.json({ ok: true, deleted:
|
|
22554
|
+
res.json({ ok: true, deleted: relative7(root, target) });
|
|
22915
22555
|
} catch (e) {
|
|
22916
22556
|
res.json({ error: e.message }, 500);
|
|
22917
22557
|
}
|
|
@@ -23245,7 +22885,7 @@ var init_devAdmin = __esm({
|
|
|
23245
22885
|
});
|
|
23246
22886
|
};
|
|
23247
22887
|
handleDevAdminJs = async (_req, res) => {
|
|
23248
|
-
const { readFileSync:
|
|
22888
|
+
const { readFileSync: readFileSync27, existsSync: existsSync27 } = await import("node:fs");
|
|
23249
22889
|
const { dirname: dirname15, join: join32, resolve: resolve20 } = await import("node:path");
|
|
23250
22890
|
const { fileURLToPath: fileURLToPath8 } = await import("node:url");
|
|
23251
22891
|
const dir = dirname15(fileURLToPath8(import.meta.url));
|
|
@@ -23261,7 +22901,7 @@ var init_devAdmin = __esm({
|
|
|
23261
22901
|
for (const jsPath of candidates) {
|
|
23262
22902
|
if (existsSync27(jsPath)) {
|
|
23263
22903
|
try {
|
|
23264
|
-
const content =
|
|
22904
|
+
const content = readFileSync27(jsPath, "utf-8");
|
|
23265
22905
|
res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
23266
22906
|
res.raw.end(content);
|
|
23267
22907
|
return;
|
|
@@ -23276,7 +22916,7 @@ var init_devAdmin = __esm({
|
|
|
23276
22916
|
});
|
|
23277
22917
|
|
|
23278
22918
|
// ../core/src/i18n.ts
|
|
23279
|
-
import { readFileSync as
|
|
22919
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync20 } from "node:fs";
|
|
23280
22920
|
import { join as join22, resolve as resolve12 } from "node:path";
|
|
23281
22921
|
var I18n;
|
|
23282
22922
|
var init_i18n = __esm({
|
|
@@ -23373,7 +23013,7 @@ var init_i18n = __esm({
|
|
|
23373
23013
|
const filePath = join22(this._localeDir, `${locale}.json`);
|
|
23374
23014
|
if (existsSync20(filePath)) {
|
|
23375
23015
|
try {
|
|
23376
|
-
const raw =
|
|
23016
|
+
const raw = readFileSync19(filePath, "utf-8");
|
|
23377
23017
|
const data = JSON.parse(raw);
|
|
23378
23018
|
this._translations.set(locale, _I18n._flatten(data));
|
|
23379
23019
|
return;
|
|
@@ -23386,7 +23026,7 @@ var init_i18n = __esm({
|
|
|
23386
23026
|
const yamlPath = join22(this._localeDir, `${locale}${ext}`);
|
|
23387
23027
|
if (existsSync20(yamlPath)) {
|
|
23388
23028
|
try {
|
|
23389
|
-
const raw =
|
|
23029
|
+
const raw = readFileSync19(yamlPath, "utf-8");
|
|
23390
23030
|
const data = _I18n._parseSimpleYaml(raw);
|
|
23391
23031
|
this._translations.set(locale, _I18n._flatten(data));
|
|
23392
23032
|
return;
|
|
@@ -24249,8 +23889,8 @@ var init_docsAutoDiscovery = __esm({
|
|
|
24249
23889
|
// ../core/src/server.ts
|
|
24250
23890
|
import { createServer as createServer2 } from "node:http";
|
|
24251
23891
|
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
24252
|
-
import { resolve as resolve14, dirname as dirname11, join as join24, relative as
|
|
24253
|
-
import { existsSync as existsSync22, readdirSync as readdirSync14, readFileSync as
|
|
23892
|
+
import { resolve as resolve14, dirname as dirname11, join as join24, relative as relative8 } from "node:path";
|
|
23893
|
+
import { existsSync as existsSync22, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
|
|
24254
23894
|
import { isatty } from "node:tty";
|
|
24255
23895
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
24256
23896
|
import { execFileSync as execFileSync3, exec } from "node:child_process";
|
|
@@ -24474,7 +24114,7 @@ function getGalleryDeployedState() {
|
|
|
24474
24114
|
if (existsSync22(srcDir)) {
|
|
24475
24115
|
const files = walkGalleryFiles(srcDir);
|
|
24476
24116
|
const projectSrc = resolve14(process.cwd(), "src");
|
|
24477
|
-
state[entry] = files.every((f) => existsSync22(join24(projectSrc,
|
|
24117
|
+
state[entry] = files.every((f) => existsSync22(join24(projectSrc, relative8(srcDir, f))));
|
|
24478
24118
|
} else {
|
|
24479
24119
|
state[entry] = false;
|
|
24480
24120
|
}
|
|
@@ -24911,7 +24551,7 @@ function serveTemplateFallback(ctx) {
|
|
|
24911
24551
|
if ((ctx.req.method ?? "GET") !== "GET") return false;
|
|
24912
24552
|
const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
|
|
24913
24553
|
if (!tplFile) return false;
|
|
24914
|
-
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) :
|
|
24554
|
+
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(resolve14(ctx.templatesDir, tplFile), "utf-8");
|
|
24915
24555
|
ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
24916
24556
|
ctx.res.raw.end(html);
|
|
24917
24557
|
return true;
|
|
@@ -26206,7 +25846,7 @@ var init_mqttMessage = __esm({
|
|
|
26206
25846
|
import net2 from "node:net";
|
|
26207
25847
|
import tls from "node:tls";
|
|
26208
25848
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
26209
|
-
import { existsSync as existsSync24, readFileSync as
|
|
25849
|
+
import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
|
|
26210
25850
|
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;
|
|
26211
25851
|
var init_mqtt = __esm({
|
|
26212
25852
|
"../core/src/mqtt.ts"() {
|
|
@@ -26671,7 +26311,7 @@ var init_mqtt = __esm({
|
|
|
26671
26311
|
servername: this.host,
|
|
26672
26312
|
rejectUnauthorized: this.tlsVerify
|
|
26673
26313
|
};
|
|
26674
|
-
if (this.tlsVerify && this.caFile) opts.ca =
|
|
26314
|
+
if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
|
|
26675
26315
|
sock = tls.connect(opts, () => settle(() => resolve20(sock)));
|
|
26676
26316
|
} else {
|
|
26677
26317
|
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
|
|
@@ -26892,7 +26532,7 @@ var init_mqtt = __esm({
|
|
|
26892
26532
|
|
|
26893
26533
|
// ../core/src/service.ts
|
|
26894
26534
|
import { readdirSync as readdirSync16, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
|
|
26895
|
-
import { join as join26, extname as
|
|
26535
|
+
import { join as join26, extname as extname6 } from "node:path";
|
|
26896
26536
|
import { pathToFileURL } from "node:url";
|
|
26897
26537
|
function matchCronField(field, value) {
|
|
26898
26538
|
if (field === "*") return true;
|
|
@@ -27063,7 +26703,7 @@ var init_service = __esm({
|
|
|
27063
26703
|
return discovered;
|
|
27064
26704
|
}
|
|
27065
26705
|
for (const entry of entries) {
|
|
27066
|
-
const ext =
|
|
26706
|
+
const ext = extname6(entry);
|
|
27067
26707
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
27068
26708
|
const fullPath = join26(dir, entry);
|
|
27069
26709
|
const stat = statSync16(fullPath);
|
|
@@ -27179,7 +26819,7 @@ var init_service = __esm({
|
|
|
27179
26819
|
return;
|
|
27180
26820
|
}
|
|
27181
26821
|
for (const entry of entries) {
|
|
27182
|
-
const ext =
|
|
26822
|
+
const ext = extname6(entry);
|
|
27183
26823
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
27184
26824
|
const fullPath = join26(dir, entry);
|
|
27185
26825
|
if (watchedFiles.has(fullPath)) continue;
|
|
@@ -27882,7 +27522,7 @@ var init_api = __esm({
|
|
|
27882
27522
|
// ../core/src/messenger.ts
|
|
27883
27523
|
import net3 from "node:net";
|
|
27884
27524
|
import tls2 from "node:tls";
|
|
27885
|
-
import { readFileSync as
|
|
27525
|
+
import { readFileSync as readFileSync23 } from "node:fs";
|
|
27886
27526
|
import { basename as basename6 } from "node:path";
|
|
27887
27527
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
27888
27528
|
function tlsRejectUnauthorized() {
|
|
@@ -27979,7 +27619,7 @@ function buildMimeMessage(options) {
|
|
|
27979
27619
|
}
|
|
27980
27620
|
for (const filePath of options.attachments) {
|
|
27981
27621
|
const fileName = basename6(filePath);
|
|
27982
|
-
const fileData =
|
|
27622
|
+
const fileData = readFileSync23(filePath);
|
|
27983
27623
|
const base64Data = fileData.toString("base64");
|
|
27984
27624
|
lines.push("");
|
|
27985
27625
|
lines.push(`--${boundary}`);
|
|
@@ -29442,9 +29082,9 @@ var init_htmlElement = __esm({
|
|
|
29442
29082
|
});
|
|
29443
29083
|
|
|
29444
29084
|
// ../core/src/ai.ts
|
|
29445
|
-
import { existsSync as existsSync25, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15, readFileSync as
|
|
29085
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15, readFileSync as readFileSync24 } from "node:fs";
|
|
29446
29086
|
import { homedir } from "node:os";
|
|
29447
|
-
import { join as join27, resolve as resolve16, relative as
|
|
29087
|
+
import { join as join27, resolve as resolve16, relative as relative9, dirname as dirname12 } from "node:path";
|
|
29448
29088
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
29449
29089
|
import { execSync, execFileSync as execFileSync4 } from "node:child_process";
|
|
29450
29090
|
import { createInterface } from "node:readline";
|
|
@@ -29452,7 +29092,7 @@ function readVersion() {
|
|
|
29452
29092
|
try {
|
|
29453
29093
|
const thisDir = dirname12(fileURLToPath7(import.meta.url));
|
|
29454
29094
|
const rootPkg = resolve16(thisDir, "..", "..", "..", "package.json");
|
|
29455
|
-
const pkg = JSON.parse(
|
|
29095
|
+
const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
|
|
29456
29096
|
return pkg.version ?? "0.0.0";
|
|
29457
29097
|
} catch {
|
|
29458
29098
|
return "0.0.0";
|
|
@@ -29675,7 +29315,7 @@ function writeOrMerge(contextPath, contextFile, frameworkGuide) {
|
|
|
29675
29315
|
writeFileSync15(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
29676
29316
|
return "Installed";
|
|
29677
29317
|
}
|
|
29678
|
-
const existing =
|
|
29318
|
+
const existing = readFileSync24(contextPath, "utf-8");
|
|
29679
29319
|
if (hasMarkers(existing, start2, end)) {
|
|
29680
29320
|
writeFileSync15(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
|
|
29681
29321
|
return "Refreshed skill block in";
|
|
@@ -29699,7 +29339,7 @@ function installForTool(root, tool, context) {
|
|
|
29699
29339
|
const parentDir = dirname12(contextPath);
|
|
29700
29340
|
mkdirSync16(parentDir, { recursive: true });
|
|
29701
29341
|
const action = writeOrMerge(contextPath, tool.contextFile, context);
|
|
29702
|
-
const rel =
|
|
29342
|
+
const rel = relative9(root, contextPath);
|
|
29703
29343
|
created.push(rel);
|
|
29704
29344
|
console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
|
|
29705
29345
|
if (tool.name === "claude-code") {
|
|
@@ -30077,7 +29717,7 @@ function generateClaudeCodeContext() {
|
|
|
30077
29717
|
const repoRoot = resolve16(thisDir, "..", "..", "..");
|
|
30078
29718
|
const claudeMdPath = join27(repoRoot, "CLAUDE.md");
|
|
30079
29719
|
if (existsSync25(claudeMdPath)) {
|
|
30080
|
-
return
|
|
29720
|
+
return readFileSync24(claudeMdPath, "utf-8");
|
|
30081
29721
|
}
|
|
30082
29722
|
} catch {
|
|
30083
29723
|
}
|
|
@@ -30271,6 +29911,292 @@ export default class User {
|
|
|
30271
29911
|
}
|
|
30272
29912
|
});
|
|
30273
29913
|
|
|
29914
|
+
// ../core/src/aiClient.ts
|
|
29915
|
+
import http2 from "node:http";
|
|
29916
|
+
import https2 from "node:https";
|
|
29917
|
+
var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai;
|
|
29918
|
+
var init_aiClient = __esm({
|
|
29919
|
+
"../core/src/aiClient.ts"() {
|
|
29920
|
+
"use strict";
|
|
29921
|
+
AiError = class extends Error {
|
|
29922
|
+
};
|
|
29923
|
+
AiConfigError = class extends AiError {
|
|
29924
|
+
};
|
|
29925
|
+
AiTimeoutError = class extends AiError {
|
|
29926
|
+
};
|
|
29927
|
+
AiParseError = class extends AiError {
|
|
29928
|
+
};
|
|
29929
|
+
AiHTTPError = class extends AiError {
|
|
29930
|
+
constructor(message, status2 = null) {
|
|
29931
|
+
super(message);
|
|
29932
|
+
this.status = status2;
|
|
29933
|
+
}
|
|
29934
|
+
};
|
|
29935
|
+
Ai = class {
|
|
29936
|
+
static chat(messages, options = {}) {
|
|
29937
|
+
this.validateMessages(messages);
|
|
29938
|
+
const config = this.config("chat", options);
|
|
29939
|
+
const body = this.chatBody(config, messages, options);
|
|
29940
|
+
const headers = this.headers(config);
|
|
29941
|
+
return options.stream ? this.streamRequest(config, headers, body) : this.chatResponse(config, headers, body);
|
|
29942
|
+
}
|
|
29943
|
+
static async complete(prompt, options = {}) {
|
|
29944
|
+
if (typeof prompt !== "string") throw new AiConfigError("AI prompt must be a string");
|
|
29945
|
+
return (await this.chat([{ role: "user", content: prompt }], { ...options, stream: false })).text;
|
|
29946
|
+
}
|
|
29947
|
+
static async embed(textOrTexts, options = {}) {
|
|
29948
|
+
const single = typeof textOrTexts === "string";
|
|
29949
|
+
if (!single && (!Array.isArray(textOrTexts) || textOrTexts.length === 0 || !textOrTexts.every((item) => typeof item === "string"))) {
|
|
29950
|
+
throw new AiConfigError("AI embedding input must be a string or a non-empty list of strings");
|
|
29951
|
+
}
|
|
29952
|
+
const config = this.config("embed", options);
|
|
29953
|
+
if (config.provider === "anthropic") throw new AiConfigError("Anthropic does not provide the embedding endpoint in this contract");
|
|
29954
|
+
const raw = await this.requestJson(config, this.headers(config), { model: config.model, input: textOrTexts });
|
|
29955
|
+
try {
|
|
29956
|
+
const data = raw.data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
29957
|
+
const vectors = data.map((item) => item.embedding);
|
|
29958
|
+
const expected = single ? 1 : textOrTexts.length;
|
|
29959
|
+
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();
|
|
29960
|
+
return single ? vectors[0] : vectors;
|
|
29961
|
+
} catch {
|
|
29962
|
+
throw new AiParseError("AI provider returned a malformed embedding response");
|
|
29963
|
+
}
|
|
29964
|
+
}
|
|
29965
|
+
static validateMessages(messages) {
|
|
29966
|
+
if (!Array.isArray(messages) || messages.length === 0 || !messages.every((message) => message && ["system", "user", "assistant"].includes(message.role) && typeof message.content === "string")) {
|
|
29967
|
+
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
29968
|
+
}
|
|
29969
|
+
}
|
|
29970
|
+
static number(name, fallback, minimum) {
|
|
29971
|
+
const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
|
|
29972
|
+
if (!Number.isFinite(value) || value < minimum) throw new AiConfigError(`${name} must be numeric and at least ${minimum}`);
|
|
29973
|
+
return value;
|
|
29974
|
+
}
|
|
29975
|
+
static config(capability, options) {
|
|
29976
|
+
const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
|
|
29977
|
+
if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
|
|
29978
|
+
const key = process.env.TINA4_AI_KEY || null;
|
|
29979
|
+
if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
29980
|
+
const defaults = {
|
|
29981
|
+
local: ["http://localhost:11437", "llama3.2"],
|
|
29982
|
+
openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
|
|
29983
|
+
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
|
|
29984
|
+
};
|
|
29985
|
+
const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
|
|
29986
|
+
const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
|
|
29987
|
+
if (!model) throw new AiConfigError("AI model must be a non-empty string");
|
|
29988
|
+
const totalTimeout = options.timeout === void 0 ? this.number("TINA4_AI_TIMEOUT", 60, 1e-3) : Number(options.timeout);
|
|
29989
|
+
if (!Number.isFinite(totalTimeout) || totalTimeout <= 0) throw new AiConfigError("AI timeout must be greater than zero");
|
|
29990
|
+
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)) };
|
|
29991
|
+
}
|
|
29992
|
+
static endpoint(value, capability, provider) {
|
|
29993
|
+
let url;
|
|
29994
|
+
try {
|
|
29995
|
+
url = new URL(value);
|
|
29996
|
+
} catch {
|
|
29997
|
+
throw new AiConfigError("AI URL must be an http or https URL");
|
|
29998
|
+
}
|
|
29999
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
|
|
30000
|
+
const path8 = url.pathname.replace(/\/+$/, "");
|
|
30001
|
+
if (path8 === "" || path8 === "/v1" || path8 === "/api") {
|
|
30002
|
+
const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
|
|
30003
|
+
url.pathname = (path8 || "/v1") + suffix;
|
|
30004
|
+
}
|
|
30005
|
+
return url.toString();
|
|
30006
|
+
}
|
|
30007
|
+
static headers(config) {
|
|
30008
|
+
const headers = { "content-type": "application/json", accept: "application/json" };
|
|
30009
|
+
if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
|
|
30010
|
+
if (config.provider === "anthropic") {
|
|
30011
|
+
headers["x-api-key"] = config.key;
|
|
30012
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
30013
|
+
}
|
|
30014
|
+
return headers;
|
|
30015
|
+
}
|
|
30016
|
+
static chatBody(config, messages, options) {
|
|
30017
|
+
const body = { model: config.model, messages, stream: options.stream ?? false };
|
|
30018
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
30019
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
30020
|
+
if (config.provider === "anthropic") {
|
|
30021
|
+
const system = messages.filter((message) => message.role === "system").map((message) => message.content);
|
|
30022
|
+
body.messages = messages.filter((message) => message.role !== "system");
|
|
30023
|
+
body.max_tokens = options.maxTokens ?? 1024;
|
|
30024
|
+
if (system.length) body.system = system.join("\n\n");
|
|
30025
|
+
}
|
|
30026
|
+
return body;
|
|
30027
|
+
}
|
|
30028
|
+
static open(config, deadline, headers, body) {
|
|
30029
|
+
const remainingMs = deadline - performance.now();
|
|
30030
|
+
if (remainingMs <= 0) return Promise.reject(new AiTimeoutError("AI total request timeout expired"));
|
|
30031
|
+
const url = new URL(config.url);
|
|
30032
|
+
const payload = JSON.stringify(body);
|
|
30033
|
+
const controller = new AbortController();
|
|
30034
|
+
const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
|
|
30035
|
+
return new Promise((resolve20, reject) => {
|
|
30036
|
+
const client = url.protocol === "https:" ? https2 : http2;
|
|
30037
|
+
const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
|
|
30038
|
+
clearTimeout(connectTimer);
|
|
30039
|
+
resolve20({ response, cleanup: () => {
|
|
30040
|
+
clearTimeout(totalTimer);
|
|
30041
|
+
clearTimeout(connectTimer);
|
|
30042
|
+
} });
|
|
30043
|
+
});
|
|
30044
|
+
const connectTimer = setTimeout(() => request.destroy(new AiTimeoutError("AI connection timeout expired")), Math.min(config.connectTimeout * 1e3, remainingMs));
|
|
30045
|
+
request.on("socket", (socket) => {
|
|
30046
|
+
if (!socket.connecting) clearTimeout(connectTimer);
|
|
30047
|
+
socket.once(url.protocol === "https:" ? "secureConnect" : "connect", () => clearTimeout(connectTimer));
|
|
30048
|
+
});
|
|
30049
|
+
request.once("error", (error) => {
|
|
30050
|
+
clearTimeout(totalTimer);
|
|
30051
|
+
clearTimeout(connectTimer);
|
|
30052
|
+
if (error instanceof AiError) reject(error);
|
|
30053
|
+
else if (controller.signal.aborted) reject(new AiTimeoutError("AI total request timeout expired"));
|
|
30054
|
+
else reject(new AiHTTPError(`AI transport failed (${error.name})`));
|
|
30055
|
+
});
|
|
30056
|
+
request.end(payload);
|
|
30057
|
+
});
|
|
30058
|
+
}
|
|
30059
|
+
static async readBody(response) {
|
|
30060
|
+
const chunks = [];
|
|
30061
|
+
for await (const chunk of response) chunks.push(Buffer.from(chunk));
|
|
30062
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
30063
|
+
}
|
|
30064
|
+
static retryDelay(headers, deadline) {
|
|
30065
|
+
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
30066
|
+
const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
|
|
30067
|
+
const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
|
|
30068
|
+
return new Promise((resolve20) => setTimeout(resolve20, delay));
|
|
30069
|
+
}
|
|
30070
|
+
static async requestJson(config, headers, body) {
|
|
30071
|
+
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
30072
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
30073
|
+
let opened = null;
|
|
30074
|
+
try {
|
|
30075
|
+
opened = await this.open(config, deadline, headers, body);
|
|
30076
|
+
const status2 = opened.response.statusCode ?? 0;
|
|
30077
|
+
const responseHeaders = opened.response.headers;
|
|
30078
|
+
const raw = await this.readBody(opened.response);
|
|
30079
|
+
opened.cleanup();
|
|
30080
|
+
opened = null;
|
|
30081
|
+
if (status2 < 200 || status2 >= 300) {
|
|
30082
|
+
if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
|
|
30083
|
+
await this.retryDelay(responseHeaders, deadline);
|
|
30084
|
+
continue;
|
|
30085
|
+
}
|
|
30086
|
+
throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
|
|
30087
|
+
}
|
|
30088
|
+
let parsed;
|
|
30089
|
+
try {
|
|
30090
|
+
parsed = JSON.parse(raw);
|
|
30091
|
+
} catch {
|
|
30092
|
+
throw new AiParseError("AI provider returned malformed JSON");
|
|
30093
|
+
}
|
|
30094
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new AiParseError("AI provider returned a non-object JSON response");
|
|
30095
|
+
return parsed;
|
|
30096
|
+
} catch (error) {
|
|
30097
|
+
opened?.cleanup();
|
|
30098
|
+
if (error instanceof AiParseError || error instanceof AiHTTPError && error.status !== null) throw error;
|
|
30099
|
+
if (attempt >= config.maxRetries) throw error;
|
|
30100
|
+
}
|
|
30101
|
+
}
|
|
30102
|
+
throw new AiHTTPError("AI request failed");
|
|
30103
|
+
}
|
|
30104
|
+
static normalizeChat(provider, raw) {
|
|
30105
|
+
try {
|
|
30106
|
+
if (provider === "anthropic") {
|
|
30107
|
+
const content = raw.content;
|
|
30108
|
+
const parts = content.filter((item) => (item.type ?? "text") === "text").map((item) => item.text);
|
|
30109
|
+
if (!parts.length || !parts.every((part) => typeof part === "string")) throw new Error();
|
|
30110
|
+
const usage2 = raw.usage ?? {};
|
|
30111
|
+
const promptTokens = Number(usage2.input_tokens ?? 0);
|
|
30112
|
+
const completionTokens = Number(usage2.output_tokens ?? 0);
|
|
30113
|
+
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 };
|
|
30114
|
+
}
|
|
30115
|
+
const choice = raw.choices[0];
|
|
30116
|
+
const text = choice.message.content;
|
|
30117
|
+
if (typeof text !== "string") throw new Error();
|
|
30118
|
+
const usage = raw.usage ?? {};
|
|
30119
|
+
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 };
|
|
30120
|
+
} catch {
|
|
30121
|
+
throw new AiParseError("AI provider returned a malformed chat response");
|
|
30122
|
+
}
|
|
30123
|
+
}
|
|
30124
|
+
static async chatResponse(config, headers, body) {
|
|
30125
|
+
return this.normalizeChat(config.provider, await this.requestJson(config, headers, body));
|
|
30126
|
+
}
|
|
30127
|
+
static streamDelta(provider, data) {
|
|
30128
|
+
if (data === "[DONE]") return { completed: true };
|
|
30129
|
+
let event;
|
|
30130
|
+
try {
|
|
30131
|
+
event = JSON.parse(data);
|
|
30132
|
+
} catch {
|
|
30133
|
+
throw new AiParseError("AI provider returned malformed stream data");
|
|
30134
|
+
}
|
|
30135
|
+
const text = provider === "anthropic" ? event.type === "content_block_delta" ? event.delta?.text : void 0 : event.choices?.[0]?.delta?.content;
|
|
30136
|
+
if (text !== void 0 && text !== null && typeof text !== "string") throw new AiParseError("AI provider returned malformed stream data");
|
|
30137
|
+
return { completed: false, text };
|
|
30138
|
+
}
|
|
30139
|
+
static async *streamData(response) {
|
|
30140
|
+
let buffer = "";
|
|
30141
|
+
for await (const chunk of response) {
|
|
30142
|
+
buffer += Buffer.from(chunk).toString("utf8");
|
|
30143
|
+
let newline;
|
|
30144
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
30145
|
+
const line = buffer.slice(0, newline).trim();
|
|
30146
|
+
buffer = buffer.slice(newline + 1);
|
|
30147
|
+
if (line.startsWith("data:")) yield line.slice(5).trim();
|
|
30148
|
+
}
|
|
30149
|
+
}
|
|
30150
|
+
}
|
|
30151
|
+
static streamError(error) {
|
|
30152
|
+
if (error instanceof AiError) return error;
|
|
30153
|
+
if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
|
|
30154
|
+
return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
|
|
30155
|
+
}
|
|
30156
|
+
static async *streamRequest(config, headers, body) {
|
|
30157
|
+
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
30158
|
+
let yielded = false;
|
|
30159
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
30160
|
+
let opened = null;
|
|
30161
|
+
try {
|
|
30162
|
+
opened = await this.open(config, deadline, { ...headers, accept: "text/event-stream" }, body);
|
|
30163
|
+
const status2 = opened.response.statusCode ?? 0;
|
|
30164
|
+
if (status2 < 200 || status2 >= 300) {
|
|
30165
|
+
await this.readBody(opened.response);
|
|
30166
|
+
if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
|
|
30167
|
+
await this.retryDelay(opened.response.headers, deadline);
|
|
30168
|
+
opened.cleanup();
|
|
30169
|
+
opened = null;
|
|
30170
|
+
continue;
|
|
30171
|
+
}
|
|
30172
|
+
throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
|
|
30173
|
+
}
|
|
30174
|
+
let completed = false;
|
|
30175
|
+
for await (const data of this.streamData(opened.response)) {
|
|
30176
|
+
const delta = this.streamDelta(config.provider, data);
|
|
30177
|
+
if (delta.completed) {
|
|
30178
|
+
completed = true;
|
|
30179
|
+
break;
|
|
30180
|
+
}
|
|
30181
|
+
if (delta.text === void 0) continue;
|
|
30182
|
+
yielded = true;
|
|
30183
|
+
yield delta.text;
|
|
30184
|
+
}
|
|
30185
|
+
opened.cleanup();
|
|
30186
|
+
opened = null;
|
|
30187
|
+
if (completed) return;
|
|
30188
|
+
throw new AiParseError("AI provider stream ended before [DONE]");
|
|
30189
|
+
} catch (error) {
|
|
30190
|
+
opened?.cleanup();
|
|
30191
|
+
const failure = this.streamError(error);
|
|
30192
|
+
if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || yielded || attempt >= config.maxRetries) throw failure;
|
|
30193
|
+
}
|
|
30194
|
+
}
|
|
30195
|
+
}
|
|
30196
|
+
};
|
|
30197
|
+
}
|
|
30198
|
+
});
|
|
30199
|
+
|
|
30274
30200
|
// ../core/src/queueBackends/rabbitmqBackend.ts
|
|
30275
30201
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
30276
30202
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
@@ -31974,6 +31900,12 @@ __export(src_exports2, {
|
|
|
31974
31900
|
APPLICATION_JSON: () => APPLICATION_JSON,
|
|
31975
31901
|
APPLICATION_OCTET: () => APPLICATION_OCTET,
|
|
31976
31902
|
APPLICATION_XML: () => APPLICATION_XML,
|
|
31903
|
+
Ai: () => Ai,
|
|
31904
|
+
AiConfigError: () => AiConfigError,
|
|
31905
|
+
AiError: () => AiError,
|
|
31906
|
+
AiHTTPError: () => AiHTTPError,
|
|
31907
|
+
AiParseError: () => AiParseError,
|
|
31908
|
+
AiTimeoutError: () => AiTimeoutError,
|
|
31977
31909
|
Api: () => Api,
|
|
31978
31910
|
Auth: () => Auth,
|
|
31979
31911
|
CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
|
|
@@ -32301,6 +32233,7 @@ var init_src2 = __esm({
|
|
|
32301
32233
|
init_htmlElement();
|
|
32302
32234
|
init_errorOverlay();
|
|
32303
32235
|
init_ai();
|
|
32236
|
+
init_aiClient();
|
|
32304
32237
|
init_liteBackend();
|
|
32305
32238
|
init_rabbitmqBackend();
|
|
32306
32239
|
init_kafkaBackend();
|
|
@@ -37739,7 +37672,7 @@ var init_database = __esm({
|
|
|
37739
37672
|
|
|
37740
37673
|
// src/model.ts
|
|
37741
37674
|
import { readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
|
|
37742
|
-
import { join as join29, extname as
|
|
37675
|
+
import { join as join29, extname as extname7 } from "node:path";
|
|
37743
37676
|
async function discoverModels(modelsDir) {
|
|
37744
37677
|
const models = [];
|
|
37745
37678
|
let files;
|
|
@@ -37752,7 +37685,7 @@ async function discoverModels(modelsDir) {
|
|
|
37752
37685
|
const filePath = join29(modelsDir, file);
|
|
37753
37686
|
const stat = statSync17(filePath);
|
|
37754
37687
|
if (!stat.isFile()) continue;
|
|
37755
|
-
const ext =
|
|
37688
|
+
const ext = extname7(file);
|
|
37756
37689
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
37757
37690
|
try {
|
|
37758
37691
|
const moduleUrl = `file://${filePath}?t=${Date.now()}`;
|
|
@@ -37791,7 +37724,7 @@ var init_model = __esm({
|
|
|
37791
37724
|
});
|
|
37792
37725
|
|
|
37793
37726
|
// src/migration.ts
|
|
37794
|
-
import { existsSync as existsSync26, readdirSync as readdirSync18, readFileSync as
|
|
37727
|
+
import { existsSync as existsSync26, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "node:fs";
|
|
37795
37728
|
import { join as join30, resolve as resolve18 } from "node:path";
|
|
37796
37729
|
function unwrapAdapter(db) {
|
|
37797
37730
|
let cur = db;
|
|
@@ -38119,7 +38052,7 @@ async function rollback(migrationsDir, delimiter2) {
|
|
|
38119
38052
|
`Cannot rollback ${migration.migration_name}: no .down.sql file found`
|
|
38120
38053
|
);
|
|
38121
38054
|
}
|
|
38122
|
-
const sqlContent =
|
|
38055
|
+
const sqlContent = readFileSync25(downPath, "utf-8").trim();
|
|
38123
38056
|
if (sqlContent) {
|
|
38124
38057
|
const statements = splitStatements(sqlContent, delim);
|
|
38125
38058
|
try {
|
|
@@ -38317,7 +38250,7 @@ async function migrate(adapter, options) {
|
|
|
38317
38250
|
result.skipped.push(file);
|
|
38318
38251
|
continue;
|
|
38319
38252
|
}
|
|
38320
|
-
const sqlContent =
|
|
38253
|
+
const sqlContent = readFileSync25(join30(dir, file), "utf-8").trim();
|
|
38321
38254
|
if (!sqlContent) {
|
|
38322
38255
|
result.skipped.push(file);
|
|
38323
38256
|
continue;
|
|
@@ -42008,7 +41941,7 @@ var init_attachment = __esm({
|
|
|
42008
41941
|
|
|
42009
41942
|
// src/realtime/storage.ts
|
|
42010
41943
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
42011
|
-
import { mkdirSync as mkdirSync20, readFileSync as
|
|
41944
|
+
import { mkdirSync as mkdirSync20, readFileSync as readFileSync26, writeFileSync as writeFileSync17, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
|
|
42012
41945
|
import { resolve as resolve19, sep as sep5 } from "node:path";
|
|
42013
41946
|
import { createRequire as createRequire8 } from "node:module";
|
|
42014
41947
|
function storageKey(filename = "") {
|
|
@@ -42061,7 +41994,7 @@ var init_storage = __esm({
|
|
|
42061
41994
|
}
|
|
42062
41995
|
get(key) {
|
|
42063
41996
|
try {
|
|
42064
|
-
return
|
|
41997
|
+
return readFileSync26(this.pathFor(key));
|
|
42065
41998
|
} catch {
|
|
42066
41999
|
return null;
|
|
42067
42000
|
}
|