tina4-nodejs 3.13.99 → 3.13.101
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 +1 -1
- package/packages/cli/dist/bin.js +585 -692
- package/packages/cli/src/bin.ts +0 -6
- package/packages/core/dist/index.js +573 -550
- package/packages/core/src/ai.ts +15 -6
- 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 +79 -631
- package/packages/frond/dist/index.js +118 -36
- package/packages/frond/src/engine.ts +195 -45
- package/packages/orm/dist/index.js +574 -557
- package/types/core/src/ai.d.ts +29 -0
- 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 +50 -8
- package/packages/cli/src/commands/metrics.ts +0 -160
- package/types/cli/src/commands/metrics.d.ts +0 -6
|
@@ -1460,7 +1460,10 @@ var init_trustedProxy = __esm({
|
|
|
1460
1460
|
var engine_exports = {};
|
|
1461
1461
|
__export(engine_exports, {
|
|
1462
1462
|
Frond: () => Frond,
|
|
1463
|
+
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
1463
1464
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
1465
|
+
filterChainCache: () => filterChainCache,
|
|
1466
|
+
pathParseCache: () => pathParseCache,
|
|
1464
1467
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
1465
1468
|
});
|
|
1466
1469
|
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -1564,6 +1567,12 @@ function capCache(cache, maxEntries) {
|
|
|
1564
1567
|
if (--drop <= 0) break;
|
|
1565
1568
|
}
|
|
1566
1569
|
}
|
|
1570
|
+
function sweepExpiredCache(cache) {
|
|
1571
|
+
const now = Date.now();
|
|
1572
|
+
for (const [key, [, expiresAt]] of cache) {
|
|
1573
|
+
if (expiresAt <= now) cache.delete(key);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1567
1576
|
function tokenize(source) {
|
|
1568
1577
|
const rawBlocks = [];
|
|
1569
1578
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -1627,6 +1636,16 @@ function stripTag(raw) {
|
|
|
1627
1636
|
}
|
|
1628
1637
|
return [inner.trim(), stripBefore, stripAfter];
|
|
1629
1638
|
}
|
|
1639
|
+
function extendsTarget(source) {
|
|
1640
|
+
const matches = source.match(EXTENDS_RE_GLOBAL);
|
|
1641
|
+
if (matches && matches.length > 1) {
|
|
1642
|
+
throw new Error(
|
|
1643
|
+
`Frond: template has ${matches.length} "{% extends %}" tags -- a template can extend only one parent`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
const match = source.match(EXTENDS_RE);
|
|
1647
|
+
return match ? match[1] : "";
|
|
1648
|
+
}
|
|
1630
1649
|
function resolveVar(expr, context) {
|
|
1631
1650
|
expr = expr.trim();
|
|
1632
1651
|
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
@@ -1707,6 +1726,7 @@ function resolveVar(expr, context) {
|
|
|
1707
1726
|
fromBracket.push(false);
|
|
1708
1727
|
}
|
|
1709
1728
|
}
|
|
1729
|
+
capCache(pathParseCache, MEMO_CACHE_MAX);
|
|
1710
1730
|
pathParseCache.set(expr, [parts, fromBracket]);
|
|
1711
1731
|
}
|
|
1712
1732
|
let value = context;
|
|
@@ -2294,6 +2314,7 @@ function parseFilterChain(expr) {
|
|
|
2294
2314
|
}
|
|
2295
2315
|
}
|
|
2296
2316
|
const result = [variable, filters];
|
|
2317
|
+
capCache(filterChainCache, MEMO_CACHE_MAX);
|
|
2297
2318
|
filterChainCache.set(expr, result);
|
|
2298
2319
|
return result;
|
|
2299
2320
|
}
|
|
@@ -2473,7 +2494,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
2473
2494
|
function _generateFormTokenValue(descriptor = "") {
|
|
2474
2495
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
2475
2496
|
}
|
|
2476
|
-
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, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
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;
|
|
2477
2498
|
var init_engine = __esm({
|
|
2478
2499
|
"../frond/src/engine.ts"() {
|
|
2479
2500
|
"use strict";
|
|
@@ -2567,9 +2588,12 @@ var init_engine = __esm({
|
|
|
2567
2588
|
LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
|
|
2568
2589
|
LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
|
|
2569
2590
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
2591
|
+
EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
|
|
2592
|
+
EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
|
|
2570
2593
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
2571
2594
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
2572
2595
|
TEMPLATE_CACHE_MAX = 256;
|
|
2596
|
+
MEMO_CACHE_MAX = 1024;
|
|
2573
2597
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
2574
2598
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
2575
2599
|
VarRef = class {
|
|
@@ -2922,29 +2946,22 @@ var init_engine = __esm({
|
|
|
2922
2946
|
return this;
|
|
2923
2947
|
}
|
|
2924
2948
|
/**
|
|
2925
|
-
* Register a custom filter
|
|
2926
|
-
*
|
|
2927
|
-
* the live instance's local filter map also receives the addition
|
|
2928
|
-
* immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
|
|
2949
|
+
* Register a custom filter on this instance only. Use the static method
|
|
2950
|
+
* for process-global registration. tina4: ADR-0052.
|
|
2929
2951
|
*/
|
|
2930
2952
|
addFilter(name, fn) {
|
|
2931
|
-
_Frond.classFilters.set(name, fn);
|
|
2932
2953
|
this.filters[name] = fn;
|
|
2933
2954
|
}
|
|
2934
2955
|
/**
|
|
2935
|
-
* Register a global variable
|
|
2936
|
-
* at class level — see ``addFilter`` for the dual-call semantics.
|
|
2956
|
+
* Register a global variable on this instance only.
|
|
2937
2957
|
*/
|
|
2938
2958
|
addGlobal(name, value) {
|
|
2939
|
-
_Frond.classGlobals.set(name, value);
|
|
2940
2959
|
this.globals[name] = value;
|
|
2941
2960
|
}
|
|
2942
2961
|
/**
|
|
2943
|
-
* Register a custom test
|
|
2944
|
-
* ``addFilter`` for the dual-call semantics.
|
|
2962
|
+
* Register a custom test on this instance only.
|
|
2945
2963
|
*/
|
|
2946
2964
|
addTest(name, fn) {
|
|
2947
|
-
_Frond.classTests.set(name, fn);
|
|
2948
2965
|
this.tests[name] = fn;
|
|
2949
2966
|
}
|
|
2950
2967
|
/**
|
|
@@ -3059,9 +3076,8 @@ var init_engine = __esm({
|
|
|
3059
3076
|
if (Object.keys(this.tests).length > 0) {
|
|
3060
3077
|
context.__frond_tests__ = this.tests;
|
|
3061
3078
|
}
|
|
3062
|
-
const
|
|
3063
|
-
if (
|
|
3064
|
-
const parentName = extendsMatch[1];
|
|
3079
|
+
const parentName = extendsTarget(source);
|
|
3080
|
+
if (parentName) {
|
|
3065
3081
|
const parentSource = this.load(parentName);
|
|
3066
3082
|
const childBlocks = this.extractBlocks(source);
|
|
3067
3083
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -3072,9 +3088,8 @@ var init_engine = __esm({
|
|
|
3072
3088
|
if (Object.keys(this.tests).length > 0) {
|
|
3073
3089
|
context.__frond_tests__ = this.tests;
|
|
3074
3090
|
}
|
|
3075
|
-
const
|
|
3076
|
-
if (
|
|
3077
|
-
const parentName = extendsMatch[1];
|
|
3091
|
+
const parentName = extendsTarget(source);
|
|
3092
|
+
if (parentName) {
|
|
3078
3093
|
const parentSource = this.load(parentName);
|
|
3079
3094
|
const childBlocks = this.extractBlocks(source);
|
|
3080
3095
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -3119,10 +3134,93 @@ var init_engine = __esm({
|
|
|
3119
3134
|
}
|
|
3120
3135
|
return blocks;
|
|
3121
3136
|
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Depth-aware block substitution against `source` (typically the
|
|
3139
|
+
* fully-resolved root template).
|
|
3140
|
+
*
|
|
3141
|
+
* A single regex `.replace()` pass (the flat `pattern` this replaces in
|
|
3142
|
+
* renderWithBlocks) pairs an OUTER block's open tag with the FIRST
|
|
3143
|
+
* `{% endblock %}` found -- which, when the outer block wraps a NESTED
|
|
3144
|
+
* `{% block %}`, is the nested block's own close tag, not the outer's.
|
|
3145
|
+
* That silently truncates the outer block's captured content and drops
|
|
3146
|
+
* everything after the inner endblock (the root-nested-block
|
|
3147
|
+
* content-loss bug). This scans with an open/close depth counter
|
|
3148
|
+
* instead (mirroring extractBlocks), so an outer block always captures
|
|
3149
|
+
* its FULL body, nested child blocks included.
|
|
3150
|
+
*
|
|
3151
|
+
* The content chosen for each block -- the child override in `blocks`
|
|
3152
|
+
* if present, else the block's own default body -- is then recursively
|
|
3153
|
+
* substituted against the SAME `blocks` map before being tokenized and
|
|
3154
|
+
* rendered, so a block nested inside another block resolves correctly
|
|
3155
|
+
* regardless of which template in the inheritance chain declared the
|
|
3156
|
+
* nesting (the root, an intermediate, however many levels deep).
|
|
3157
|
+
*
|
|
3158
|
+
* `{{ parent() }}` / `{{ super() }}` inside a block still render that
|
|
3159
|
+
* block's OWN default content at this level (lazy, on first call).
|
|
3160
|
+
*/
|
|
3161
|
+
substituteBlocks(source, blocks, context) {
|
|
3162
|
+
const blockOpen = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}/g;
|
|
3163
|
+
const blockClose = /\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
3164
|
+
const engine = this;
|
|
3165
|
+
const pieces = [];
|
|
3166
|
+
let pos = 0;
|
|
3167
|
+
while (pos < source.length) {
|
|
3168
|
+
blockOpen.lastIndex = pos;
|
|
3169
|
+
const mOpen = blockOpen.exec(source);
|
|
3170
|
+
if (!mOpen) {
|
|
3171
|
+
pieces.push(source.slice(pos));
|
|
3172
|
+
break;
|
|
3173
|
+
}
|
|
3174
|
+
pieces.push(source.slice(pos, mOpen.index));
|
|
3175
|
+
const name = mOpen[1];
|
|
3176
|
+
const contentStart = mOpen.index + mOpen[0].length;
|
|
3177
|
+
let depth = 1;
|
|
3178
|
+
let scan = contentStart;
|
|
3179
|
+
let closeMatch = null;
|
|
3180
|
+
while (depth > 0 && scan < source.length) {
|
|
3181
|
+
blockOpen.lastIndex = scan;
|
|
3182
|
+
blockClose.lastIndex = scan;
|
|
3183
|
+
const nextOpen = blockOpen.exec(source);
|
|
3184
|
+
const nextClose = blockClose.exec(source);
|
|
3185
|
+
if (!nextClose) break;
|
|
3186
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
3187
|
+
depth++;
|
|
3188
|
+
scan = nextOpen.index + nextOpen[0].length;
|
|
3189
|
+
} else {
|
|
3190
|
+
depth--;
|
|
3191
|
+
if (depth === 0) {
|
|
3192
|
+
closeMatch = nextClose;
|
|
3193
|
+
} else {
|
|
3194
|
+
scan = nextClose.index + nextClose[0].length;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
if (!closeMatch) {
|
|
3199
|
+
pieces.push(source.slice(mOpen.index));
|
|
3200
|
+
pos = source.length;
|
|
3201
|
+
break;
|
|
3202
|
+
}
|
|
3203
|
+
const parentContent = source.slice(contentStart, closeMatch.index);
|
|
3204
|
+
const blockSource = blocks[name] ?? parentContent;
|
|
3205
|
+
const resolvedSource = engine.substituteBlocks(blockSource, blocks, context);
|
|
3206
|
+
let renderedParent = null;
|
|
3207
|
+
const getParent = () => {
|
|
3208
|
+
if (renderedParent === null) {
|
|
3209
|
+
renderedParent = new SafeString(
|
|
3210
|
+
engine.renderTokens(tokenize(parentContent), context)
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
return renderedParent;
|
|
3214
|
+
};
|
|
3215
|
+
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
3216
|
+
pieces.push(engine.renderTokens(tokenize(resolvedSource), blockCtx));
|
|
3217
|
+
pos = closeMatch.index + closeMatch[0].length;
|
|
3218
|
+
}
|
|
3219
|
+
return pieces.join("");
|
|
3220
|
+
}
|
|
3122
3221
|
renderWithBlocks(parentSource, context, childBlocks) {
|
|
3123
|
-
const
|
|
3124
|
-
if (
|
|
3125
|
-
const grandparentName = extendsMatch[1];
|
|
3222
|
+
const grandparentName = extendsTarget(parentSource);
|
|
3223
|
+
if (grandparentName) {
|
|
3126
3224
|
const grandparentSource = this.load(grandparentName);
|
|
3127
3225
|
const parentBlocks = this.extractBlocks(parentSource);
|
|
3128
3226
|
const mergedBlocks = { ...parentBlocks, ...childBlocks };
|
|
@@ -3142,22 +3240,7 @@ var init_engine = __esm({
|
|
|
3142
3240
|
}
|
|
3143
3241
|
return this.renderWithBlocks(grandparentSource, context, mergedBlocks);
|
|
3144
3242
|
}
|
|
3145
|
-
const
|
|
3146
|
-
const engine = this;
|
|
3147
|
-
const result = parentSource.replace(pattern, (_match, name, parentContent) => {
|
|
3148
|
-
const blockSource = childBlocks[name] ?? parentContent;
|
|
3149
|
-
let renderedParent = null;
|
|
3150
|
-
const getParent = () => {
|
|
3151
|
-
if (renderedParent === null) {
|
|
3152
|
-
renderedParent = new SafeString(
|
|
3153
|
-
engine.renderTokens(tokenize(parentContent), context)
|
|
3154
|
-
);
|
|
3155
|
-
}
|
|
3156
|
-
return renderedParent;
|
|
3157
|
-
};
|
|
3158
|
-
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
3159
|
-
return this.renderTokens(tokenize(blockSource), blockCtx);
|
|
3160
|
-
});
|
|
3243
|
+
const result = this.substituteBlocks(parentSource, childBlocks, context);
|
|
3161
3244
|
return this.renderTokens(tokenize(result), context);
|
|
3162
3245
|
}
|
|
3163
3246
|
renderTokens(tokens, context) {
|
|
@@ -3980,6 +4063,7 @@ var init_engine = __esm({
|
|
|
3980
4063
|
const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
|
|
3981
4064
|
const cacheKey = m ? m[1] : "default";
|
|
3982
4065
|
const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
|
|
4066
|
+
sweepExpiredCache(this.fragmentCache);
|
|
3983
4067
|
const cached = this.fragmentCache.get(cacheKey);
|
|
3984
4068
|
if (cached) {
|
|
3985
4069
|
const [htmlContent, expiresAt] = cached;
|
|
@@ -4027,6 +4111,7 @@ var init_engine = __esm({
|
|
|
4027
4111
|
i++;
|
|
4028
4112
|
}
|
|
4029
4113
|
const rendered = this.renderTokens([...bodyTokens], context);
|
|
4114
|
+
capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
|
|
4030
4115
|
this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
|
|
4031
4116
|
return [rendered, i];
|
|
4032
4117
|
}
|
|
@@ -19481,14 +19566,14 @@ async function discoverRoutes(routesDir) {
|
|
|
19481
19566
|
const currentMtime = statSync7(filePath).mtimeMs;
|
|
19482
19567
|
if (_seenFiles.has(filePath) && _seenMtimes.get(filePath) === currentMtime) continue;
|
|
19483
19568
|
const method = name.toUpperCase();
|
|
19484
|
-
const
|
|
19485
|
-
const pattern = filePathToPattern(
|
|
19569
|
+
const relativePath2 = relative(routesDir, filePath);
|
|
19570
|
+
const pattern = filePathToPattern(relativePath2);
|
|
19486
19571
|
try {
|
|
19487
19572
|
const moduleUrl = `file://${filePath}?t=${currentMtime}`;
|
|
19488
19573
|
const mod = await import(moduleUrl);
|
|
19489
19574
|
const handler = mod.default ?? mod.handler;
|
|
19490
19575
|
if (typeof handler !== "function") {
|
|
19491
|
-
console.warn(` Warning: ${
|
|
19576
|
+
console.warn(` Warning: ${relativePath2} does not export a handler function, skipping`);
|
|
19492
19577
|
continue;
|
|
19493
19578
|
}
|
|
19494
19579
|
const meta = mod.meta;
|
|
@@ -19500,7 +19585,7 @@ async function discoverRoutes(routesDir) {
|
|
|
19500
19585
|
_seenMtimes.set(filePath, currentMtime);
|
|
19501
19586
|
registeredFromThisScan++;
|
|
19502
19587
|
} catch (err) {
|
|
19503
|
-
console.error(` Error loading route ${
|
|
19588
|
+
console.error(` Error loading route ${relativePath2}:`, err);
|
|
19504
19589
|
recordBrokenImport(filePath, err);
|
|
19505
19590
|
}
|
|
19506
19591
|
}
|
|
@@ -19534,8 +19619,8 @@ function recordBrokenImport(filePath, error) {
|
|
|
19534
19619
|
} catch {
|
|
19535
19620
|
}
|
|
19536
19621
|
}
|
|
19537
|
-
function filePathToPattern(
|
|
19538
|
-
const parts =
|
|
19622
|
+
function filePathToPattern(relativePath2) {
|
|
19623
|
+
const parts = relativePath2.replace(/\\/g, "/").split("/").slice(0, -1);
|
|
19539
19624
|
const urlParts = parts.map((part) => {
|
|
19540
19625
|
if (part.startsWith("[...") && part.endsWith("]")) {
|
|
19541
19626
|
const name = part.slice(4, -1);
|
|
@@ -22772,489 +22857,127 @@ import * as fs3 from "node:fs";
|
|
|
22772
22857
|
import * as path2 from "node:path";
|
|
22773
22858
|
import { spawnSync } from "node:child_process";
|
|
22774
22859
|
import { fileURLToPath } from "node:url";
|
|
22775
|
-
function
|
|
22776
|
-
|
|
22777
|
-
|
|
22778
|
-
|
|
22779
|
-
|
|
22780
|
-
|
|
22781
|
-
if (entry.isDirectory()) {
|
|
22782
|
-
if (!exclude.includes(entry.name)) {
|
|
22783
|
-
results.push(...walkFiles(fullPath, extensions, exclude));
|
|
22784
|
-
}
|
|
22785
|
-
} else if (entry.isFile()) {
|
|
22786
|
-
const ext = path2.extname(entry.name);
|
|
22787
|
-
if (extensions.includes(ext) && !entry.name.endsWith(".d.ts")) {
|
|
22788
|
-
results.push(fullPath);
|
|
22789
|
-
}
|
|
22790
|
-
}
|
|
22791
|
-
}
|
|
22792
|
-
return results;
|
|
22793
|
-
}
|
|
22794
|
-
function readFileSafe(filePath) {
|
|
22795
|
-
try {
|
|
22796
|
-
return fs3.readFileSync(filePath, "utf-8");
|
|
22797
|
-
} catch {
|
|
22798
|
-
return null;
|
|
22799
|
-
}
|
|
22800
|
-
}
|
|
22801
|
-
function relativePath(filePath, root = ".") {
|
|
22802
|
-
return path2.relative(root, filePath);
|
|
22803
|
-
}
|
|
22804
|
-
function countLines(source) {
|
|
22805
|
-
const lines = source.split("\n");
|
|
22806
|
-
let loc = 0;
|
|
22807
|
-
let blank = 0;
|
|
22808
|
-
let comment = 0;
|
|
22809
|
-
let inBlockComment = false;
|
|
22810
|
-
for (const line of lines) {
|
|
22811
|
-
const stripped = line.trim();
|
|
22812
|
-
if (!stripped) {
|
|
22813
|
-
blank++;
|
|
22814
|
-
continue;
|
|
22815
|
-
}
|
|
22816
|
-
if (inBlockComment) {
|
|
22817
|
-
comment++;
|
|
22818
|
-
if (stripped.includes("*/")) {
|
|
22819
|
-
inBlockComment = false;
|
|
22820
|
-
}
|
|
22821
|
-
continue;
|
|
22822
|
-
}
|
|
22823
|
-
if (stripped.startsWith("/*")) {
|
|
22824
|
-
comment++;
|
|
22825
|
-
if (!stripped.includes("*/") || stripped.endsWith("/*")) {
|
|
22826
|
-
inBlockComment = true;
|
|
22827
|
-
}
|
|
22828
|
-
continue;
|
|
22829
|
-
}
|
|
22830
|
-
if (stripped.startsWith("//")) {
|
|
22831
|
-
comment++;
|
|
22832
|
-
continue;
|
|
22833
|
-
}
|
|
22834
|
-
loc++;
|
|
22835
|
-
}
|
|
22836
|
-
return { loc, blank, comment };
|
|
22837
|
-
}
|
|
22838
|
-
function stripLiterals(source) {
|
|
22839
|
-
const out = [];
|
|
22840
|
-
const n = source.length;
|
|
22841
|
-
let i = 0;
|
|
22842
|
-
let prevSignificant = "";
|
|
22843
|
-
let prevWord = "";
|
|
22844
|
-
const regexKeywords = /* @__PURE__ */ new Set([
|
|
22845
|
-
"return",
|
|
22846
|
-
"typeof",
|
|
22847
|
-
"instanceof",
|
|
22848
|
-
"in",
|
|
22849
|
-
"of",
|
|
22850
|
-
"new",
|
|
22851
|
-
"delete",
|
|
22852
|
-
"void",
|
|
22853
|
-
"throw",
|
|
22854
|
-
"case",
|
|
22855
|
-
"do",
|
|
22856
|
-
"else",
|
|
22857
|
-
"yield",
|
|
22858
|
-
"await"
|
|
22859
|
-
]);
|
|
22860
|
-
function prevEndsExpression() {
|
|
22861
|
-
if (prevSignificant === "") return false;
|
|
22862
|
-
if (/[A-Za-z0-9_$]/.test(prevSignificant)) {
|
|
22863
|
-
return !regexKeywords.has(prevWord);
|
|
22864
|
-
}
|
|
22865
|
-
if (prevSignificant === ")" || prevSignificant === "]") return true;
|
|
22866
|
-
if (prevSignificant === ".") return true;
|
|
22867
|
-
return false;
|
|
22868
|
-
}
|
|
22869
|
-
while (i < n) {
|
|
22870
|
-
const ch = source[i];
|
|
22871
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
22872
|
-
if (ch === "/" && next === "/") {
|
|
22873
|
-
out.push("//");
|
|
22874
|
-
i += 2;
|
|
22875
|
-
while (i < n && source[i] !== "\n") {
|
|
22876
|
-
out.push(" ");
|
|
22877
|
-
i++;
|
|
22878
|
-
}
|
|
22879
|
-
continue;
|
|
22880
|
-
}
|
|
22881
|
-
if (ch === "/" && next === "*") {
|
|
22882
|
-
out.push("/*");
|
|
22883
|
-
i += 2;
|
|
22884
|
-
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) {
|
|
22885
|
-
out.push(source[i] === "\n" ? "\n" : " ");
|
|
22886
|
-
i++;
|
|
22887
|
-
}
|
|
22888
|
-
if (i < n) {
|
|
22889
|
-
out.push("*/");
|
|
22890
|
-
i += 2;
|
|
22891
|
-
}
|
|
22892
|
-
continue;
|
|
22893
|
-
}
|
|
22894
|
-
if (ch === '"' || ch === "'") {
|
|
22895
|
-
const quote = ch;
|
|
22896
|
-
out.push(quote);
|
|
22897
|
-
i++;
|
|
22898
|
-
while (i < n && source[i] !== quote) {
|
|
22899
|
-
if (source[i] === "\\" && i + 1 < n) {
|
|
22900
|
-
out.push(" ");
|
|
22901
|
-
i += 2;
|
|
22902
|
-
continue;
|
|
22903
|
-
}
|
|
22904
|
-
if (source[i] === "\n") {
|
|
22905
|
-
out.push("\n");
|
|
22906
|
-
i++;
|
|
22907
|
-
break;
|
|
22908
|
-
}
|
|
22909
|
-
out.push(" ");
|
|
22910
|
-
i++;
|
|
22911
|
-
}
|
|
22912
|
-
if (i < n && source[i] === quote) {
|
|
22913
|
-
out.push(quote);
|
|
22914
|
-
i++;
|
|
22915
|
-
}
|
|
22916
|
-
prevSignificant = quote;
|
|
22917
|
-
prevWord = "";
|
|
22918
|
-
continue;
|
|
22919
|
-
}
|
|
22920
|
-
if (ch === "`") {
|
|
22921
|
-
out.push("`");
|
|
22922
|
-
i++;
|
|
22923
|
-
while (i < n && source[i] !== "`") {
|
|
22924
|
-
if (source[i] === "\\" && i + 1 < n) {
|
|
22925
|
-
out.push(source[i + 1] === "\n" ? " \n" : " ");
|
|
22926
|
-
i += 2;
|
|
22927
|
-
continue;
|
|
22928
|
-
}
|
|
22929
|
-
if (source[i] === "$" && source[i + 1] === "{") {
|
|
22930
|
-
out.push("${");
|
|
22931
|
-
i += 2;
|
|
22932
|
-
let depth = 1;
|
|
22933
|
-
const exprStart = i;
|
|
22934
|
-
while (i < n && depth > 0) {
|
|
22935
|
-
if (source[i] === "{") depth++;
|
|
22936
|
-
else if (source[i] === "}") depth--;
|
|
22937
|
-
if (depth === 0) break;
|
|
22938
|
-
i++;
|
|
22939
|
-
}
|
|
22940
|
-
out.push(stripLiterals(source.slice(exprStart, i)));
|
|
22941
|
-
if (i < n && source[i] === "}") {
|
|
22942
|
-
out.push("}");
|
|
22943
|
-
i++;
|
|
22944
|
-
}
|
|
22945
|
-
continue;
|
|
22946
|
-
}
|
|
22947
|
-
out.push(source[i] === "\n" ? "\n" : " ");
|
|
22948
|
-
i++;
|
|
22949
|
-
}
|
|
22950
|
-
if (i < n && source[i] === "`") {
|
|
22951
|
-
out.push("`");
|
|
22952
|
-
i++;
|
|
22953
|
-
}
|
|
22954
|
-
prevSignificant = "`";
|
|
22955
|
-
prevWord = "";
|
|
22956
|
-
continue;
|
|
22957
|
-
}
|
|
22958
|
-
if (ch === "/" && !prevEndsExpression()) {
|
|
22959
|
-
let j = i + 1;
|
|
22960
|
-
let ok = false;
|
|
22961
|
-
let inClass = false;
|
|
22962
|
-
while (j < n) {
|
|
22963
|
-
const c = source[j];
|
|
22964
|
-
if (c === "\\") {
|
|
22965
|
-
j += 2;
|
|
22966
|
-
continue;
|
|
22967
|
-
}
|
|
22968
|
-
if (c === "\n") break;
|
|
22969
|
-
if (c === "[") inClass = true;
|
|
22970
|
-
else if (c === "]") inClass = false;
|
|
22971
|
-
else if (c === "/" && !inClass) {
|
|
22972
|
-
ok = true;
|
|
22973
|
-
break;
|
|
22974
|
-
}
|
|
22975
|
-
j++;
|
|
22976
|
-
}
|
|
22977
|
-
if (ok) {
|
|
22978
|
-
out.push("/");
|
|
22979
|
-
for (let k = i + 1; k < j; k++) out.push(" ");
|
|
22980
|
-
out.push("/");
|
|
22981
|
-
i = j + 1;
|
|
22982
|
-
while (i < n && /[a-z]/i.test(source[i])) {
|
|
22983
|
-
out.push(source[i]);
|
|
22984
|
-
i++;
|
|
22985
|
-
}
|
|
22986
|
-
prevSignificant = "/";
|
|
22987
|
-
prevWord = "";
|
|
22988
|
-
continue;
|
|
22989
|
-
}
|
|
22990
|
-
}
|
|
22991
|
-
out.push(ch);
|
|
22992
|
-
if (!/\s/.test(ch)) {
|
|
22993
|
-
prevSignificant = ch;
|
|
22994
|
-
if (/[A-Za-z0-9_$]/.test(ch)) {
|
|
22995
|
-
prevWord = /[A-Za-z0-9_$]/.test(source[i - 1] ?? "") ? prevWord + ch : ch;
|
|
22996
|
-
} else {
|
|
22997
|
-
prevWord = "";
|
|
22998
|
-
}
|
|
22999
|
-
}
|
|
23000
|
-
i++;
|
|
22860
|
+
function containsTypeScript(directory) {
|
|
22861
|
+
if (!fs3.existsSync(directory) || !fs3.statSync(directory).isDirectory()) return false;
|
|
22862
|
+
for (const entry of fs3.readdirSync(directory, { withFileTypes: true })) {
|
|
22863
|
+
if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
|
|
22864
|
+
const target = path2.join(directory, entry.name);
|
|
22865
|
+
if (entry.isDirectory() ? containsTypeScript(target) : /\.[cm]?[jt]sx?$/.test(entry.name)) return true;
|
|
23001
22866
|
}
|
|
23002
|
-
return
|
|
23003
|
-
}
|
|
23004
|
-
function countClassesQuick(source) {
|
|
23005
|
-
const matches = source.match(
|
|
23006
|
-
/(?:^|\n)\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/g
|
|
23007
|
-
);
|
|
23008
|
-
return matches ? matches.length : 0;
|
|
23009
|
-
}
|
|
23010
|
-
function countFunctionsQuick(source) {
|
|
23011
|
-
const clean = stripLiterals(source);
|
|
23012
|
-
let count = 0;
|
|
23013
|
-
const funcDecls = clean.match(
|
|
23014
|
-
/(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(/g
|
|
23015
|
-
);
|
|
23016
|
-
if (funcDecls) count += funcDecls.length;
|
|
23017
|
-
const methods = clean.match(
|
|
23018
|
-
/(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*\([^)]*\)\s*(?::\s*\S+)?\s*\{/g
|
|
23019
|
-
);
|
|
23020
|
-
if (methods) count += methods.length;
|
|
23021
|
-
const arrows = clean.match(
|
|
23022
|
-
/(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\(/g
|
|
23023
|
-
);
|
|
23024
|
-
if (arrows) count += arrows.length;
|
|
23025
|
-
return count;
|
|
23026
|
-
}
|
|
23027
|
-
function resolveRoot(root = "src") {
|
|
23028
|
-
const rootPath = path2.resolve(root);
|
|
23029
|
-
if (fs3.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
|
|
23030
|
-
_lastScanRoot = rootPath;
|
|
23031
|
-
return root;
|
|
23032
|
-
}
|
|
23033
|
-
const fwDir = path2.resolve(path2.dirname(new URL(import.meta.url).pathname));
|
|
23034
|
-
_lastScanRoot = fwDir;
|
|
23035
|
-
return fwDir;
|
|
23036
|
-
}
|
|
23037
|
-
function quickMetrics(root = "src") {
|
|
23038
|
-
root = resolveRoot(root);
|
|
23039
|
-
const rootPath = path2.resolve(root);
|
|
23040
|
-
if (!fs3.existsSync(rootPath)) {
|
|
23041
|
-
return { error: `Directory not found: ${root}` };
|
|
23042
|
-
}
|
|
23043
|
-
const tsFiles = walkFiles(rootPath, [".ts", ".js"]);
|
|
23044
|
-
const twigFiles = walkFiles(rootPath, [".twig", ".html"]);
|
|
23045
|
-
const migrationsDir = path2.resolve("migrations");
|
|
23046
|
-
const migrationFiles = [
|
|
23047
|
-
...walkFiles(migrationsDir, [".sql"]),
|
|
23048
|
-
...walkFiles(migrationsDir, [".ts"])
|
|
23049
|
-
];
|
|
23050
|
-
const scssFiles = walkFiles(rootPath, [".scss", ".css"]);
|
|
23051
|
-
let totalLoc = 0;
|
|
23052
|
-
let totalBlank = 0;
|
|
23053
|
-
let totalComment = 0;
|
|
23054
|
-
let totalClasses = 0;
|
|
23055
|
-
let totalFunctions = 0;
|
|
23056
|
-
const fileDetails = [];
|
|
23057
|
-
for (const f of tsFiles) {
|
|
23058
|
-
const source = readFileSafe(f);
|
|
23059
|
-
if (source === null) continue;
|
|
23060
|
-
const counts = countLines(source);
|
|
23061
|
-
const classes = countClassesQuick(source);
|
|
23062
|
-
const functions = countFunctionsQuick(source);
|
|
23063
|
-
totalLoc += counts.loc;
|
|
23064
|
-
totalBlank += counts.blank;
|
|
23065
|
-
totalComment += counts.comment;
|
|
23066
|
-
totalClasses += classes;
|
|
23067
|
-
totalFunctions += functions;
|
|
23068
|
-
fileDetails.push({
|
|
23069
|
-
path: relativePath(f, rootPath),
|
|
23070
|
-
loc: counts.loc,
|
|
23071
|
-
blank: counts.blank,
|
|
23072
|
-
comment: counts.comment,
|
|
23073
|
-
classes,
|
|
23074
|
-
functions
|
|
23075
|
-
});
|
|
23076
|
-
}
|
|
23077
|
-
fileDetails.sort((a, b) => b.loc - a.loc);
|
|
23078
|
-
let routeCount = 0;
|
|
23079
|
-
let ormCount = 0;
|
|
23080
|
-
for (const f of tsFiles) {
|
|
23081
|
-
const source = readFileSafe(f);
|
|
23082
|
-
if (source === null) continue;
|
|
23083
|
-
const routes = source.match(
|
|
23084
|
-
/(?:router\s*\.\s*(?:get|post|put|delete|patch|any)\s*\(|@(?:get|post|put|delete|patch)\s*\()/g
|
|
23085
|
-
);
|
|
23086
|
-
if (routes) routeCount += routes.length;
|
|
23087
|
-
const orms = source.match(
|
|
23088
|
-
/class\s+\w+\s+extends\s+(?:ORM|Model)\b/g
|
|
23089
|
-
);
|
|
23090
|
-
if (orms) ormCount += orms.length;
|
|
23091
|
-
}
|
|
23092
|
-
const breakdown = {
|
|
23093
|
-
typescript: tsFiles.filter((f) => f.endsWith(".ts")).length,
|
|
23094
|
-
javascript: tsFiles.filter((f) => f.endsWith(".js")).length,
|
|
23095
|
-
templates: twigFiles.length,
|
|
23096
|
-
migrations: migrationFiles.length,
|
|
23097
|
-
stylesheets: scssFiles.length
|
|
23098
|
-
};
|
|
23099
|
-
return {
|
|
23100
|
-
file_count: tsFiles.length,
|
|
23101
|
-
total_loc: totalLoc,
|
|
23102
|
-
total_blank: totalBlank,
|
|
23103
|
-
total_comment: totalComment,
|
|
23104
|
-
lloc: totalLoc,
|
|
23105
|
-
classes: totalClasses,
|
|
23106
|
-
functions: totalFunctions,
|
|
23107
|
-
route_count: routeCount,
|
|
23108
|
-
orm_count: ormCount,
|
|
23109
|
-
template_count: twigFiles.length,
|
|
23110
|
-
migration_count: migrationFiles.length,
|
|
23111
|
-
avg_file_size: tsFiles.length > 0 ? Math.round(totalLoc / tsFiles.length * 10) / 10 : 0,
|
|
23112
|
-
largest_files: fileDetails.slice(0, 10),
|
|
23113
|
-
breakdown
|
|
23114
|
-
};
|
|
22867
|
+
return false;
|
|
23115
22868
|
}
|
|
23116
|
-
function
|
|
23117
|
-
const resolved =
|
|
23118
|
-
const
|
|
23119
|
-
|
|
23120
|
-
|
|
23121
|
-
return [resolved, scanningFramework ? "framework" : "project"];
|
|
22869
|
+
function resolveTarget(root = "src") {
|
|
22870
|
+
const resolved = containsTypeScript(root) ? path2.resolve(root) : path2.dirname(fileURLToPath(import.meta.url));
|
|
22871
|
+
const mode = containsTypeScript(root) ? "project" : "framework";
|
|
22872
|
+
lastScanRoot = resolved;
|
|
22873
|
+
return [resolved, mode];
|
|
23122
22874
|
}
|
|
23123
22875
|
function enginePath() {
|
|
23124
|
-
const names = process.platform === "win32" ? ["tina4.exe", "tina4
|
|
23125
|
-
for (const
|
|
23126
|
-
if (!dir) continue;
|
|
22876
|
+
const names = process.platform === "win32" ? ["tina4.exe", "tina4"] : ["tina4"];
|
|
22877
|
+
for (const directory of (process.env.PATH || "").split(path2.delimiter)) {
|
|
23127
22878
|
for (const name of names) {
|
|
23128
|
-
const candidate = path2.join(
|
|
22879
|
+
const candidate = path2.join(directory, name);
|
|
23129
22880
|
try {
|
|
23130
|
-
if (!fs3.statSync(candidate).isFile()) continue;
|
|
23131
22881
|
fs3.accessSync(candidate, fs3.constants.X_OK);
|
|
22882
|
+
if (!fs3.statSync(candidate).isFile()) continue;
|
|
22883
|
+
const descriptor = fs3.openSync(candidate, "r");
|
|
22884
|
+
const header = Buffer.alloc(2);
|
|
22885
|
+
fs3.readSync(descriptor, header, 0, 2, 0);
|
|
22886
|
+
fs3.closeSync(descriptor);
|
|
22887
|
+
if (header.toString("latin1") !== "#!") return candidate;
|
|
23132
22888
|
} catch {
|
|
23133
22889
|
continue;
|
|
23134
22890
|
}
|
|
23135
|
-
try {
|
|
23136
|
-
const fd = fs3.openSync(candidate, "r");
|
|
23137
|
-
const buf = Buffer.alloc(2);
|
|
23138
|
-
fs3.readSync(fd, buf, 0, 2, 0);
|
|
23139
|
-
fs3.closeSync(fd);
|
|
23140
|
-
if (buf.toString("latin1") === "#!") continue;
|
|
23141
|
-
} catch {
|
|
23142
|
-
}
|
|
23143
|
-
return candidate;
|
|
23144
22891
|
}
|
|
23145
22892
|
}
|
|
23146
22893
|
return null;
|
|
23147
22894
|
}
|
|
23148
22895
|
function runEngine(target) {
|
|
23149
22896
|
const binary = enginePath();
|
|
23150
|
-
if (binary
|
|
23151
|
-
|
|
23152
|
-
}
|
|
23153
|
-
const proc = spawnSync(binary, ["metrics", "--path", target, "--json"], {
|
|
22897
|
+
if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
|
|
22898
|
+
const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
|
|
23154
22899
|
encoding: "utf8",
|
|
23155
|
-
timeout:
|
|
22900
|
+
timeout: 6e4,
|
|
23156
22901
|
maxBuffer: 64 * 1024 * 1024
|
|
23157
22902
|
});
|
|
23158
|
-
if (
|
|
23159
|
-
|
|
23160
|
-
if (err.code === "ETIMEDOUT") {
|
|
23161
|
-
throw new MetricsEngineError(`tina4 metrics timed out after ${TIMEOUT_MS / 1e3}s on ${target}`);
|
|
23162
|
-
}
|
|
23163
|
-
throw new MetricsEngineError(`could not run ${binary}: ${err.message}`);
|
|
22903
|
+
if (processResult.error) {
|
|
22904
|
+
throw new MetricsEngineError(`could not run ${binary}: ${processResult.error.message}`);
|
|
23164
22905
|
}
|
|
23165
|
-
if (
|
|
23166
|
-
const detail = (
|
|
23167
|
-
throw new MetricsEngineError(
|
|
23168
|
-
`tina4 metrics failed on ${target}: ${detail || `exit code ${proc.status}`}`
|
|
23169
|
-
);
|
|
23170
|
-
}
|
|
23171
|
-
if (!proc.stdout || !proc.stdout.trim()) {
|
|
23172
|
-
throw new MetricsEngineError(`tina4 metrics produced no output for ${target}`);
|
|
22906
|
+
if (processResult.status !== 0) {
|
|
22907
|
+
const detail = (processResult.stderr || processResult.stdout || "").trim().split("\n")[0];
|
|
22908
|
+
throw new MetricsEngineError(`tina4 metrics failed on ${target}: ${detail || processResult.status}`);
|
|
23173
22909
|
}
|
|
23174
|
-
let payload;
|
|
23175
22910
|
try {
|
|
23176
|
-
payload = JSON.parse(
|
|
23177
|
-
|
|
23178
|
-
|
|
23179
|
-
|
|
23180
|
-
|
|
23181
|
-
|
|
22911
|
+
const payload = JSON.parse(processResult.stdout);
|
|
22912
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
22913
|
+
throw new Error("non-object payload");
|
|
22914
|
+
}
|
|
22915
|
+
return payload;
|
|
22916
|
+
} catch (error) {
|
|
22917
|
+
throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${error.message}`);
|
|
23182
22918
|
}
|
|
23183
|
-
return payload;
|
|
23184
22919
|
}
|
|
23185
|
-
function
|
|
23186
|
-
|
|
23187
|
-
|
|
23188
|
-
if (!ok) {
|
|
23189
|
-
throw new MetricsEngineError(
|
|
23190
|
-
`engine payload has no usable '${key}' - the installed tina4 CLI predates a field the dashboard renders. Update it: ${INSTALL_HINT}`
|
|
23191
|
-
);
|
|
22920
|
+
function requireArray(payload, key) {
|
|
22921
|
+
if (!Array.isArray(payload[key])) {
|
|
22922
|
+
throw new MetricsEngineError(`engine payload has no usable '${key}' - ${INSTALL_HINT}`);
|
|
23192
22923
|
}
|
|
23193
|
-
return
|
|
22924
|
+
return payload[key];
|
|
23194
22925
|
}
|
|
23195
22926
|
function fullAnalysis(root = "src") {
|
|
23196
|
-
const [resolved, scanMode] =
|
|
22927
|
+
const [resolved, scanMode] = resolveTarget(root);
|
|
23197
22928
|
const payload = runEngine(resolved);
|
|
23198
|
-
const summary =
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
|
|
23203
|
-
|
|
23204
|
-
|
|
23205
|
-
|
|
23206
|
-
|
|
23207
|
-
if (
|
|
23208
|
-
|
|
23209
|
-
|
|
23210
|
-
|
|
23211
|
-
if (functions.length) {
|
|
23212
|
-
const absent = FUNCTION_KEYS.filter((k) => !(k in functions[0]));
|
|
23213
|
-
if (absent.length) throw new MetricsEngineError(`engine function metrics are missing ${absent.join(", ")}`);
|
|
22929
|
+
const summary = payload.summary;
|
|
22930
|
+
if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
|
|
22931
|
+
throw new MetricsEngineError(`engine payload has no usable 'summary' - ${INSTALL_HINT}`);
|
|
22932
|
+
}
|
|
22933
|
+
const fileMetrics = requireArray(payload, "file_metrics");
|
|
22934
|
+
const functions = requireArray(payload, "most_complex_functions");
|
|
22935
|
+
const missingSummary = SUMMARY_KEYS.filter((key) => !(key in summary));
|
|
22936
|
+
if (missingSummary.length) throw new MetricsEngineError(`engine summary is missing ${missingSummary.join(", ")}`);
|
|
22937
|
+
const missingFile = fileMetrics.length ? FILE_KEYS.filter((key) => !(key in fileMetrics[0])) : [];
|
|
22938
|
+
if (missingFile.length) throw new MetricsEngineError(`engine file_metrics is missing ${missingFile.join(", ")}`);
|
|
22939
|
+
const missingFunction = functions.length ? FUNCTION_KEYS.filter((key) => !(key in functions[0])) : [];
|
|
22940
|
+
if (missingFunction.length) {
|
|
22941
|
+
throw new MetricsEngineError(`engine function metrics are missing ${missingFunction.join(", ")}`);
|
|
23214
22942
|
}
|
|
23215
|
-
|
|
23216
|
-
|
|
23217
|
-
|
|
23218
|
-
|
|
23219
|
-
|
|
23220
|
-
|
|
23221
|
-
|
|
23222
|
-
|
|
23223
|
-
|
|
22943
|
+
return {
|
|
22944
|
+
...Object.fromEntries(SUMMARY_KEYS.map((key) => [key, summary[key]])),
|
|
22945
|
+
file_metrics: fileMetrics,
|
|
22946
|
+
most_complex_functions: functions.slice(0, 15),
|
|
22947
|
+
dependency_graph: payload.dependency_graph || {},
|
|
22948
|
+
scan_mode: scanMode,
|
|
22949
|
+
scan_root: resolved,
|
|
22950
|
+
engine: "tina4-cli"
|
|
22951
|
+
};
|
|
23224
22952
|
}
|
|
23225
22953
|
function fileDetail(filePath) {
|
|
23226
22954
|
if (!filePath) throw new MetricsEngineError("fileDetail needs a path");
|
|
23227
22955
|
let target = filePath;
|
|
23228
|
-
if (!fs3.existsSync(target) &&
|
|
23229
|
-
const candidate = path2.join(_lastScanRoot, filePath);
|
|
23230
|
-
if (fs3.existsSync(candidate)) target = candidate;
|
|
23231
|
-
}
|
|
22956
|
+
if (!fs3.existsSync(target) && lastScanRoot) target = path2.join(lastScanRoot, filePath);
|
|
23232
22957
|
if (!fs3.existsSync(target)) throw new MetricsEngineError(`no such file: ${filePath}`);
|
|
23233
22958
|
if (fs3.statSync(target).isDirectory()) throw new MetricsEngineError(`not a file: ${filePath}`);
|
|
23234
22959
|
const payload = runEngine(target);
|
|
23235
|
-
const
|
|
23236
|
-
if (!
|
|
23237
|
-
|
|
23238
|
-
|
|
23239
|
-
|
|
22960
|
+
const files = requireArray(payload, "file_metrics");
|
|
22961
|
+
if (!files.length) throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
|
|
22962
|
+
return {
|
|
22963
|
+
...files[0],
|
|
22964
|
+
function_count: files[0].functions || 0,
|
|
22965
|
+
functions: requireArray(payload, "most_complex_functions"),
|
|
22966
|
+
engine: "tina4-cli"
|
|
22967
|
+
};
|
|
23240
22968
|
}
|
|
23241
|
-
var
|
|
22969
|
+
var lastScanRoot, MetricsEngineError, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
|
|
23242
22970
|
var init_metrics = __esm({
|
|
23243
22971
|
"src/metrics.ts"() {
|
|
23244
22972
|
"use strict";
|
|
23245
|
-
|
|
22973
|
+
lastScanRoot = "";
|
|
23246
22974
|
MetricsEngineError = class extends Error {
|
|
23247
22975
|
constructor(message) {
|
|
23248
22976
|
super(message);
|
|
23249
22977
|
this.name = "MetricsEngineError";
|
|
23250
22978
|
}
|
|
23251
22979
|
};
|
|
23252
|
-
|
|
23253
|
-
INSTALL_HINT = [
|
|
23254
|
-
"the tina4 CLI provides the metrics engine (ADR-0002). Install it with",
|
|
23255
|
-
" curl -fsSL https://tina4.com/install.sh | sh",
|
|
23256
|
-
"or see https://tina4.com/cli"
|
|
23257
|
-
].join("\n");
|
|
22980
|
+
INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
|
|
23258
22981
|
SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
|
|
23259
22982
|
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_tests"];
|
|
23260
22983
|
FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
|
|
@@ -23262,7 +22985,7 @@ var init_metrics = __esm({
|
|
|
23262
22985
|
});
|
|
23263
22986
|
|
|
23264
22987
|
// src/feedback.ts
|
|
23265
|
-
import { readFileSync as
|
|
22988
|
+
import { readFileSync as readFileSync12, existsSync as existsSync13 } from "node:fs";
|
|
23266
22989
|
import { dirname as dirname7, join as join18, resolve as resolve9 } from "node:path";
|
|
23267
22990
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
23268
22991
|
function feedbackEnabled() {
|
|
@@ -23403,7 +23126,7 @@ var init_feedback = __esm({
|
|
|
23403
23126
|
handleFeedbackWidgetJs = (_req, res) => {
|
|
23404
23127
|
let body;
|
|
23405
23128
|
if (existsSync13(WIDGET_BUNDLE_PATH)) {
|
|
23406
|
-
body =
|
|
23129
|
+
body = readFileSync12(WIDGET_BUNDLE_PATH);
|
|
23407
23130
|
} else {
|
|
23408
23131
|
body = "console.warn('tina4-feedback-widget bundle not built yet');";
|
|
23409
23132
|
}
|
|
@@ -23418,7 +23141,7 @@ var init_feedback = __esm({
|
|
|
23418
23141
|
});
|
|
23419
23142
|
|
|
23420
23143
|
// src/version.ts
|
|
23421
|
-
import { existsSync as existsSync14, readFileSync as
|
|
23144
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
|
|
23422
23145
|
import { dirname as dirname8, join as join19 } from "node:path";
|
|
23423
23146
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
23424
23147
|
function resolveFrameworkVersion() {
|
|
@@ -23427,7 +23150,7 @@ function resolveFrameworkVersion() {
|
|
|
23427
23150
|
const pkgPath = join19(dir, "package.json");
|
|
23428
23151
|
if (existsSync14(pkgPath)) {
|
|
23429
23152
|
try {
|
|
23430
|
-
const pkg = JSON.parse(
|
|
23153
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
23431
23154
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
23432
23155
|
} catch {
|
|
23433
23156
|
}
|
|
@@ -26215,8 +25938,8 @@ __export(context_exports, {
|
|
|
26215
25938
|
fts5Supported: () => fts5Supported
|
|
26216
25939
|
});
|
|
26217
25940
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
26218
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as
|
|
26219
|
-
import { basename as basename4, dirname as dirname10, extname as
|
|
25941
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync15, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
|
|
25942
|
+
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";
|
|
26220
25943
|
function fts5Supported() {
|
|
26221
25944
|
try {
|
|
26222
25945
|
const conn = new DatabaseSync4(":memory:");
|
|
@@ -26351,7 +26074,7 @@ var init_context = __esm({
|
|
|
26351
26074
|
}
|
|
26352
26075
|
// ── indexing ───────────────────────────────────────────────
|
|
26353
26076
|
static chunksFor(label, text) {
|
|
26354
|
-
const ext =
|
|
26077
|
+
const ext = extname5(label).toLowerCase();
|
|
26355
26078
|
const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
|
|
26356
26079
|
if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
|
|
26357
26080
|
return chunkCode(text, label);
|
|
@@ -26369,7 +26092,7 @@ var init_context = __esm({
|
|
|
26369
26092
|
const stored = label != null ? String(label) : String(file);
|
|
26370
26093
|
let text;
|
|
26371
26094
|
try {
|
|
26372
|
-
text =
|
|
26095
|
+
text = readFileSync15(file, "utf-8");
|
|
26373
26096
|
} catch {
|
|
26374
26097
|
return 0;
|
|
26375
26098
|
}
|
|
@@ -26390,7 +26113,7 @@ var init_context = __esm({
|
|
|
26390
26113
|
static eligible(filename) {
|
|
26391
26114
|
const fn = filename.toLowerCase();
|
|
26392
26115
|
if (fn.endsWith(".min.js")) return false;
|
|
26393
|
-
const ext =
|
|
26116
|
+
const ext = extname5(fn);
|
|
26394
26117
|
return CODE_EXTS.has(ext) || DOC_EXTS.has(ext) || CONFIG_EXTS.has(ext) || SPECIAL_FILES.has(fn);
|
|
26395
26118
|
}
|
|
26396
26119
|
/**
|
|
@@ -26419,7 +26142,7 @@ var init_context = __esm({
|
|
|
26419
26142
|
for (const fn of files) {
|
|
26420
26143
|
if (!_Context.eligible(fn)) continue;
|
|
26421
26144
|
const full = join21(dir, fn);
|
|
26422
|
-
const rel =
|
|
26145
|
+
const rel = relative3(rootAbs, full);
|
|
26423
26146
|
total += this.indexPath(full, rel);
|
|
26424
26147
|
}
|
|
26425
26148
|
for (const d of subdirs) walk2(join21(dir, d));
|
|
@@ -26440,7 +26163,7 @@ var init_context = __esm({
|
|
|
26440
26163
|
const raw = String(changedPath);
|
|
26441
26164
|
const abs = isAbsolute6(raw) ? raw : join21(process.cwd(), raw);
|
|
26442
26165
|
const resolved = realResolve(resolve11(abs));
|
|
26443
|
-
const rel =
|
|
26166
|
+
const rel = relative3(this.root, resolved);
|
|
26444
26167
|
if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) {
|
|
26445
26168
|
return -1;
|
|
26446
26169
|
}
|
|
@@ -28362,7 +28085,7 @@ var init_job = __esm({
|
|
|
28362
28085
|
});
|
|
28363
28086
|
|
|
28364
28087
|
// src/queueBackends/liteBackend.ts
|
|
28365
|
-
import { mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as
|
|
28088
|
+
import { mkdirSync as mkdirSync14, readdirSync as readdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync10, unlinkSync as unlinkSync7, existsSync as existsSync17 } from "node:fs";
|
|
28366
28089
|
import { join as join22 } from "node:path";
|
|
28367
28090
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
28368
28091
|
var LiteBackend;
|
|
@@ -28462,7 +28185,7 @@ var init_liteBackend = __esm({
|
|
|
28462
28185
|
const filePath = join22(dir, filename);
|
|
28463
28186
|
let job;
|
|
28464
28187
|
try {
|
|
28465
|
-
job = JSON.parse(
|
|
28188
|
+
job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28466
28189
|
} catch {
|
|
28467
28190
|
continue;
|
|
28468
28191
|
}
|
|
@@ -28526,7 +28249,7 @@ var init_liteBackend = __esm({
|
|
|
28526
28249
|
const filePath = join22(reservedDir, filename);
|
|
28527
28250
|
let record;
|
|
28528
28251
|
try {
|
|
28529
|
-
record = JSON.parse(
|
|
28252
|
+
record = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28530
28253
|
} catch {
|
|
28531
28254
|
continue;
|
|
28532
28255
|
}
|
|
@@ -28639,7 +28362,7 @@ var init_liteBackend = __esm({
|
|
|
28639
28362
|
let count = 0;
|
|
28640
28363
|
for (const file of files) {
|
|
28641
28364
|
try {
|
|
28642
|
-
const job = JSON.parse(
|
|
28365
|
+
const job = JSON.parse(readFileSync16(join22(scanDir, file), "utf-8"));
|
|
28643
28366
|
if (job.status === status2) count++;
|
|
28644
28367
|
} catch {
|
|
28645
28368
|
}
|
|
@@ -28696,7 +28419,7 @@ var init_liteBackend = __esm({
|
|
|
28696
28419
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
28697
28420
|
for (const file of files) {
|
|
28698
28421
|
try {
|
|
28699
|
-
const job = JSON.parse(
|
|
28422
|
+
const job = JSON.parse(readFileSync16(join22(dir, file), "utf-8"));
|
|
28700
28423
|
const attempts = job.attempts || 0;
|
|
28701
28424
|
if (attempts > 0 && attempts < maxRetries) {
|
|
28702
28425
|
results.push(job);
|
|
@@ -28722,7 +28445,7 @@ var init_liteBackend = __esm({
|
|
|
28722
28445
|
const failedDir = join22(this.basePath, q, "failed");
|
|
28723
28446
|
const filePath = join22(failedDir, `${jobId}.queue-data`);
|
|
28724
28447
|
if (existsSync17(filePath)) {
|
|
28725
|
-
const job = JSON.parse(
|
|
28448
|
+
const job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28726
28449
|
job.status = "pending";
|
|
28727
28450
|
job.attempts = (job.attempts || 0) + 1;
|
|
28728
28451
|
job.error = void 0;
|
|
@@ -28746,7 +28469,7 @@ var init_liteBackend = __esm({
|
|
|
28746
28469
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
28747
28470
|
for (const file of files) {
|
|
28748
28471
|
try {
|
|
28749
|
-
const job = JSON.parse(
|
|
28472
|
+
const job = JSON.parse(readFileSync16(join22(failedDir, file), "utf-8"));
|
|
28750
28473
|
if ((job.attempts || 0) >= maxRetries) {
|
|
28751
28474
|
job.status = "dead";
|
|
28752
28475
|
results.push(job);
|
|
@@ -28780,7 +28503,7 @@ var init_liteBackend = __esm({
|
|
|
28780
28503
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
|
|
28781
28504
|
for (const file of files) {
|
|
28782
28505
|
try {
|
|
28783
|
-
const job = JSON.parse(
|
|
28506
|
+
const job = JSON.parse(readFileSync16(join22(dir, file), "utf-8"));
|
|
28784
28507
|
if (job.status === status2) {
|
|
28785
28508
|
unlinkSync7(join22(dir, file));
|
|
28786
28509
|
count++;
|
|
@@ -28807,7 +28530,7 @@ var init_liteBackend = __esm({
|
|
|
28807
28530
|
for (const file of files) {
|
|
28808
28531
|
try {
|
|
28809
28532
|
const filePath = join22(failedDir, file);
|
|
28810
|
-
const job = JSON.parse(
|
|
28533
|
+
const job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28811
28534
|
if ((job.attempts || 0) >= maxRetries) {
|
|
28812
28535
|
continue;
|
|
28813
28536
|
}
|
|
@@ -28839,7 +28562,7 @@ var init_liteBackend = __esm({
|
|
|
28839
28562
|
const filePath = join22(dir, file);
|
|
28840
28563
|
let job;
|
|
28841
28564
|
try {
|
|
28842
|
-
job = JSON.parse(
|
|
28565
|
+
job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28843
28566
|
} catch {
|
|
28844
28567
|
continue;
|
|
28845
28568
|
}
|
|
@@ -30302,7 +30025,7 @@ function detectVersion(projectRoot3) {
|
|
|
30302
30025
|
}
|
|
30303
30026
|
return "0.0.0";
|
|
30304
30027
|
}
|
|
30305
|
-
function
|
|
30028
|
+
function relativePath(absPath, projectRoot3, frameworkRoots) {
|
|
30306
30029
|
const norm = path6.resolve(absPath);
|
|
30307
30030
|
for (const fw of frameworkRoots) {
|
|
30308
30031
|
const parent = path6.dirname(fw);
|
|
@@ -30767,7 +30490,7 @@ function buildEntriesForFile(absPath, source, fwRoots, projectRoot3, version, ou
|
|
|
30767
30490
|
} catch {
|
|
30768
30491
|
return;
|
|
30769
30492
|
}
|
|
30770
|
-
const rel =
|
|
30493
|
+
const rel = relativePath(absPath, projectRoot3, fwRoots);
|
|
30771
30494
|
for (const cls of parsed.classes) {
|
|
30772
30495
|
if (!cls.exported && source === "framework") {
|
|
30773
30496
|
continue;
|
|
@@ -31403,8 +31126,8 @@ ${end}
|
|
|
31403
31126
|
|
|
31404
31127
|
// src/devAdmin.ts
|
|
31405
31128
|
import { cpus as osCpus } from "node:os";
|
|
31406
|
-
import { readFileSync as
|
|
31407
|
-
import { join as join26, dirname as dirname12, resolve as resolve15, relative as
|
|
31129
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, existsSync as existsSync21, readdirSync as readdirSync15, mkdirSync as mkdirSync17, copyFileSync, statSync as statSync16 } from "node:fs";
|
|
31130
|
+
import { join as join26, dirname as dirname12, resolve as resolve15, relative as relative7 } from "node:path";
|
|
31408
31131
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
31409
31132
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
31410
31133
|
function escapeHtml(value) {
|
|
@@ -31523,7 +31246,7 @@ function readQueueDir(dir, topic, status2) {
|
|
|
31523
31246
|
for (const filename of readdirSync15(dir).sort()) {
|
|
31524
31247
|
if (!filename.endsWith(".queue-data")) continue;
|
|
31525
31248
|
try {
|
|
31526
|
-
jobs.push(mapQueueJob(JSON.parse(
|
|
31249
|
+
jobs.push(mapQueueJob(JSON.parse(readFileSync20(join26(dir, filename), "utf-8")), topic, status2));
|
|
31527
31250
|
} catch {
|
|
31528
31251
|
}
|
|
31529
31252
|
}
|
|
@@ -31642,7 +31365,7 @@ function resolveDevEnvVar(key) {
|
|
|
31642
31365
|
if (live !== void 0 && live !== "") return live;
|
|
31643
31366
|
const envPath = join26(process.cwd(), ".env");
|
|
31644
31367
|
if (!existsSync21(envPath)) return "";
|
|
31645
|
-
for (const line of
|
|
31368
|
+
for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
|
|
31646
31369
|
const t = line.trim();
|
|
31647
31370
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
31648
31371
|
const eq = t.indexOf("=");
|
|
@@ -31652,7 +31375,7 @@ function resolveDevEnvVar(key) {
|
|
|
31652
31375
|
}
|
|
31653
31376
|
function upsertDevEnvVar(key, value) {
|
|
31654
31377
|
const envPath = join26(process.cwd(), ".env");
|
|
31655
|
-
const lines = existsSync21(envPath) ?
|
|
31378
|
+
const lines = existsSync21(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
|
|
31656
31379
|
let found = false;
|
|
31657
31380
|
const out = [];
|
|
31658
31381
|
for (const line of lines) {
|
|
@@ -31685,7 +31408,7 @@ function parseEnvFile() {
|
|
|
31685
31408
|
const envPath = join26(process.cwd(), ".env");
|
|
31686
31409
|
const result = {};
|
|
31687
31410
|
if (!existsSync21(envPath)) return result;
|
|
31688
|
-
const lines =
|
|
31411
|
+
const lines = readFileSync20(envPath, "utf-8").split("\n");
|
|
31689
31412
|
for (const line of lines) {
|
|
31690
31413
|
const trimmed = line.trim();
|
|
31691
31414
|
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -31725,7 +31448,7 @@ function handleGalleryDeploy(router) {
|
|
|
31725
31448
|
const copied = [];
|
|
31726
31449
|
const allFiles = walkDirRecursive(gallerySrc);
|
|
31727
31450
|
for (const srcFile of allFiles) {
|
|
31728
|
-
const rel =
|
|
31451
|
+
const rel = relative7(gallerySrc, srcFile);
|
|
31729
31452
|
const dest = join26(projectSrc, rel);
|
|
31730
31453
|
mkdirSync17(dirname12(dest), { recursive: true });
|
|
31731
31454
|
copyFileSync(srcFile, dest);
|
|
@@ -32387,9 +32110,6 @@ var init_devAdmin = __esm({
|
|
|
32387
32110
|
{ method: "GET", pattern: "/__dev/api/gallery", handler: handleGalleryList },
|
|
32388
32111
|
{ method: "POST", pattern: "/__dev/api/gallery/deploy", handler: handleGalleryDeploy(router) },
|
|
32389
32112
|
// Metrics
|
|
32390
|
-
{ method: "GET", pattern: "/__dev/api/metrics", handler: (_req, res) => {
|
|
32391
|
-
res.json(quickMetrics());
|
|
32392
|
-
} },
|
|
32393
32113
|
// No fallback (ADR-0002): a missing or stale CLI is a 503 naming the
|
|
32394
32114
|
// install command, never zeros that read as a healthy codebase.
|
|
32395
32115
|
{ method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req, res) => {
|
|
@@ -33198,7 +32918,7 @@ var init_devAdmin = __esm({
|
|
|
33198
32918
|
}
|
|
33199
32919
|
try {
|
|
33200
32920
|
const envPath = join26(process.cwd(), ".env");
|
|
33201
|
-
const lines = existsSync21(envPath) ?
|
|
32921
|
+
const lines = existsSync21(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
|
|
33202
32922
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
33203
32923
|
const newLines = [];
|
|
33204
32924
|
for (const line of lines) {
|
|
@@ -33244,12 +32964,12 @@ var init_devAdmin = __esm({
|
|
|
33244
32964
|
const metaFile = join26(entryPath, "meta.json");
|
|
33245
32965
|
if (statSync16(entryPath).isDirectory() && existsSync21(metaFile)) {
|
|
33246
32966
|
try {
|
|
33247
|
-
const meta = JSON.parse(
|
|
32967
|
+
const meta = JSON.parse(readFileSync20(metaFile, "utf-8"));
|
|
33248
32968
|
meta.id = entry;
|
|
33249
32969
|
const srcDir = join26(entryPath, "src");
|
|
33250
32970
|
if (existsSync21(srcDir)) {
|
|
33251
32971
|
const allFiles = walkDirRecursive(srcDir);
|
|
33252
|
-
meta.files = allFiles.map((f) =>
|
|
32972
|
+
meta.files = allFiles.map((f) => relative7(srcDir, f));
|
|
33253
32973
|
}
|
|
33254
32974
|
const projectSrc = resolve15(process.cwd(), "src");
|
|
33255
32975
|
if (existsSync21(srcDir) && meta.files) {
|
|
@@ -33367,7 +33087,7 @@ var init_devAdmin = __esm({
|
|
|
33367
33087
|
for (const name of readdirSync15(target).sort()) {
|
|
33368
33088
|
if (devFilesHidden(name)) continue;
|
|
33369
33089
|
const full = join26(target, name);
|
|
33370
|
-
const entryRel =
|
|
33090
|
+
const entryRel = relative7(root, full).replace(/\\/g, "/");
|
|
33371
33091
|
if (isSecretPath(entryRel)) continue;
|
|
33372
33092
|
let isDir = false;
|
|
33373
33093
|
let size = null;
|
|
@@ -33412,7 +33132,7 @@ var init_devAdmin = __esm({
|
|
|
33412
33132
|
size
|
|
33413
33133
|
});
|
|
33414
33134
|
}
|
|
33415
|
-
res.json({ path:
|
|
33135
|
+
res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
|
|
33416
33136
|
};
|
|
33417
33137
|
DEV_ADMIN_LANG_MAP = {
|
|
33418
33138
|
".py": "python",
|
|
@@ -33464,8 +33184,8 @@ var init_devAdmin = __esm({
|
|
|
33464
33184
|
return;
|
|
33465
33185
|
}
|
|
33466
33186
|
try {
|
|
33467
|
-
const content =
|
|
33468
|
-
const path8 =
|
|
33187
|
+
const content = readFileSync20(target, "utf-8");
|
|
33188
|
+
const path8 = relative7(root, target);
|
|
33469
33189
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
33470
33190
|
} catch (e) {
|
|
33471
33191
|
res.json({ error: e.message }, 500);
|
|
@@ -33487,10 +33207,10 @@ var init_devAdmin = __esm({
|
|
|
33487
33207
|
writeFileSync14(target, content, "utf-8");
|
|
33488
33208
|
try {
|
|
33489
33209
|
const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
33490
|
-
Plan2.recordAction(existed ? "patched" : "created",
|
|
33210
|
+
Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
|
|
33491
33211
|
} catch {
|
|
33492
33212
|
}
|
|
33493
|
-
res.json({ ok: true, path:
|
|
33213
|
+
res.json({ ok: true, path: relative7(root, target), bytes: Buffer.byteLength(content, "utf-8") });
|
|
33494
33214
|
} catch (e) {
|
|
33495
33215
|
res.json({ error: e.message }, 500);
|
|
33496
33216
|
}
|
|
@@ -33510,7 +33230,7 @@ var init_devAdmin = __esm({
|
|
|
33510
33230
|
return;
|
|
33511
33231
|
}
|
|
33512
33232
|
try {
|
|
33513
|
-
const buf =
|
|
33233
|
+
const buf = readFileSync20(target);
|
|
33514
33234
|
const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
33515
33235
|
const mime = {
|
|
33516
33236
|
js: "application/javascript",
|
|
@@ -33552,7 +33272,7 @@ var init_devAdmin = __esm({
|
|
|
33552
33272
|
const { renameSync: renameSync3 } = await import("node:fs");
|
|
33553
33273
|
mkdirSync17(dirname12(dst), { recursive: true });
|
|
33554
33274
|
renameSync3(src, dst);
|
|
33555
|
-
res.json({ ok: true, from:
|
|
33275
|
+
res.json({ ok: true, from: relative7(root, src), to: relative7(root, dst) });
|
|
33556
33276
|
} catch (e) {
|
|
33557
33277
|
res.json({ error: e.message }, 500);
|
|
33558
33278
|
}
|
|
@@ -33573,7 +33293,7 @@ var init_devAdmin = __esm({
|
|
|
33573
33293
|
try {
|
|
33574
33294
|
const { rmSync } = await import("node:fs");
|
|
33575
33295
|
rmSync(target, { recursive: true, force: true });
|
|
33576
|
-
res.json({ ok: true, deleted:
|
|
33296
|
+
res.json({ ok: true, deleted: relative7(root, target) });
|
|
33577
33297
|
} catch (e) {
|
|
33578
33298
|
res.json({ error: e.message }, 500);
|
|
33579
33299
|
}
|
|
@@ -33907,7 +33627,7 @@ var init_devAdmin = __esm({
|
|
|
33907
33627
|
});
|
|
33908
33628
|
};
|
|
33909
33629
|
handleDevAdminJs = async (_req, res) => {
|
|
33910
|
-
const { readFileSync:
|
|
33630
|
+
const { readFileSync: readFileSync27, existsSync: existsSync27 } = await import("node:fs");
|
|
33911
33631
|
const { dirname: dirname15, join: join32, resolve: resolve20 } = await import("node:path");
|
|
33912
33632
|
const { fileURLToPath: fileURLToPath8 } = await import("node:url");
|
|
33913
33633
|
const dir = dirname15(fileURLToPath8(import.meta.url));
|
|
@@ -33923,7 +33643,7 @@ var init_devAdmin = __esm({
|
|
|
33923
33643
|
for (const jsPath of candidates) {
|
|
33924
33644
|
if (existsSync27(jsPath)) {
|
|
33925
33645
|
try {
|
|
33926
|
-
const content =
|
|
33646
|
+
const content = readFileSync27(jsPath, "utf-8");
|
|
33927
33647
|
res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
33928
33648
|
res.raw.end(content);
|
|
33929
33649
|
return;
|
|
@@ -33938,7 +33658,7 @@ var init_devAdmin = __esm({
|
|
|
33938
33658
|
});
|
|
33939
33659
|
|
|
33940
33660
|
// src/i18n.ts
|
|
33941
|
-
import { readFileSync as
|
|
33661
|
+
import { readFileSync as readFileSync21, readdirSync as readdirSync16, existsSync as existsSync22 } from "node:fs";
|
|
33942
33662
|
import { join as join27, resolve as resolve16 } from "node:path";
|
|
33943
33663
|
var I18n;
|
|
33944
33664
|
var init_i18n = __esm({
|
|
@@ -34035,7 +33755,7 @@ var init_i18n = __esm({
|
|
|
34035
33755
|
const filePath = join27(this._localeDir, `${locale}.json`);
|
|
34036
33756
|
if (existsSync22(filePath)) {
|
|
34037
33757
|
try {
|
|
34038
|
-
const raw =
|
|
33758
|
+
const raw = readFileSync21(filePath, "utf-8");
|
|
34039
33759
|
const data = JSON.parse(raw);
|
|
34040
33760
|
this._translations.set(locale, _I18n._flatten(data));
|
|
34041
33761
|
return;
|
|
@@ -34048,7 +33768,7 @@ var init_i18n = __esm({
|
|
|
34048
33768
|
const yamlPath = join27(this._localeDir, `${locale}${ext}`);
|
|
34049
33769
|
if (existsSync22(yamlPath)) {
|
|
34050
33770
|
try {
|
|
34051
|
-
const raw =
|
|
33771
|
+
const raw = readFileSync21(yamlPath, "utf-8");
|
|
34052
33772
|
const data = _I18n._parseSimpleYaml(raw);
|
|
34053
33773
|
this._translations.set(locale, _I18n._flatten(data));
|
|
34054
33774
|
return;
|
|
@@ -34911,8 +34631,8 @@ var init_docsAutoDiscovery = __esm({
|
|
|
34911
34631
|
// src/server.ts
|
|
34912
34632
|
import { createServer as createServer2 } from "node:http";
|
|
34913
34633
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
34914
|
-
import { resolve as resolve18, dirname as dirname13, join as join29, relative as
|
|
34915
|
-
import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as
|
|
34634
|
+
import { resolve as resolve18, dirname as dirname13, join as join29, relative as relative8 } from "node:path";
|
|
34635
|
+
import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
|
|
34916
34636
|
import { isatty } from "node:tty";
|
|
34917
34637
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
34918
34638
|
import { execFileSync as execFileSync3, exec } from "node:child_process";
|
|
@@ -35136,7 +34856,7 @@ function getGalleryDeployedState() {
|
|
|
35136
34856
|
if (existsSync24(srcDir)) {
|
|
35137
34857
|
const files = walkGalleryFiles(srcDir);
|
|
35138
34858
|
const projectSrc = resolve18(process.cwd(), "src");
|
|
35139
|
-
state[entry] = files.every((f) => existsSync24(join29(projectSrc,
|
|
34859
|
+
state[entry] = files.every((f) => existsSync24(join29(projectSrc, relative8(srcDir, f))));
|
|
35140
34860
|
} else {
|
|
35141
34861
|
state[entry] = false;
|
|
35142
34862
|
}
|
|
@@ -35573,7 +35293,7 @@ function serveTemplateFallback(ctx) {
|
|
|
35573
35293
|
if ((ctx.req.method ?? "GET") !== "GET") return false;
|
|
35574
35294
|
const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
|
|
35575
35295
|
if (!tplFile) return false;
|
|
35576
|
-
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) :
|
|
35296
|
+
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync23(resolve18(ctx.templatesDir, tplFile), "utf-8");
|
|
35577
35297
|
ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
35578
35298
|
ctx.res.raw.end(html);
|
|
35579
35299
|
return true;
|
|
@@ -36400,7 +36120,7 @@ var init_mqttMessage = __esm({
|
|
|
36400
36120
|
import net2 from "node:net";
|
|
36401
36121
|
import tls from "node:tls";
|
|
36402
36122
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
36403
|
-
import { existsSync as existsSync25, readFileSync as
|
|
36123
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
|
|
36404
36124
|
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;
|
|
36405
36125
|
var init_mqtt = __esm({
|
|
36406
36126
|
"src/mqtt.ts"() {
|
|
@@ -36865,7 +36585,7 @@ var init_mqtt = __esm({
|
|
|
36865
36585
|
servername: this.host,
|
|
36866
36586
|
rejectUnauthorized: this.tlsVerify
|
|
36867
36587
|
};
|
|
36868
|
-
if (this.tlsVerify && this.caFile) opts.ca =
|
|
36588
|
+
if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
|
|
36869
36589
|
sock = tls.connect(opts, () => settle(() => resolve20(sock)));
|
|
36870
36590
|
} else {
|
|
36871
36591
|
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
|
|
@@ -37086,7 +36806,7 @@ var init_mqtt = __esm({
|
|
|
37086
36806
|
|
|
37087
36807
|
// src/service.ts
|
|
37088
36808
|
import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
|
|
37089
|
-
import { join as join30, extname as
|
|
36809
|
+
import { join as join30, extname as extname7 } from "node:path";
|
|
37090
36810
|
import { pathToFileURL } from "node:url";
|
|
37091
36811
|
function matchCronField(field, value) {
|
|
37092
36812
|
if (field === "*") return true;
|
|
@@ -37257,7 +36977,7 @@ var init_service = __esm({
|
|
|
37257
36977
|
return discovered;
|
|
37258
36978
|
}
|
|
37259
36979
|
for (const entry of entries) {
|
|
37260
|
-
const ext =
|
|
36980
|
+
const ext = extname7(entry);
|
|
37261
36981
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
37262
36982
|
const fullPath = join30(dir, entry);
|
|
37263
36983
|
const stat = statSync18(fullPath);
|
|
@@ -37373,7 +37093,7 @@ var init_service = __esm({
|
|
|
37373
37093
|
return;
|
|
37374
37094
|
}
|
|
37375
37095
|
for (const entry of entries) {
|
|
37376
|
-
const ext =
|
|
37096
|
+
const ext = extname7(entry);
|
|
37377
37097
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
37378
37098
|
const fullPath = join30(dir, entry);
|
|
37379
37099
|
if (watchedFiles.has(fullPath)) continue;
|
|
@@ -38076,7 +37796,7 @@ var init_api = __esm({
|
|
|
38076
37796
|
// src/messenger.ts
|
|
38077
37797
|
import net3 from "node:net";
|
|
38078
37798
|
import tls2 from "node:tls";
|
|
38079
|
-
import { readFileSync as
|
|
37799
|
+
import { readFileSync as readFileSync25 } from "node:fs";
|
|
38080
37800
|
import { basename as basename6 } from "node:path";
|
|
38081
37801
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
38082
37802
|
function tlsRejectUnauthorized() {
|
|
@@ -38173,7 +37893,7 @@ function buildMimeMessage(options) {
|
|
|
38173
37893
|
}
|
|
38174
37894
|
for (const filePath of options.attachments) {
|
|
38175
37895
|
const fileName = basename6(filePath);
|
|
38176
|
-
const fileData =
|
|
37896
|
+
const fileData = readFileSync25(filePath);
|
|
38177
37897
|
const base64Data = fileData.toString("base64");
|
|
38178
37898
|
lines.push("");
|
|
38179
37899
|
lines.push(`--${boundary}`);
|
|
@@ -39636,9 +39356,9 @@ var init_htmlElement = __esm({
|
|
|
39636
39356
|
});
|
|
39637
39357
|
|
|
39638
39358
|
// src/ai.ts
|
|
39639
|
-
import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync17, readFileSync as
|
|
39359
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync20, writeFileSync as writeFileSync17, readFileSync as readFileSync26 } from "node:fs";
|
|
39640
39360
|
import { homedir } from "node:os";
|
|
39641
|
-
import { join as join31, resolve as resolve19, relative as
|
|
39361
|
+
import { join as join31, resolve as resolve19, relative as relative9, dirname as dirname14 } from "node:path";
|
|
39642
39362
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
39643
39363
|
import { execSync, execFileSync as execFileSync4 } from "node:child_process";
|
|
39644
39364
|
import { createInterface } from "node:readline";
|
|
@@ -39646,7 +39366,7 @@ function readVersion() {
|
|
|
39646
39366
|
try {
|
|
39647
39367
|
const thisDir = dirname14(fileURLToPath7(import.meta.url));
|
|
39648
39368
|
const rootPkg = resolve19(thisDir, "..", "..", "..", "package.json");
|
|
39649
|
-
const pkg = JSON.parse(
|
|
39369
|
+
const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
|
|
39650
39370
|
return pkg.version ?? "0.0.0";
|
|
39651
39371
|
} catch {
|
|
39652
39372
|
return "0.0.0";
|
|
@@ -39664,14 +39384,16 @@ function downloadSkillsSync(jobs) {
|
|
|
39664
39384
|
const jobs = JSON.parse(process.argv[1]);
|
|
39665
39385
|
const fs = require("node:fs");
|
|
39666
39386
|
const path = require("node:path");
|
|
39387
|
+
const transientStatuses = new Set([429, 500, 502, 503, 504]);
|
|
39667
39388
|
async function fetchOne(job) {
|
|
39668
39389
|
const resp = await fetch(job.url, { signal: AbortSignal.timeout(15000) });
|
|
39669
|
-
if (!resp.ok)
|
|
39390
|
+
if (!resp.ok) return { ok: false, retry: transientStatuses.has(resp.status) };
|
|
39670
39391
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
39671
39392
|
for (const dest of job.dests) {
|
|
39672
39393
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
39673
39394
|
fs.writeFileSync(dest, buf);
|
|
39674
39395
|
}
|
|
39396
|
+
return { ok: true, retry: false };
|
|
39675
39397
|
}
|
|
39676
39398
|
(async () => {
|
|
39677
39399
|
const ok = [];
|
|
@@ -39680,9 +39402,11 @@ function downloadSkillsSync(jobs) {
|
|
|
39680
39402
|
const failed = [];
|
|
39681
39403
|
await Promise.all(pending.map(async (job) => {
|
|
39682
39404
|
try {
|
|
39683
|
-
await fetchOne(job);
|
|
39684
|
-
ok.push(job.url);
|
|
39405
|
+
const result = await fetchOne(job);
|
|
39406
|
+
if (result.ok) ok.push(job.url);
|
|
39407
|
+
else if (result.retry) failed.push(job);
|
|
39685
39408
|
} catch {
|
|
39409
|
+
// DNS, TLS, timeout and connection failures are transient.
|
|
39686
39410
|
failed.push(job);
|
|
39687
39411
|
}
|
|
39688
39412
|
}));
|
|
@@ -39865,7 +39589,7 @@ function writeOrMerge(contextPath, contextFile, frameworkGuide) {
|
|
|
39865
39589
|
writeFileSync17(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
39866
39590
|
return "Installed";
|
|
39867
39591
|
}
|
|
39868
|
-
const existing =
|
|
39592
|
+
const existing = readFileSync26(contextPath, "utf-8");
|
|
39869
39593
|
if (hasMarkers(existing, start2, end)) {
|
|
39870
39594
|
writeFileSync17(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
|
|
39871
39595
|
return "Refreshed skill block in";
|
|
@@ -39889,7 +39613,7 @@ function installForTool(root, tool, context) {
|
|
|
39889
39613
|
const parentDir = dirname14(contextPath);
|
|
39890
39614
|
mkdirSync20(parentDir, { recursive: true });
|
|
39891
39615
|
const action = writeOrMerge(contextPath, tool.contextFile, context);
|
|
39892
|
-
const rel =
|
|
39616
|
+
const rel = relative9(root, contextPath);
|
|
39893
39617
|
created.push(rel);
|
|
39894
39618
|
console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
|
|
39895
39619
|
if (tool.name === "claude-code") {
|
|
@@ -40267,7 +39991,7 @@ function generateClaudeCodeContext() {
|
|
|
40267
39991
|
const repoRoot = resolve19(thisDir, "..", "..", "..");
|
|
40268
39992
|
const claudeMdPath = join31(repoRoot, "CLAUDE.md");
|
|
40269
39993
|
if (existsSync26(claudeMdPath)) {
|
|
40270
|
-
return
|
|
39994
|
+
return readFileSync26(claudeMdPath, "utf-8");
|
|
40271
39995
|
}
|
|
40272
39996
|
} catch {
|
|
40273
39997
|
}
|
|
@@ -40461,6 +40185,292 @@ export default class User {
|
|
|
40461
40185
|
}
|
|
40462
40186
|
});
|
|
40463
40187
|
|
|
40188
|
+
// src/aiClient.ts
|
|
40189
|
+
import http2 from "node:http";
|
|
40190
|
+
import https2 from "node:https";
|
|
40191
|
+
var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai;
|
|
40192
|
+
var init_aiClient = __esm({
|
|
40193
|
+
"src/aiClient.ts"() {
|
|
40194
|
+
"use strict";
|
|
40195
|
+
AiError = class extends Error {
|
|
40196
|
+
};
|
|
40197
|
+
AiConfigError = class extends AiError {
|
|
40198
|
+
};
|
|
40199
|
+
AiTimeoutError = class extends AiError {
|
|
40200
|
+
};
|
|
40201
|
+
AiParseError = class extends AiError {
|
|
40202
|
+
};
|
|
40203
|
+
AiHTTPError = class extends AiError {
|
|
40204
|
+
constructor(message, status2 = null) {
|
|
40205
|
+
super(message);
|
|
40206
|
+
this.status = status2;
|
|
40207
|
+
}
|
|
40208
|
+
};
|
|
40209
|
+
Ai = class {
|
|
40210
|
+
static chat(messages, options = {}) {
|
|
40211
|
+
this.validateMessages(messages);
|
|
40212
|
+
const config = this.config("chat", options);
|
|
40213
|
+
const body = this.chatBody(config, messages, options);
|
|
40214
|
+
const headers = this.headers(config);
|
|
40215
|
+
return options.stream ? this.streamRequest(config, headers, body) : this.chatResponse(config, headers, body);
|
|
40216
|
+
}
|
|
40217
|
+
static async complete(prompt, options = {}) {
|
|
40218
|
+
if (typeof prompt !== "string") throw new AiConfigError("AI prompt must be a string");
|
|
40219
|
+
return (await this.chat([{ role: "user", content: prompt }], { ...options, stream: false })).text;
|
|
40220
|
+
}
|
|
40221
|
+
static async embed(textOrTexts, options = {}) {
|
|
40222
|
+
const single = typeof textOrTexts === "string";
|
|
40223
|
+
if (!single && (!Array.isArray(textOrTexts) || textOrTexts.length === 0 || !textOrTexts.every((item) => typeof item === "string"))) {
|
|
40224
|
+
throw new AiConfigError("AI embedding input must be a string or a non-empty list of strings");
|
|
40225
|
+
}
|
|
40226
|
+
const config = this.config("embed", options);
|
|
40227
|
+
if (config.provider === "anthropic") throw new AiConfigError("Anthropic does not provide the embedding endpoint in this contract");
|
|
40228
|
+
const raw = await this.requestJson(config, this.headers(config), { model: config.model, input: textOrTexts });
|
|
40229
|
+
try {
|
|
40230
|
+
const data = raw.data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
40231
|
+
const vectors = data.map((item) => item.embedding);
|
|
40232
|
+
const expected = single ? 1 : textOrTexts.length;
|
|
40233
|
+
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();
|
|
40234
|
+
return single ? vectors[0] : vectors;
|
|
40235
|
+
} catch {
|
|
40236
|
+
throw new AiParseError("AI provider returned a malformed embedding response");
|
|
40237
|
+
}
|
|
40238
|
+
}
|
|
40239
|
+
static validateMessages(messages) {
|
|
40240
|
+
if (!Array.isArray(messages) || messages.length === 0 || !messages.every((message) => message && ["system", "user", "assistant"].includes(message.role) && typeof message.content === "string")) {
|
|
40241
|
+
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
40242
|
+
}
|
|
40243
|
+
}
|
|
40244
|
+
static number(name, fallback, minimum) {
|
|
40245
|
+
const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
|
|
40246
|
+
if (!Number.isFinite(value) || value < minimum) throw new AiConfigError(`${name} must be numeric and at least ${minimum}`);
|
|
40247
|
+
return value;
|
|
40248
|
+
}
|
|
40249
|
+
static config(capability, options) {
|
|
40250
|
+
const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
|
|
40251
|
+
if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
|
|
40252
|
+
const key = process.env.TINA4_AI_KEY || null;
|
|
40253
|
+
if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
40254
|
+
const defaults = {
|
|
40255
|
+
local: ["http://localhost:11437", "llama3.2"],
|
|
40256
|
+
openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
|
|
40257
|
+
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
|
|
40258
|
+
};
|
|
40259
|
+
const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
|
|
40260
|
+
const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
|
|
40261
|
+
if (!model) throw new AiConfigError("AI model must be a non-empty string");
|
|
40262
|
+
const totalTimeout = options.timeout === void 0 ? this.number("TINA4_AI_TIMEOUT", 60, 1e-3) : Number(options.timeout);
|
|
40263
|
+
if (!Number.isFinite(totalTimeout) || totalTimeout <= 0) throw new AiConfigError("AI timeout must be greater than zero");
|
|
40264
|
+
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)) };
|
|
40265
|
+
}
|
|
40266
|
+
static endpoint(value, capability, provider) {
|
|
40267
|
+
let url;
|
|
40268
|
+
try {
|
|
40269
|
+
url = new URL(value);
|
|
40270
|
+
} catch {
|
|
40271
|
+
throw new AiConfigError("AI URL must be an http or https URL");
|
|
40272
|
+
}
|
|
40273
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
|
|
40274
|
+
const path8 = url.pathname.replace(/\/+$/, "");
|
|
40275
|
+
if (path8 === "" || path8 === "/v1" || path8 === "/api") {
|
|
40276
|
+
const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
|
|
40277
|
+
url.pathname = (path8 || "/v1") + suffix;
|
|
40278
|
+
}
|
|
40279
|
+
return url.toString();
|
|
40280
|
+
}
|
|
40281
|
+
static headers(config) {
|
|
40282
|
+
const headers = { "content-type": "application/json", accept: "application/json" };
|
|
40283
|
+
if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
|
|
40284
|
+
if (config.provider === "anthropic") {
|
|
40285
|
+
headers["x-api-key"] = config.key;
|
|
40286
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
40287
|
+
}
|
|
40288
|
+
return headers;
|
|
40289
|
+
}
|
|
40290
|
+
static chatBody(config, messages, options) {
|
|
40291
|
+
const body = { model: config.model, messages, stream: options.stream ?? false };
|
|
40292
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
40293
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
40294
|
+
if (config.provider === "anthropic") {
|
|
40295
|
+
const system = messages.filter((message) => message.role === "system").map((message) => message.content);
|
|
40296
|
+
body.messages = messages.filter((message) => message.role !== "system");
|
|
40297
|
+
body.max_tokens = options.maxTokens ?? 1024;
|
|
40298
|
+
if (system.length) body.system = system.join("\n\n");
|
|
40299
|
+
}
|
|
40300
|
+
return body;
|
|
40301
|
+
}
|
|
40302
|
+
static open(config, deadline, headers, body) {
|
|
40303
|
+
const remainingMs = deadline - performance.now();
|
|
40304
|
+
if (remainingMs <= 0) return Promise.reject(new AiTimeoutError("AI total request timeout expired"));
|
|
40305
|
+
const url = new URL(config.url);
|
|
40306
|
+
const payload = JSON.stringify(body);
|
|
40307
|
+
const controller = new AbortController();
|
|
40308
|
+
const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
|
|
40309
|
+
return new Promise((resolve20, reject) => {
|
|
40310
|
+
const client = url.protocol === "https:" ? https2 : http2;
|
|
40311
|
+
const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
|
|
40312
|
+
clearTimeout(connectTimer);
|
|
40313
|
+
resolve20({ response, cleanup: () => {
|
|
40314
|
+
clearTimeout(totalTimer);
|
|
40315
|
+
clearTimeout(connectTimer);
|
|
40316
|
+
} });
|
|
40317
|
+
});
|
|
40318
|
+
const connectTimer = setTimeout(() => request.destroy(new AiTimeoutError("AI connection timeout expired")), Math.min(config.connectTimeout * 1e3, remainingMs));
|
|
40319
|
+
request.on("socket", (socket) => {
|
|
40320
|
+
if (!socket.connecting) clearTimeout(connectTimer);
|
|
40321
|
+
socket.once(url.protocol === "https:" ? "secureConnect" : "connect", () => clearTimeout(connectTimer));
|
|
40322
|
+
});
|
|
40323
|
+
request.once("error", (error) => {
|
|
40324
|
+
clearTimeout(totalTimer);
|
|
40325
|
+
clearTimeout(connectTimer);
|
|
40326
|
+
if (error instanceof AiError) reject(error);
|
|
40327
|
+
else if (controller.signal.aborted) reject(new AiTimeoutError("AI total request timeout expired"));
|
|
40328
|
+
else reject(new AiHTTPError(`AI transport failed (${error.name})`));
|
|
40329
|
+
});
|
|
40330
|
+
request.end(payload);
|
|
40331
|
+
});
|
|
40332
|
+
}
|
|
40333
|
+
static async readBody(response) {
|
|
40334
|
+
const chunks = [];
|
|
40335
|
+
for await (const chunk of response) chunks.push(Buffer.from(chunk));
|
|
40336
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
40337
|
+
}
|
|
40338
|
+
static retryDelay(headers, deadline) {
|
|
40339
|
+
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
40340
|
+
const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
|
|
40341
|
+
const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
|
|
40342
|
+
return new Promise((resolve20) => setTimeout(resolve20, delay));
|
|
40343
|
+
}
|
|
40344
|
+
static async requestJson(config, headers, body) {
|
|
40345
|
+
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
40346
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
40347
|
+
let opened = null;
|
|
40348
|
+
try {
|
|
40349
|
+
opened = await this.open(config, deadline, headers, body);
|
|
40350
|
+
const status2 = opened.response.statusCode ?? 0;
|
|
40351
|
+
const responseHeaders = opened.response.headers;
|
|
40352
|
+
const raw = await this.readBody(opened.response);
|
|
40353
|
+
opened.cleanup();
|
|
40354
|
+
opened = null;
|
|
40355
|
+
if (status2 < 200 || status2 >= 300) {
|
|
40356
|
+
if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
|
|
40357
|
+
await this.retryDelay(responseHeaders, deadline);
|
|
40358
|
+
continue;
|
|
40359
|
+
}
|
|
40360
|
+
throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
|
|
40361
|
+
}
|
|
40362
|
+
let parsed;
|
|
40363
|
+
try {
|
|
40364
|
+
parsed = JSON.parse(raw);
|
|
40365
|
+
} catch {
|
|
40366
|
+
throw new AiParseError("AI provider returned malformed JSON");
|
|
40367
|
+
}
|
|
40368
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new AiParseError("AI provider returned a non-object JSON response");
|
|
40369
|
+
return parsed;
|
|
40370
|
+
} catch (error) {
|
|
40371
|
+
opened?.cleanup();
|
|
40372
|
+
if (error instanceof AiParseError || error instanceof AiHTTPError && error.status !== null) throw error;
|
|
40373
|
+
if (attempt >= config.maxRetries) throw error;
|
|
40374
|
+
}
|
|
40375
|
+
}
|
|
40376
|
+
throw new AiHTTPError("AI request failed");
|
|
40377
|
+
}
|
|
40378
|
+
static normalizeChat(provider, raw) {
|
|
40379
|
+
try {
|
|
40380
|
+
if (provider === "anthropic") {
|
|
40381
|
+
const content = raw.content;
|
|
40382
|
+
const parts = content.filter((item) => (item.type ?? "text") === "text").map((item) => item.text);
|
|
40383
|
+
if (!parts.length || !parts.every((part) => typeof part === "string")) throw new Error();
|
|
40384
|
+
const usage2 = raw.usage ?? {};
|
|
40385
|
+
const promptTokens = Number(usage2.input_tokens ?? 0);
|
|
40386
|
+
const completionTokens = Number(usage2.output_tokens ?? 0);
|
|
40387
|
+
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 };
|
|
40388
|
+
}
|
|
40389
|
+
const choice = raw.choices[0];
|
|
40390
|
+
const text = choice.message.content;
|
|
40391
|
+
if (typeof text !== "string") throw new Error();
|
|
40392
|
+
const usage = raw.usage ?? {};
|
|
40393
|
+
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 };
|
|
40394
|
+
} catch {
|
|
40395
|
+
throw new AiParseError("AI provider returned a malformed chat response");
|
|
40396
|
+
}
|
|
40397
|
+
}
|
|
40398
|
+
static async chatResponse(config, headers, body) {
|
|
40399
|
+
return this.normalizeChat(config.provider, await this.requestJson(config, headers, body));
|
|
40400
|
+
}
|
|
40401
|
+
static streamDelta(provider, data) {
|
|
40402
|
+
if (data === "[DONE]") return { completed: true };
|
|
40403
|
+
let event;
|
|
40404
|
+
try {
|
|
40405
|
+
event = JSON.parse(data);
|
|
40406
|
+
} catch {
|
|
40407
|
+
throw new AiParseError("AI provider returned malformed stream data");
|
|
40408
|
+
}
|
|
40409
|
+
const text = provider === "anthropic" ? event.type === "content_block_delta" ? event.delta?.text : void 0 : event.choices?.[0]?.delta?.content;
|
|
40410
|
+
if (text !== void 0 && text !== null && typeof text !== "string") throw new AiParseError("AI provider returned malformed stream data");
|
|
40411
|
+
return { completed: false, text };
|
|
40412
|
+
}
|
|
40413
|
+
static async *streamData(response) {
|
|
40414
|
+
let buffer = "";
|
|
40415
|
+
for await (const chunk of response) {
|
|
40416
|
+
buffer += Buffer.from(chunk).toString("utf8");
|
|
40417
|
+
let newline;
|
|
40418
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
40419
|
+
const line = buffer.slice(0, newline).trim();
|
|
40420
|
+
buffer = buffer.slice(newline + 1);
|
|
40421
|
+
if (line.startsWith("data:")) yield line.slice(5).trim();
|
|
40422
|
+
}
|
|
40423
|
+
}
|
|
40424
|
+
}
|
|
40425
|
+
static streamError(error) {
|
|
40426
|
+
if (error instanceof AiError) return error;
|
|
40427
|
+
if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
|
|
40428
|
+
return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
|
|
40429
|
+
}
|
|
40430
|
+
static async *streamRequest(config, headers, body) {
|
|
40431
|
+
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
40432
|
+
let yielded = false;
|
|
40433
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
40434
|
+
let opened = null;
|
|
40435
|
+
try {
|
|
40436
|
+
opened = await this.open(config, deadline, { ...headers, accept: "text/event-stream" }, body);
|
|
40437
|
+
const status2 = opened.response.statusCode ?? 0;
|
|
40438
|
+
if (status2 < 200 || status2 >= 300) {
|
|
40439
|
+
await this.readBody(opened.response);
|
|
40440
|
+
if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
|
|
40441
|
+
await this.retryDelay(opened.response.headers, deadline);
|
|
40442
|
+
opened.cleanup();
|
|
40443
|
+
opened = null;
|
|
40444
|
+
continue;
|
|
40445
|
+
}
|
|
40446
|
+
throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
|
|
40447
|
+
}
|
|
40448
|
+
let completed = false;
|
|
40449
|
+
for await (const data of this.streamData(opened.response)) {
|
|
40450
|
+
const delta = this.streamDelta(config.provider, data);
|
|
40451
|
+
if (delta.completed) {
|
|
40452
|
+
completed = true;
|
|
40453
|
+
break;
|
|
40454
|
+
}
|
|
40455
|
+
if (delta.text === void 0) continue;
|
|
40456
|
+
yielded = true;
|
|
40457
|
+
yield delta.text;
|
|
40458
|
+
}
|
|
40459
|
+
opened.cleanup();
|
|
40460
|
+
opened = null;
|
|
40461
|
+
if (completed) return;
|
|
40462
|
+
throw new AiParseError("AI provider stream ended before [DONE]");
|
|
40463
|
+
} catch (error) {
|
|
40464
|
+
opened?.cleanup();
|
|
40465
|
+
const failure = this.streamError(error);
|
|
40466
|
+
if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || yielded || attempt >= config.maxRetries) throw failure;
|
|
40467
|
+
}
|
|
40468
|
+
}
|
|
40469
|
+
}
|
|
40470
|
+
};
|
|
40471
|
+
}
|
|
40472
|
+
});
|
|
40473
|
+
|
|
40464
40474
|
// src/queueBackends/rabbitmqBackend.ts
|
|
40465
40475
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
40466
40476
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
@@ -42164,6 +42174,12 @@ __export(index_exports, {
|
|
|
42164
42174
|
APPLICATION_JSON: () => APPLICATION_JSON,
|
|
42165
42175
|
APPLICATION_OCTET: () => APPLICATION_OCTET,
|
|
42166
42176
|
APPLICATION_XML: () => APPLICATION_XML,
|
|
42177
|
+
Ai: () => Ai,
|
|
42178
|
+
AiConfigError: () => AiConfigError,
|
|
42179
|
+
AiError: () => AiError,
|
|
42180
|
+
AiHTTPError: () => AiHTTPError,
|
|
42181
|
+
AiParseError: () => AiParseError,
|
|
42182
|
+
AiTimeoutError: () => AiTimeoutError,
|
|
42167
42183
|
Api: () => Api,
|
|
42168
42184
|
Auth: () => Auth,
|
|
42169
42185
|
CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
|
|
@@ -42490,6 +42506,7 @@ var init_index = __esm({
|
|
|
42490
42506
|
init_htmlElement();
|
|
42491
42507
|
init_errorOverlay();
|
|
42492
42508
|
init_ai();
|
|
42509
|
+
init_aiClient();
|
|
42493
42510
|
init_liteBackend();
|
|
42494
42511
|
init_rabbitmqBackend();
|
|
42495
42512
|
init_kafkaBackend();
|
|
@@ -42519,6 +42536,12 @@ export {
|
|
|
42519
42536
|
APPLICATION_JSON,
|
|
42520
42537
|
APPLICATION_OCTET,
|
|
42521
42538
|
APPLICATION_XML,
|
|
42539
|
+
Ai,
|
|
42540
|
+
AiConfigError,
|
|
42541
|
+
AiError,
|
|
42542
|
+
AiHTTPError,
|
|
42543
|
+
AiParseError,
|
|
42544
|
+
AiTimeoutError,
|
|
42522
42545
|
Api,
|
|
42523
42546
|
Auth,
|
|
42524
42547
|
CANONICAL_SESSION_BACKENDS,
|