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
package/packages/cli/dist/bin.js
CHANGED
|
@@ -1461,7 +1461,10 @@ var init_trustedProxy = __esm({
|
|
|
1461
1461
|
var engine_exports = {};
|
|
1462
1462
|
__export(engine_exports, {
|
|
1463
1463
|
Frond: () => Frond,
|
|
1464
|
+
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
1464
1465
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
1466
|
+
filterChainCache: () => filterChainCache,
|
|
1467
|
+
pathParseCache: () => pathParseCache,
|
|
1465
1468
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
1466
1469
|
});
|
|
1467
1470
|
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -1565,6 +1568,12 @@ function capCache(cache, maxEntries) {
|
|
|
1565
1568
|
if (--drop <= 0) break;
|
|
1566
1569
|
}
|
|
1567
1570
|
}
|
|
1571
|
+
function sweepExpiredCache(cache) {
|
|
1572
|
+
const now = Date.now();
|
|
1573
|
+
for (const [key, [, expiresAt]] of cache) {
|
|
1574
|
+
if (expiresAt <= now) cache.delete(key);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1568
1577
|
function tokenize(source) {
|
|
1569
1578
|
const rawBlocks = [];
|
|
1570
1579
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -1628,6 +1637,16 @@ function stripTag(raw) {
|
|
|
1628
1637
|
}
|
|
1629
1638
|
return [inner.trim(), stripBefore, stripAfter];
|
|
1630
1639
|
}
|
|
1640
|
+
function extendsTarget(source) {
|
|
1641
|
+
const matches = source.match(EXTENDS_RE_GLOBAL);
|
|
1642
|
+
if (matches && matches.length > 1) {
|
|
1643
|
+
throw new Error(
|
|
1644
|
+
`Frond: template has ${matches.length} "{% extends %}" tags -- a template can extend only one parent`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
const match = source.match(EXTENDS_RE);
|
|
1648
|
+
return match ? match[1] : "";
|
|
1649
|
+
}
|
|
1631
1650
|
function resolveVar(expr, context) {
|
|
1632
1651
|
expr = expr.trim();
|
|
1633
1652
|
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
@@ -1708,6 +1727,7 @@ function resolveVar(expr, context) {
|
|
|
1708
1727
|
fromBracket.push(false);
|
|
1709
1728
|
}
|
|
1710
1729
|
}
|
|
1730
|
+
capCache(pathParseCache, MEMO_CACHE_MAX);
|
|
1711
1731
|
pathParseCache.set(expr, [parts, fromBracket]);
|
|
1712
1732
|
}
|
|
1713
1733
|
let value = context;
|
|
@@ -2295,6 +2315,7 @@ function parseFilterChain(expr) {
|
|
|
2295
2315
|
}
|
|
2296
2316
|
}
|
|
2297
2317
|
const result = [variable, filters];
|
|
2318
|
+
capCache(filterChainCache, MEMO_CACHE_MAX);
|
|
2298
2319
|
filterChainCache.set(expr, result);
|
|
2299
2320
|
return result;
|
|
2300
2321
|
}
|
|
@@ -2474,7 +2495,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
2474
2495
|
function _generateFormTokenValue(descriptor = "") {
|
|
2475
2496
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
2476
2497
|
}
|
|
2477
|
-
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;
|
|
2498
|
+
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;
|
|
2478
2499
|
var init_engine = __esm({
|
|
2479
2500
|
"../frond/src/engine.ts"() {
|
|
2480
2501
|
"use strict";
|
|
@@ -2568,9 +2589,12 @@ var init_engine = __esm({
|
|
|
2568
2589
|
LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
|
|
2569
2590
|
LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
|
|
2570
2591
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
2592
|
+
EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
|
|
2593
|
+
EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
|
|
2571
2594
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
2572
2595
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
2573
2596
|
TEMPLATE_CACHE_MAX = 256;
|
|
2597
|
+
MEMO_CACHE_MAX = 1024;
|
|
2574
2598
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
2575
2599
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
2576
2600
|
VarRef = class {
|
|
@@ -2923,29 +2947,22 @@ var init_engine = __esm({
|
|
|
2923
2947
|
return this;
|
|
2924
2948
|
}
|
|
2925
2949
|
/**
|
|
2926
|
-
* Register a custom filter
|
|
2927
|
-
*
|
|
2928
|
-
* the live instance's local filter map also receives the addition
|
|
2929
|
-
* immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
|
|
2950
|
+
* Register a custom filter on this instance only. Use the static method
|
|
2951
|
+
* for process-global registration. tina4: ADR-0052.
|
|
2930
2952
|
*/
|
|
2931
2953
|
addFilter(name, fn) {
|
|
2932
|
-
_Frond.classFilters.set(name, fn);
|
|
2933
2954
|
this.filters[name] = fn;
|
|
2934
2955
|
}
|
|
2935
2956
|
/**
|
|
2936
|
-
* Register a global variable
|
|
2937
|
-
* at class level — see ``addFilter`` for the dual-call semantics.
|
|
2957
|
+
* Register a global variable on this instance only.
|
|
2938
2958
|
*/
|
|
2939
2959
|
addGlobal(name, value) {
|
|
2940
|
-
_Frond.classGlobals.set(name, value);
|
|
2941
2960
|
this.globals[name] = value;
|
|
2942
2961
|
}
|
|
2943
2962
|
/**
|
|
2944
|
-
* Register a custom test
|
|
2945
|
-
* ``addFilter`` for the dual-call semantics.
|
|
2963
|
+
* Register a custom test on this instance only.
|
|
2946
2964
|
*/
|
|
2947
2965
|
addTest(name, fn) {
|
|
2948
|
-
_Frond.classTests.set(name, fn);
|
|
2949
2966
|
this.tests[name] = fn;
|
|
2950
2967
|
}
|
|
2951
2968
|
/**
|
|
@@ -3060,9 +3077,8 @@ var init_engine = __esm({
|
|
|
3060
3077
|
if (Object.keys(this.tests).length > 0) {
|
|
3061
3078
|
context.__frond_tests__ = this.tests;
|
|
3062
3079
|
}
|
|
3063
|
-
const
|
|
3064
|
-
if (
|
|
3065
|
-
const parentName = extendsMatch[1];
|
|
3080
|
+
const parentName = extendsTarget(source);
|
|
3081
|
+
if (parentName) {
|
|
3066
3082
|
const parentSource = this.load(parentName);
|
|
3067
3083
|
const childBlocks = this.extractBlocks(source);
|
|
3068
3084
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -3073,9 +3089,8 @@ var init_engine = __esm({
|
|
|
3073
3089
|
if (Object.keys(this.tests).length > 0) {
|
|
3074
3090
|
context.__frond_tests__ = this.tests;
|
|
3075
3091
|
}
|
|
3076
|
-
const
|
|
3077
|
-
if (
|
|
3078
|
-
const parentName = extendsMatch[1];
|
|
3092
|
+
const parentName = extendsTarget(source);
|
|
3093
|
+
if (parentName) {
|
|
3079
3094
|
const parentSource = this.load(parentName);
|
|
3080
3095
|
const childBlocks = this.extractBlocks(source);
|
|
3081
3096
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -3120,10 +3135,93 @@ var init_engine = __esm({
|
|
|
3120
3135
|
}
|
|
3121
3136
|
return blocks;
|
|
3122
3137
|
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Depth-aware block substitution against `source` (typically the
|
|
3140
|
+
* fully-resolved root template).
|
|
3141
|
+
*
|
|
3142
|
+
* A single regex `.replace()` pass (the flat `pattern` this replaces in
|
|
3143
|
+
* renderWithBlocks) pairs an OUTER block's open tag with the FIRST
|
|
3144
|
+
* `{% endblock %}` found -- which, when the outer block wraps a NESTED
|
|
3145
|
+
* `{% block %}`, is the nested block's own close tag, not the outer's.
|
|
3146
|
+
* That silently truncates the outer block's captured content and drops
|
|
3147
|
+
* everything after the inner endblock (the root-nested-block
|
|
3148
|
+
* content-loss bug). This scans with an open/close depth counter
|
|
3149
|
+
* instead (mirroring extractBlocks), so an outer block always captures
|
|
3150
|
+
* its FULL body, nested child blocks included.
|
|
3151
|
+
*
|
|
3152
|
+
* The content chosen for each block -- the child override in `blocks`
|
|
3153
|
+
* if present, else the block's own default body -- is then recursively
|
|
3154
|
+
* substituted against the SAME `blocks` map before being tokenized and
|
|
3155
|
+
* rendered, so a block nested inside another block resolves correctly
|
|
3156
|
+
* regardless of which template in the inheritance chain declared the
|
|
3157
|
+
* nesting (the root, an intermediate, however many levels deep).
|
|
3158
|
+
*
|
|
3159
|
+
* `{{ parent() }}` / `{{ super() }}` inside a block still render that
|
|
3160
|
+
* block's OWN default content at this level (lazy, on first call).
|
|
3161
|
+
*/
|
|
3162
|
+
substituteBlocks(source, blocks, context) {
|
|
3163
|
+
const blockOpen = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}/g;
|
|
3164
|
+
const blockClose = /\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
3165
|
+
const engine = this;
|
|
3166
|
+
const pieces = [];
|
|
3167
|
+
let pos = 0;
|
|
3168
|
+
while (pos < source.length) {
|
|
3169
|
+
blockOpen.lastIndex = pos;
|
|
3170
|
+
const mOpen = blockOpen.exec(source);
|
|
3171
|
+
if (!mOpen) {
|
|
3172
|
+
pieces.push(source.slice(pos));
|
|
3173
|
+
break;
|
|
3174
|
+
}
|
|
3175
|
+
pieces.push(source.slice(pos, mOpen.index));
|
|
3176
|
+
const name = mOpen[1];
|
|
3177
|
+
const contentStart = mOpen.index + mOpen[0].length;
|
|
3178
|
+
let depth = 1;
|
|
3179
|
+
let scan = contentStart;
|
|
3180
|
+
let closeMatch = null;
|
|
3181
|
+
while (depth > 0 && scan < source.length) {
|
|
3182
|
+
blockOpen.lastIndex = scan;
|
|
3183
|
+
blockClose.lastIndex = scan;
|
|
3184
|
+
const nextOpen = blockOpen.exec(source);
|
|
3185
|
+
const nextClose = blockClose.exec(source);
|
|
3186
|
+
if (!nextClose) break;
|
|
3187
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
3188
|
+
depth++;
|
|
3189
|
+
scan = nextOpen.index + nextOpen[0].length;
|
|
3190
|
+
} else {
|
|
3191
|
+
depth--;
|
|
3192
|
+
if (depth === 0) {
|
|
3193
|
+
closeMatch = nextClose;
|
|
3194
|
+
} else {
|
|
3195
|
+
scan = nextClose.index + nextClose[0].length;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
if (!closeMatch) {
|
|
3200
|
+
pieces.push(source.slice(mOpen.index));
|
|
3201
|
+
pos = source.length;
|
|
3202
|
+
break;
|
|
3203
|
+
}
|
|
3204
|
+
const parentContent = source.slice(contentStart, closeMatch.index);
|
|
3205
|
+
const blockSource = blocks[name] ?? parentContent;
|
|
3206
|
+
const resolvedSource = engine.substituteBlocks(blockSource, blocks, context);
|
|
3207
|
+
let renderedParent = null;
|
|
3208
|
+
const getParent = () => {
|
|
3209
|
+
if (renderedParent === null) {
|
|
3210
|
+
renderedParent = new SafeString(
|
|
3211
|
+
engine.renderTokens(tokenize(parentContent), context)
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
return renderedParent;
|
|
3215
|
+
};
|
|
3216
|
+
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
3217
|
+
pieces.push(engine.renderTokens(tokenize(resolvedSource), blockCtx));
|
|
3218
|
+
pos = closeMatch.index + closeMatch[0].length;
|
|
3219
|
+
}
|
|
3220
|
+
return pieces.join("");
|
|
3221
|
+
}
|
|
3123
3222
|
renderWithBlocks(parentSource, context, childBlocks) {
|
|
3124
|
-
const
|
|
3125
|
-
if (
|
|
3126
|
-
const grandparentName = extendsMatch[1];
|
|
3223
|
+
const grandparentName = extendsTarget(parentSource);
|
|
3224
|
+
if (grandparentName) {
|
|
3127
3225
|
const grandparentSource = this.load(grandparentName);
|
|
3128
3226
|
const parentBlocks = this.extractBlocks(parentSource);
|
|
3129
3227
|
const mergedBlocks = { ...parentBlocks, ...childBlocks };
|
|
@@ -3143,22 +3241,7 @@ var init_engine = __esm({
|
|
|
3143
3241
|
}
|
|
3144
3242
|
return this.renderWithBlocks(grandparentSource, context, mergedBlocks);
|
|
3145
3243
|
}
|
|
3146
|
-
const
|
|
3147
|
-
const engine = this;
|
|
3148
|
-
const result = parentSource.replace(pattern, (_match, name, parentContent) => {
|
|
3149
|
-
const blockSource = childBlocks[name] ?? parentContent;
|
|
3150
|
-
let renderedParent = null;
|
|
3151
|
-
const getParent = () => {
|
|
3152
|
-
if (renderedParent === null) {
|
|
3153
|
-
renderedParent = new SafeString(
|
|
3154
|
-
engine.renderTokens(tokenize(parentContent), context)
|
|
3155
|
-
);
|
|
3156
|
-
}
|
|
3157
|
-
return renderedParent;
|
|
3158
|
-
};
|
|
3159
|
-
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
3160
|
-
return this.renderTokens(tokenize(blockSource), blockCtx);
|
|
3161
|
-
});
|
|
3244
|
+
const result = this.substituteBlocks(parentSource, childBlocks, context);
|
|
3162
3245
|
return this.renderTokens(tokenize(result), context);
|
|
3163
3246
|
}
|
|
3164
3247
|
renderTokens(tokens, context) {
|
|
@@ -3981,6 +4064,7 @@ var init_engine = __esm({
|
|
|
3981
4064
|
const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
|
|
3982
4065
|
const cacheKey = m ? m[1] : "default";
|
|
3983
4066
|
const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
|
|
4067
|
+
sweepExpiredCache(this.fragmentCache);
|
|
3984
4068
|
const cached = this.fragmentCache.get(cacheKey);
|
|
3985
4069
|
if (cached) {
|
|
3986
4070
|
const [htmlContent, expiresAt] = cached;
|
|
@@ -4028,6 +4112,7 @@ var init_engine = __esm({
|
|
|
4028
4112
|
i++;
|
|
4029
4113
|
}
|
|
4030
4114
|
const rendered = this.renderTokens([...bodyTokens], context);
|
|
4115
|
+
capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
|
|
4031
4116
|
this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
|
|
4032
4117
|
return [rendered, i];
|
|
4033
4118
|
}
|
|
@@ -19482,14 +19567,14 @@ async function discoverRoutes(routesDir) {
|
|
|
19482
19567
|
const currentMtime = statSync7(filePath).mtimeMs;
|
|
19483
19568
|
if (_seenFiles.has(filePath) && _seenMtimes.get(filePath) === currentMtime) continue;
|
|
19484
19569
|
const method = name.toUpperCase();
|
|
19485
|
-
const
|
|
19486
|
-
const pattern = filePathToPattern(
|
|
19570
|
+
const relativePath2 = relative(routesDir, filePath);
|
|
19571
|
+
const pattern = filePathToPattern(relativePath2);
|
|
19487
19572
|
try {
|
|
19488
19573
|
const moduleUrl = `file://${filePath}?t=${currentMtime}`;
|
|
19489
19574
|
const mod = await import(moduleUrl);
|
|
19490
19575
|
const handler = mod.default ?? mod.handler;
|
|
19491
19576
|
if (typeof handler !== "function") {
|
|
19492
|
-
console.warn(` Warning: ${
|
|
19577
|
+
console.warn(` Warning: ${relativePath2} does not export a handler function, skipping`);
|
|
19493
19578
|
continue;
|
|
19494
19579
|
}
|
|
19495
19580
|
const meta = mod.meta;
|
|
@@ -19501,7 +19586,7 @@ async function discoverRoutes(routesDir) {
|
|
|
19501
19586
|
_seenMtimes.set(filePath, currentMtime);
|
|
19502
19587
|
registeredFromThisScan++;
|
|
19503
19588
|
} catch (err) {
|
|
19504
|
-
console.error(` Error loading route ${
|
|
19589
|
+
console.error(` Error loading route ${relativePath2}:`, err);
|
|
19505
19590
|
recordBrokenImport(filePath, err);
|
|
19506
19591
|
}
|
|
19507
19592
|
}
|
|
@@ -19535,8 +19620,8 @@ function recordBrokenImport(filePath, error) {
|
|
|
19535
19620
|
} catch {
|
|
19536
19621
|
}
|
|
19537
19622
|
}
|
|
19538
|
-
function filePathToPattern(
|
|
19539
|
-
const parts =
|
|
19623
|
+
function filePathToPattern(relativePath2) {
|
|
19624
|
+
const parts = relativePath2.replace(/\\/g, "/").split("/").slice(0, -1);
|
|
19540
19625
|
const urlParts = parts.map((part) => {
|
|
19541
19626
|
if (part.startsWith("[...") && part.endsWith("]")) {
|
|
19542
19627
|
const name = part.slice(4, -1);
|
|
@@ -22793,500 +22878,127 @@ import * as fs3 from "node:fs";
|
|
|
22793
22878
|
import * as path2 from "node:path";
|
|
22794
22879
|
import { spawnSync } from "node:child_process";
|
|
22795
22880
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
22796
|
-
function
|
|
22797
|
-
|
|
22798
|
-
|
|
22799
|
-
|
|
22800
|
-
|
|
22801
|
-
|
|
22802
|
-
if (entry.isDirectory()) {
|
|
22803
|
-
if (!exclude.includes(entry.name)) {
|
|
22804
|
-
results.push(...walkFiles(fullPath, extensions, exclude));
|
|
22805
|
-
}
|
|
22806
|
-
} else if (entry.isFile()) {
|
|
22807
|
-
const ext = path2.extname(entry.name);
|
|
22808
|
-
if (extensions.includes(ext) && !entry.name.endsWith(".d.ts")) {
|
|
22809
|
-
results.push(fullPath);
|
|
22810
|
-
}
|
|
22811
|
-
}
|
|
22812
|
-
}
|
|
22813
|
-
return results;
|
|
22814
|
-
}
|
|
22815
|
-
function readFileSafe(filePath) {
|
|
22816
|
-
try {
|
|
22817
|
-
return fs3.readFileSync(filePath, "utf-8");
|
|
22818
|
-
} catch {
|
|
22819
|
-
return null;
|
|
22820
|
-
}
|
|
22821
|
-
}
|
|
22822
|
-
function relativePath(filePath, root = ".") {
|
|
22823
|
-
return path2.relative(root, filePath);
|
|
22824
|
-
}
|
|
22825
|
-
function countLines(source) {
|
|
22826
|
-
const lines = source.split("\n");
|
|
22827
|
-
let loc = 0;
|
|
22828
|
-
let blank = 0;
|
|
22829
|
-
let comment = 0;
|
|
22830
|
-
let inBlockComment = false;
|
|
22831
|
-
for (const line of lines) {
|
|
22832
|
-
const stripped = line.trim();
|
|
22833
|
-
if (!stripped) {
|
|
22834
|
-
blank++;
|
|
22835
|
-
continue;
|
|
22836
|
-
}
|
|
22837
|
-
if (inBlockComment) {
|
|
22838
|
-
comment++;
|
|
22839
|
-
if (stripped.includes("*/")) {
|
|
22840
|
-
inBlockComment = false;
|
|
22841
|
-
}
|
|
22842
|
-
continue;
|
|
22843
|
-
}
|
|
22844
|
-
if (stripped.startsWith("/*")) {
|
|
22845
|
-
comment++;
|
|
22846
|
-
if (!stripped.includes("*/") || stripped.endsWith("/*")) {
|
|
22847
|
-
inBlockComment = true;
|
|
22848
|
-
}
|
|
22849
|
-
continue;
|
|
22850
|
-
}
|
|
22851
|
-
if (stripped.startsWith("//")) {
|
|
22852
|
-
comment++;
|
|
22853
|
-
continue;
|
|
22854
|
-
}
|
|
22855
|
-
loc++;
|
|
22856
|
-
}
|
|
22857
|
-
return { loc, blank, comment };
|
|
22858
|
-
}
|
|
22859
|
-
function stripLiterals(source) {
|
|
22860
|
-
const out = [];
|
|
22861
|
-
const n = source.length;
|
|
22862
|
-
let i = 0;
|
|
22863
|
-
let prevSignificant = "";
|
|
22864
|
-
let prevWord = "";
|
|
22865
|
-
const regexKeywords = /* @__PURE__ */ new Set([
|
|
22866
|
-
"return",
|
|
22867
|
-
"typeof",
|
|
22868
|
-
"instanceof",
|
|
22869
|
-
"in",
|
|
22870
|
-
"of",
|
|
22871
|
-
"new",
|
|
22872
|
-
"delete",
|
|
22873
|
-
"void",
|
|
22874
|
-
"throw",
|
|
22875
|
-
"case",
|
|
22876
|
-
"do",
|
|
22877
|
-
"else",
|
|
22878
|
-
"yield",
|
|
22879
|
-
"await"
|
|
22880
|
-
]);
|
|
22881
|
-
function prevEndsExpression() {
|
|
22882
|
-
if (prevSignificant === "") return false;
|
|
22883
|
-
if (/[A-Za-z0-9_$]/.test(prevSignificant)) {
|
|
22884
|
-
return !regexKeywords.has(prevWord);
|
|
22885
|
-
}
|
|
22886
|
-
if (prevSignificant === ")" || prevSignificant === "]") return true;
|
|
22887
|
-
if (prevSignificant === ".") return true;
|
|
22888
|
-
return false;
|
|
22889
|
-
}
|
|
22890
|
-
while (i < n) {
|
|
22891
|
-
const ch = source[i];
|
|
22892
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
22893
|
-
if (ch === "/" && next === "/") {
|
|
22894
|
-
out.push("//");
|
|
22895
|
-
i += 2;
|
|
22896
|
-
while (i < n && source[i] !== "\n") {
|
|
22897
|
-
out.push(" ");
|
|
22898
|
-
i++;
|
|
22899
|
-
}
|
|
22900
|
-
continue;
|
|
22901
|
-
}
|
|
22902
|
-
if (ch === "/" && next === "*") {
|
|
22903
|
-
out.push("/*");
|
|
22904
|
-
i += 2;
|
|
22905
|
-
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) {
|
|
22906
|
-
out.push(source[i] === "\n" ? "\n" : " ");
|
|
22907
|
-
i++;
|
|
22908
|
-
}
|
|
22909
|
-
if (i < n) {
|
|
22910
|
-
out.push("*/");
|
|
22911
|
-
i += 2;
|
|
22912
|
-
}
|
|
22913
|
-
continue;
|
|
22914
|
-
}
|
|
22915
|
-
if (ch === '"' || ch === "'") {
|
|
22916
|
-
const quote = ch;
|
|
22917
|
-
out.push(quote);
|
|
22918
|
-
i++;
|
|
22919
|
-
while (i < n && source[i] !== quote) {
|
|
22920
|
-
if (source[i] === "\\" && i + 1 < n) {
|
|
22921
|
-
out.push(" ");
|
|
22922
|
-
i += 2;
|
|
22923
|
-
continue;
|
|
22924
|
-
}
|
|
22925
|
-
if (source[i] === "\n") {
|
|
22926
|
-
out.push("\n");
|
|
22927
|
-
i++;
|
|
22928
|
-
break;
|
|
22929
|
-
}
|
|
22930
|
-
out.push(" ");
|
|
22931
|
-
i++;
|
|
22932
|
-
}
|
|
22933
|
-
if (i < n && source[i] === quote) {
|
|
22934
|
-
out.push(quote);
|
|
22935
|
-
i++;
|
|
22936
|
-
}
|
|
22937
|
-
prevSignificant = quote;
|
|
22938
|
-
prevWord = "";
|
|
22939
|
-
continue;
|
|
22940
|
-
}
|
|
22941
|
-
if (ch === "`") {
|
|
22942
|
-
out.push("`");
|
|
22943
|
-
i++;
|
|
22944
|
-
while (i < n && source[i] !== "`") {
|
|
22945
|
-
if (source[i] === "\\" && i + 1 < n) {
|
|
22946
|
-
out.push(source[i + 1] === "\n" ? " \n" : " ");
|
|
22947
|
-
i += 2;
|
|
22948
|
-
continue;
|
|
22949
|
-
}
|
|
22950
|
-
if (source[i] === "$" && source[i + 1] === "{") {
|
|
22951
|
-
out.push("${");
|
|
22952
|
-
i += 2;
|
|
22953
|
-
let depth = 1;
|
|
22954
|
-
const exprStart = i;
|
|
22955
|
-
while (i < n && depth > 0) {
|
|
22956
|
-
if (source[i] === "{") depth++;
|
|
22957
|
-
else if (source[i] === "}") depth--;
|
|
22958
|
-
if (depth === 0) break;
|
|
22959
|
-
i++;
|
|
22960
|
-
}
|
|
22961
|
-
out.push(stripLiterals(source.slice(exprStart, i)));
|
|
22962
|
-
if (i < n && source[i] === "}") {
|
|
22963
|
-
out.push("}");
|
|
22964
|
-
i++;
|
|
22965
|
-
}
|
|
22966
|
-
continue;
|
|
22967
|
-
}
|
|
22968
|
-
out.push(source[i] === "\n" ? "\n" : " ");
|
|
22969
|
-
i++;
|
|
22970
|
-
}
|
|
22971
|
-
if (i < n && source[i] === "`") {
|
|
22972
|
-
out.push("`");
|
|
22973
|
-
i++;
|
|
22974
|
-
}
|
|
22975
|
-
prevSignificant = "`";
|
|
22976
|
-
prevWord = "";
|
|
22977
|
-
continue;
|
|
22978
|
-
}
|
|
22979
|
-
if (ch === "/" && !prevEndsExpression()) {
|
|
22980
|
-
let j = i + 1;
|
|
22981
|
-
let ok = false;
|
|
22982
|
-
let inClass = false;
|
|
22983
|
-
while (j < n) {
|
|
22984
|
-
const c = source[j];
|
|
22985
|
-
if (c === "\\") {
|
|
22986
|
-
j += 2;
|
|
22987
|
-
continue;
|
|
22988
|
-
}
|
|
22989
|
-
if (c === "\n") break;
|
|
22990
|
-
if (c === "[") inClass = true;
|
|
22991
|
-
else if (c === "]") inClass = false;
|
|
22992
|
-
else if (c === "/" && !inClass) {
|
|
22993
|
-
ok = true;
|
|
22994
|
-
break;
|
|
22995
|
-
}
|
|
22996
|
-
j++;
|
|
22997
|
-
}
|
|
22998
|
-
if (ok) {
|
|
22999
|
-
out.push("/");
|
|
23000
|
-
for (let k = i + 1; k < j; k++) out.push(" ");
|
|
23001
|
-
out.push("/");
|
|
23002
|
-
i = j + 1;
|
|
23003
|
-
while (i < n && /[a-z]/i.test(source[i])) {
|
|
23004
|
-
out.push(source[i]);
|
|
23005
|
-
i++;
|
|
23006
|
-
}
|
|
23007
|
-
prevSignificant = "/";
|
|
23008
|
-
prevWord = "";
|
|
23009
|
-
continue;
|
|
23010
|
-
}
|
|
23011
|
-
}
|
|
23012
|
-
out.push(ch);
|
|
23013
|
-
if (!/\s/.test(ch)) {
|
|
23014
|
-
prevSignificant = ch;
|
|
23015
|
-
if (/[A-Za-z0-9_$]/.test(ch)) {
|
|
23016
|
-
prevWord = /[A-Za-z0-9_$]/.test(source[i - 1] ?? "") ? prevWord + ch : ch;
|
|
23017
|
-
} else {
|
|
23018
|
-
prevWord = "";
|
|
23019
|
-
}
|
|
23020
|
-
}
|
|
23021
|
-
i++;
|
|
23022
|
-
}
|
|
23023
|
-
return out.join("");
|
|
23024
|
-
}
|
|
23025
|
-
function countClassesQuick(source) {
|
|
23026
|
-
const matches = source.match(
|
|
23027
|
-
/(?:^|\n)\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/g
|
|
23028
|
-
);
|
|
23029
|
-
return matches ? matches.length : 0;
|
|
23030
|
-
}
|
|
23031
|
-
function countFunctionsQuick(source) {
|
|
23032
|
-
const clean = stripLiterals(source);
|
|
23033
|
-
let count = 0;
|
|
23034
|
-
const funcDecls = clean.match(
|
|
23035
|
-
/(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(/g
|
|
23036
|
-
);
|
|
23037
|
-
if (funcDecls) count += funcDecls.length;
|
|
23038
|
-
const methods = clean.match(
|
|
23039
|
-
/(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*\([^)]*\)\s*(?::\s*\S+)?\s*\{/g
|
|
23040
|
-
);
|
|
23041
|
-
if (methods) count += methods.length;
|
|
23042
|
-
const arrows = clean.match(
|
|
23043
|
-
/(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\(/g
|
|
23044
|
-
);
|
|
23045
|
-
if (arrows) count += arrows.length;
|
|
23046
|
-
return count;
|
|
23047
|
-
}
|
|
23048
|
-
function resolveRoot(root = "src") {
|
|
23049
|
-
const rootPath = path2.resolve(root);
|
|
23050
|
-
if (fs3.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
|
|
23051
|
-
_lastScanRoot = rootPath;
|
|
23052
|
-
return root;
|
|
23053
|
-
}
|
|
23054
|
-
const fwDir = path2.resolve(path2.dirname(new URL(import.meta.url).pathname));
|
|
23055
|
-
_lastScanRoot = fwDir;
|
|
23056
|
-
return fwDir;
|
|
23057
|
-
}
|
|
23058
|
-
function quickMetrics(root = "src") {
|
|
23059
|
-
root = resolveRoot(root);
|
|
23060
|
-
const rootPath = path2.resolve(root);
|
|
23061
|
-
if (!fs3.existsSync(rootPath)) {
|
|
23062
|
-
return { error: `Directory not found: ${root}` };
|
|
23063
|
-
}
|
|
23064
|
-
const tsFiles = walkFiles(rootPath, [".ts", ".js"]);
|
|
23065
|
-
const twigFiles = walkFiles(rootPath, [".twig", ".html"]);
|
|
23066
|
-
const migrationsDir = path2.resolve("migrations");
|
|
23067
|
-
const migrationFiles = [
|
|
23068
|
-
...walkFiles(migrationsDir, [".sql"]),
|
|
23069
|
-
...walkFiles(migrationsDir, [".ts"])
|
|
23070
|
-
];
|
|
23071
|
-
const scssFiles = walkFiles(rootPath, [".scss", ".css"]);
|
|
23072
|
-
let totalLoc = 0;
|
|
23073
|
-
let totalBlank = 0;
|
|
23074
|
-
let totalComment = 0;
|
|
23075
|
-
let totalClasses = 0;
|
|
23076
|
-
let totalFunctions = 0;
|
|
23077
|
-
const fileDetails = [];
|
|
23078
|
-
for (const f of tsFiles) {
|
|
23079
|
-
const source = readFileSafe(f);
|
|
23080
|
-
if (source === null) continue;
|
|
23081
|
-
const counts = countLines(source);
|
|
23082
|
-
const classes = countClassesQuick(source);
|
|
23083
|
-
const functions = countFunctionsQuick(source);
|
|
23084
|
-
totalLoc += counts.loc;
|
|
23085
|
-
totalBlank += counts.blank;
|
|
23086
|
-
totalComment += counts.comment;
|
|
23087
|
-
totalClasses += classes;
|
|
23088
|
-
totalFunctions += functions;
|
|
23089
|
-
fileDetails.push({
|
|
23090
|
-
path: relativePath(f, rootPath),
|
|
23091
|
-
loc: counts.loc,
|
|
23092
|
-
blank: counts.blank,
|
|
23093
|
-
comment: counts.comment,
|
|
23094
|
-
classes,
|
|
23095
|
-
functions
|
|
23096
|
-
});
|
|
22881
|
+
function containsTypeScript(directory) {
|
|
22882
|
+
if (!fs3.existsSync(directory) || !fs3.statSync(directory).isDirectory()) return false;
|
|
22883
|
+
for (const entry of fs3.readdirSync(directory, { withFileTypes: true })) {
|
|
22884
|
+
if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
|
|
22885
|
+
const target = path2.join(directory, entry.name);
|
|
22886
|
+
if (entry.isDirectory() ? containsTypeScript(target) : /\.[cm]?[jt]sx?$/.test(entry.name)) return true;
|
|
23097
22887
|
}
|
|
23098
|
-
|
|
23099
|
-
let routeCount = 0;
|
|
23100
|
-
let ormCount = 0;
|
|
23101
|
-
for (const f of tsFiles) {
|
|
23102
|
-
const source = readFileSafe(f);
|
|
23103
|
-
if (source === null) continue;
|
|
23104
|
-
const routes = source.match(
|
|
23105
|
-
/(?:router\s*\.\s*(?:get|post|put|delete|patch|any)\s*\(|@(?:get|post|put|delete|patch)\s*\()/g
|
|
23106
|
-
);
|
|
23107
|
-
if (routes) routeCount += routes.length;
|
|
23108
|
-
const orms = source.match(
|
|
23109
|
-
/class\s+\w+\s+extends\s+(?:ORM|Model)\b/g
|
|
23110
|
-
);
|
|
23111
|
-
if (orms) ormCount += orms.length;
|
|
23112
|
-
}
|
|
23113
|
-
const breakdown = {
|
|
23114
|
-
typescript: tsFiles.filter((f) => f.endsWith(".ts")).length,
|
|
23115
|
-
javascript: tsFiles.filter((f) => f.endsWith(".js")).length,
|
|
23116
|
-
templates: twigFiles.length,
|
|
23117
|
-
migrations: migrationFiles.length,
|
|
23118
|
-
stylesheets: scssFiles.length
|
|
23119
|
-
};
|
|
23120
|
-
return {
|
|
23121
|
-
file_count: tsFiles.length,
|
|
23122
|
-
total_loc: totalLoc,
|
|
23123
|
-
total_blank: totalBlank,
|
|
23124
|
-
total_comment: totalComment,
|
|
23125
|
-
lloc: totalLoc,
|
|
23126
|
-
classes: totalClasses,
|
|
23127
|
-
functions: totalFunctions,
|
|
23128
|
-
route_count: routeCount,
|
|
23129
|
-
orm_count: ormCount,
|
|
23130
|
-
template_count: twigFiles.length,
|
|
23131
|
-
migration_count: migrationFiles.length,
|
|
23132
|
-
avg_file_size: tsFiles.length > 0 ? Math.round(totalLoc / tsFiles.length * 10) / 10 : 0,
|
|
23133
|
-
largest_files: fileDetails.slice(0, 10),
|
|
23134
|
-
breakdown
|
|
23135
|
-
};
|
|
22888
|
+
return false;
|
|
23136
22889
|
}
|
|
23137
|
-
function
|
|
23138
|
-
const resolved =
|
|
23139
|
-
const
|
|
23140
|
-
|
|
23141
|
-
|
|
23142
|
-
return [resolved, scanningFramework ? "framework" : "project"];
|
|
22890
|
+
function resolveTarget(root = "src") {
|
|
22891
|
+
const resolved = containsTypeScript(root) ? path2.resolve(root) : path2.dirname(fileURLToPath2(import.meta.url));
|
|
22892
|
+
const mode = containsTypeScript(root) ? "project" : "framework";
|
|
22893
|
+
lastScanRoot = resolved;
|
|
22894
|
+
return [resolved, mode];
|
|
23143
22895
|
}
|
|
23144
22896
|
function enginePath() {
|
|
23145
|
-
const names = process.platform === "win32" ? ["tina4.exe", "tina4
|
|
23146
|
-
for (const
|
|
23147
|
-
if (!dir) continue;
|
|
22897
|
+
const names = process.platform === "win32" ? ["tina4.exe", "tina4"] : ["tina4"];
|
|
22898
|
+
for (const directory of (process.env.PATH || "").split(path2.delimiter)) {
|
|
23148
22899
|
for (const name of names) {
|
|
23149
|
-
const candidate = path2.join(
|
|
22900
|
+
const candidate = path2.join(directory, name);
|
|
23150
22901
|
try {
|
|
23151
|
-
if (!fs3.statSync(candidate).isFile()) continue;
|
|
23152
22902
|
fs3.accessSync(candidate, fs3.constants.X_OK);
|
|
22903
|
+
if (!fs3.statSync(candidate).isFile()) continue;
|
|
22904
|
+
const descriptor = fs3.openSync(candidate, "r");
|
|
22905
|
+
const header = Buffer.alloc(2);
|
|
22906
|
+
fs3.readSync(descriptor, header, 0, 2, 0);
|
|
22907
|
+
fs3.closeSync(descriptor);
|
|
22908
|
+
if (header.toString("latin1") !== "#!") return candidate;
|
|
23153
22909
|
} catch {
|
|
23154
22910
|
continue;
|
|
23155
22911
|
}
|
|
23156
|
-
try {
|
|
23157
|
-
const fd = fs3.openSync(candidate, "r");
|
|
23158
|
-
const buf = Buffer.alloc(2);
|
|
23159
|
-
fs3.readSync(fd, buf, 0, 2, 0);
|
|
23160
|
-
fs3.closeSync(fd);
|
|
23161
|
-
if (buf.toString("latin1") === "#!") continue;
|
|
23162
|
-
} catch {
|
|
23163
|
-
}
|
|
23164
|
-
return candidate;
|
|
23165
22912
|
}
|
|
23166
22913
|
}
|
|
23167
22914
|
return null;
|
|
23168
22915
|
}
|
|
23169
22916
|
function runEngine(target) {
|
|
23170
22917
|
const binary = enginePath();
|
|
23171
|
-
if (binary
|
|
23172
|
-
|
|
23173
|
-
}
|
|
23174
|
-
const proc = spawnSync(binary, ["metrics", "--path", target, "--json"], {
|
|
22918
|
+
if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
|
|
22919
|
+
const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
|
|
23175
22920
|
encoding: "utf8",
|
|
23176
|
-
timeout:
|
|
22921
|
+
timeout: 6e4,
|
|
23177
22922
|
maxBuffer: 64 * 1024 * 1024
|
|
23178
22923
|
});
|
|
23179
|
-
if (
|
|
23180
|
-
|
|
23181
|
-
if (err.code === "ETIMEDOUT") {
|
|
23182
|
-
throw new MetricsEngineError(`tina4 metrics timed out after ${TIMEOUT_MS / 1e3}s on ${target}`);
|
|
23183
|
-
}
|
|
23184
|
-
throw new MetricsEngineError(`could not run ${binary}: ${err.message}`);
|
|
23185
|
-
}
|
|
23186
|
-
if (proc.status !== 0) {
|
|
23187
|
-
const detail = (proc.stderr || proc.stdout || "").trim().split("\n")[0];
|
|
23188
|
-
throw new MetricsEngineError(
|
|
23189
|
-
`tina4 metrics failed on ${target}: ${detail || `exit code ${proc.status}`}`
|
|
23190
|
-
);
|
|
22924
|
+
if (processResult.error) {
|
|
22925
|
+
throw new MetricsEngineError(`could not run ${binary}: ${processResult.error.message}`);
|
|
23191
22926
|
}
|
|
23192
|
-
if (
|
|
23193
|
-
|
|
22927
|
+
if (processResult.status !== 0) {
|
|
22928
|
+
const detail = (processResult.stderr || processResult.stdout || "").trim().split("\n")[0];
|
|
22929
|
+
throw new MetricsEngineError(`tina4 metrics failed on ${target}: ${detail || processResult.status}`);
|
|
23194
22930
|
}
|
|
23195
|
-
let payload;
|
|
23196
22931
|
try {
|
|
23197
|
-
payload = JSON.parse(
|
|
23198
|
-
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
|
|
22932
|
+
const payload = JSON.parse(processResult.stdout);
|
|
22933
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
22934
|
+
throw new Error("non-object payload");
|
|
22935
|
+
}
|
|
22936
|
+
return payload;
|
|
22937
|
+
} catch (error) {
|
|
22938
|
+
throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${error.message}`);
|
|
23203
22939
|
}
|
|
23204
|
-
return payload;
|
|
23205
22940
|
}
|
|
23206
|
-
function
|
|
23207
|
-
|
|
23208
|
-
|
|
23209
|
-
if (!ok) {
|
|
23210
|
-
throw new MetricsEngineError(
|
|
23211
|
-
`engine payload has no usable '${key}' - the installed tina4 CLI predates a field the dashboard renders. Update it: ${INSTALL_HINT}`
|
|
23212
|
-
);
|
|
22941
|
+
function requireArray(payload, key) {
|
|
22942
|
+
if (!Array.isArray(payload[key])) {
|
|
22943
|
+
throw new MetricsEngineError(`engine payload has no usable '${key}' - ${INSTALL_HINT}`);
|
|
23213
22944
|
}
|
|
23214
|
-
return
|
|
22945
|
+
return payload[key];
|
|
23215
22946
|
}
|
|
23216
22947
|
function fullAnalysis(root = "src") {
|
|
23217
|
-
const [resolved, scanMode] =
|
|
22948
|
+
const [resolved, scanMode] = resolveTarget(root);
|
|
23218
22949
|
const payload = runEngine(resolved);
|
|
23219
|
-
const summary =
|
|
23220
|
-
|
|
23221
|
-
|
|
23222
|
-
|
|
23223
|
-
|
|
23224
|
-
|
|
23225
|
-
|
|
23226
|
-
|
|
23227
|
-
|
|
23228
|
-
if (
|
|
23229
|
-
|
|
23230
|
-
|
|
22950
|
+
const summary = payload.summary;
|
|
22951
|
+
if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
|
|
22952
|
+
throw new MetricsEngineError(`engine payload has no usable 'summary' - ${INSTALL_HINT}`);
|
|
22953
|
+
}
|
|
22954
|
+
const fileMetrics = requireArray(payload, "file_metrics");
|
|
22955
|
+
const functions = requireArray(payload, "most_complex_functions");
|
|
22956
|
+
const missingSummary = SUMMARY_KEYS.filter((key) => !(key in summary));
|
|
22957
|
+
if (missingSummary.length) throw new MetricsEngineError(`engine summary is missing ${missingSummary.join(", ")}`);
|
|
22958
|
+
const missingFile = fileMetrics.length ? FILE_KEYS.filter((key) => !(key in fileMetrics[0])) : [];
|
|
22959
|
+
if (missingFile.length) throw new MetricsEngineError(`engine file_metrics is missing ${missingFile.join(", ")}`);
|
|
22960
|
+
const missingFunction = functions.length ? FUNCTION_KEYS.filter((key) => !(key in functions[0])) : [];
|
|
22961
|
+
if (missingFunction.length) {
|
|
22962
|
+
throw new MetricsEngineError(`engine function metrics are missing ${missingFunction.join(", ")}`);
|
|
23231
22963
|
}
|
|
23232
|
-
|
|
23233
|
-
|
|
23234
|
-
|
|
23235
|
-
|
|
23236
|
-
|
|
23237
|
-
|
|
23238
|
-
|
|
23239
|
-
|
|
23240
|
-
|
|
23241
|
-
result.scan_mode = scanMode;
|
|
23242
|
-
result.scan_root = path2.resolve(resolved);
|
|
23243
|
-
result.engine = "tina4-cli";
|
|
23244
|
-
return result;
|
|
23245
|
-
}
|
|
23246
|
-
function offenders(root = "src", top = 20) {
|
|
23247
|
-
const [resolved, scanMode] = resolveScanTarget(root);
|
|
23248
|
-
const payload = runEngine(resolved);
|
|
23249
|
-
const found = requireKey(payload, "offenders", true);
|
|
23250
|
-
const summary = { ...requireKey(payload, "summary", false) };
|
|
23251
|
-
summary.scan_mode = scanMode;
|
|
23252
|
-
summary.scan_root = path2.resolve(resolved);
|
|
23253
|
-
summary.engine = "tina4-cli";
|
|
23254
|
-
if (summary.total_offenders === void 0) summary.total_offenders = found.length;
|
|
23255
|
-
return { offenders: found.slice(0, top), summary };
|
|
22964
|
+
return {
|
|
22965
|
+
...Object.fromEntries(SUMMARY_KEYS.map((key) => [key, summary[key]])),
|
|
22966
|
+
file_metrics: fileMetrics,
|
|
22967
|
+
most_complex_functions: functions.slice(0, 15),
|
|
22968
|
+
dependency_graph: payload.dependency_graph || {},
|
|
22969
|
+
scan_mode: scanMode,
|
|
22970
|
+
scan_root: resolved,
|
|
22971
|
+
engine: "tina4-cli"
|
|
22972
|
+
};
|
|
23256
22973
|
}
|
|
23257
22974
|
function fileDetail(filePath) {
|
|
23258
22975
|
if (!filePath) throw new MetricsEngineError("fileDetail needs a path");
|
|
23259
22976
|
let target = filePath;
|
|
23260
|
-
if (!fs3.existsSync(target) &&
|
|
23261
|
-
const candidate = path2.join(_lastScanRoot, filePath);
|
|
23262
|
-
if (fs3.existsSync(candidate)) target = candidate;
|
|
23263
|
-
}
|
|
22977
|
+
if (!fs3.existsSync(target) && lastScanRoot) target = path2.join(lastScanRoot, filePath);
|
|
23264
22978
|
if (!fs3.existsSync(target)) throw new MetricsEngineError(`no such file: ${filePath}`);
|
|
23265
22979
|
if (fs3.statSync(target).isDirectory()) throw new MetricsEngineError(`not a file: ${filePath}`);
|
|
23266
22980
|
const payload = runEngine(target);
|
|
23267
|
-
const
|
|
23268
|
-
if (!
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
|
|
22981
|
+
const files = requireArray(payload, "file_metrics");
|
|
22982
|
+
if (!files.length) throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
|
|
22983
|
+
return {
|
|
22984
|
+
...files[0],
|
|
22985
|
+
function_count: files[0].functions || 0,
|
|
22986
|
+
functions: requireArray(payload, "most_complex_functions"),
|
|
22987
|
+
engine: "tina4-cli"
|
|
22988
|
+
};
|
|
23272
22989
|
}
|
|
23273
|
-
var
|
|
22990
|
+
var lastScanRoot, MetricsEngineError, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
|
|
23274
22991
|
var init_metrics = __esm({
|
|
23275
22992
|
"../core/src/metrics.ts"() {
|
|
23276
22993
|
"use strict";
|
|
23277
|
-
|
|
22994
|
+
lastScanRoot = "";
|
|
23278
22995
|
MetricsEngineError = class extends Error {
|
|
23279
22996
|
constructor(message) {
|
|
23280
22997
|
super(message);
|
|
23281
22998
|
this.name = "MetricsEngineError";
|
|
23282
22999
|
}
|
|
23283
23000
|
};
|
|
23284
|
-
|
|
23285
|
-
INSTALL_HINT = [
|
|
23286
|
-
"the tina4 CLI provides the metrics engine (ADR-0002). Install it with",
|
|
23287
|
-
" curl -fsSL https://tina4.com/install.sh | sh",
|
|
23288
|
-
"or see https://tina4.com/cli"
|
|
23289
|
-
].join("\n");
|
|
23001
|
+
INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
|
|
23290
23002
|
SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
|
|
23291
23003
|
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_tests"];
|
|
23292
23004
|
FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
|
|
@@ -23294,7 +23006,7 @@ var init_metrics = __esm({
|
|
|
23294
23006
|
});
|
|
23295
23007
|
|
|
23296
23008
|
// ../core/src/feedback.ts
|
|
23297
|
-
import { readFileSync as
|
|
23009
|
+
import { readFileSync as readFileSync12, existsSync as existsSync14 } from "node:fs";
|
|
23298
23010
|
import { dirname as dirname8, join as join19, resolve as resolve10 } from "node:path";
|
|
23299
23011
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
23300
23012
|
function feedbackEnabled() {
|
|
@@ -23435,7 +23147,7 @@ var init_feedback = __esm({
|
|
|
23435
23147
|
handleFeedbackWidgetJs = (_req, res) => {
|
|
23436
23148
|
let body;
|
|
23437
23149
|
if (existsSync14(WIDGET_BUNDLE_PATH)) {
|
|
23438
|
-
body =
|
|
23150
|
+
body = readFileSync12(WIDGET_BUNDLE_PATH);
|
|
23439
23151
|
} else {
|
|
23440
23152
|
body = "console.warn('tina4-feedback-widget bundle not built yet');";
|
|
23441
23153
|
}
|
|
@@ -23450,7 +23162,7 @@ var init_feedback = __esm({
|
|
|
23450
23162
|
});
|
|
23451
23163
|
|
|
23452
23164
|
// ../core/src/version.ts
|
|
23453
|
-
import { existsSync as existsSync15, readFileSync as
|
|
23165
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
|
|
23454
23166
|
import { dirname as dirname9, join as join20 } from "node:path";
|
|
23455
23167
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
23456
23168
|
function resolveFrameworkVersion() {
|
|
@@ -23459,7 +23171,7 @@ function resolveFrameworkVersion() {
|
|
|
23459
23171
|
const pkgPath = join20(dir, "package.json");
|
|
23460
23172
|
if (existsSync15(pkgPath)) {
|
|
23461
23173
|
try {
|
|
23462
|
-
const pkg = JSON.parse(
|
|
23174
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
23463
23175
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
23464
23176
|
} catch {
|
|
23465
23177
|
}
|
|
@@ -26247,8 +25959,8 @@ __export(context_exports, {
|
|
|
26247
25959
|
fts5Supported: () => fts5Supported
|
|
26248
25960
|
});
|
|
26249
25961
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
26250
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as
|
|
26251
|
-
import { basename as basename5, dirname as dirname11, extname as
|
|
25962
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync15, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
|
|
25963
|
+
import { basename as basename5, dirname as dirname11, extname as extname5, isAbsolute as isAbsolute6, join as join22, relative as relative3, resolve as resolve12 } from "node:path";
|
|
26252
25964
|
function fts5Supported() {
|
|
26253
25965
|
try {
|
|
26254
25966
|
const conn = new DatabaseSync4(":memory:");
|
|
@@ -26383,7 +26095,7 @@ var init_context = __esm({
|
|
|
26383
26095
|
}
|
|
26384
26096
|
// ── indexing ───────────────────────────────────────────────
|
|
26385
26097
|
static chunksFor(label, text) {
|
|
26386
|
-
const ext =
|
|
26098
|
+
const ext = extname5(label).toLowerCase();
|
|
26387
26099
|
const special = SPECIAL_FILES.has(basename5(label).toLowerCase());
|
|
26388
26100
|
if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
|
|
26389
26101
|
return chunkCode(text, label);
|
|
@@ -26401,7 +26113,7 @@ var init_context = __esm({
|
|
|
26401
26113
|
const stored = label != null ? String(label) : String(file);
|
|
26402
26114
|
let text;
|
|
26403
26115
|
try {
|
|
26404
|
-
text =
|
|
26116
|
+
text = readFileSync15(file, "utf-8");
|
|
26405
26117
|
} catch {
|
|
26406
26118
|
return 0;
|
|
26407
26119
|
}
|
|
@@ -26422,7 +26134,7 @@ var init_context = __esm({
|
|
|
26422
26134
|
static eligible(filename) {
|
|
26423
26135
|
const fn = filename.toLowerCase();
|
|
26424
26136
|
if (fn.endsWith(".min.js")) return false;
|
|
26425
|
-
const ext =
|
|
26137
|
+
const ext = extname5(fn);
|
|
26426
26138
|
return CODE_EXTS.has(ext) || DOC_EXTS.has(ext) || CONFIG_EXTS.has(ext) || SPECIAL_FILES.has(fn);
|
|
26427
26139
|
}
|
|
26428
26140
|
/**
|
|
@@ -26451,7 +26163,7 @@ var init_context = __esm({
|
|
|
26451
26163
|
for (const fn of files) {
|
|
26452
26164
|
if (!_Context.eligible(fn)) continue;
|
|
26453
26165
|
const full = join22(dir, fn);
|
|
26454
|
-
const rel =
|
|
26166
|
+
const rel = relative3(rootAbs, full);
|
|
26455
26167
|
total += this.indexPath(full, rel);
|
|
26456
26168
|
}
|
|
26457
26169
|
for (const d of subdirs) walk2(join22(dir, d));
|
|
@@ -26472,7 +26184,7 @@ var init_context = __esm({
|
|
|
26472
26184
|
const raw = String(changedPath);
|
|
26473
26185
|
const abs = isAbsolute6(raw) ? raw : join22(process.cwd(), raw);
|
|
26474
26186
|
const resolved = realResolve(resolve12(abs));
|
|
26475
|
-
const rel =
|
|
26187
|
+
const rel = relative3(this.root, resolved);
|
|
26476
26188
|
if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) {
|
|
26477
26189
|
return -1;
|
|
26478
26190
|
}
|
|
@@ -28394,7 +28106,7 @@ var init_job = __esm({
|
|
|
28394
28106
|
});
|
|
28395
28107
|
|
|
28396
28108
|
// ../core/src/queueBackends/liteBackend.ts
|
|
28397
|
-
import { mkdirSync as mkdirSync15, readdirSync as readdirSync11, readFileSync as
|
|
28109
|
+
import { mkdirSync as mkdirSync15, readdirSync as readdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7, existsSync as existsSync18 } from "node:fs";
|
|
28398
28110
|
import { join as join23 } from "node:path";
|
|
28399
28111
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
28400
28112
|
var LiteBackend;
|
|
@@ -28494,7 +28206,7 @@ var init_liteBackend = __esm({
|
|
|
28494
28206
|
const filePath = join23(dir, filename);
|
|
28495
28207
|
let job;
|
|
28496
28208
|
try {
|
|
28497
|
-
job = JSON.parse(
|
|
28209
|
+
job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28498
28210
|
} catch {
|
|
28499
28211
|
continue;
|
|
28500
28212
|
}
|
|
@@ -28558,7 +28270,7 @@ var init_liteBackend = __esm({
|
|
|
28558
28270
|
const filePath = join23(reservedDir, filename);
|
|
28559
28271
|
let record;
|
|
28560
28272
|
try {
|
|
28561
|
-
record = JSON.parse(
|
|
28273
|
+
record = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28562
28274
|
} catch {
|
|
28563
28275
|
continue;
|
|
28564
28276
|
}
|
|
@@ -28671,7 +28383,7 @@ var init_liteBackend = __esm({
|
|
|
28671
28383
|
let count = 0;
|
|
28672
28384
|
for (const file of files) {
|
|
28673
28385
|
try {
|
|
28674
|
-
const job = JSON.parse(
|
|
28386
|
+
const job = JSON.parse(readFileSync16(join23(scanDir, file), "utf-8"));
|
|
28675
28387
|
if (job.status === status2) count++;
|
|
28676
28388
|
} catch {
|
|
28677
28389
|
}
|
|
@@ -28728,7 +28440,7 @@ var init_liteBackend = __esm({
|
|
|
28728
28440
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
28729
28441
|
for (const file of files) {
|
|
28730
28442
|
try {
|
|
28731
|
-
const job = JSON.parse(
|
|
28443
|
+
const job = JSON.parse(readFileSync16(join23(dir, file), "utf-8"));
|
|
28732
28444
|
const attempts = job.attempts || 0;
|
|
28733
28445
|
if (attempts > 0 && attempts < maxRetries) {
|
|
28734
28446
|
results.push(job);
|
|
@@ -28754,7 +28466,7 @@ var init_liteBackend = __esm({
|
|
|
28754
28466
|
const failedDir = join23(this.basePath, q, "failed");
|
|
28755
28467
|
const filePath = join23(failedDir, `${jobId}.queue-data`);
|
|
28756
28468
|
if (existsSync18(filePath)) {
|
|
28757
|
-
const job = JSON.parse(
|
|
28469
|
+
const job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28758
28470
|
job.status = "pending";
|
|
28759
28471
|
job.attempts = (job.attempts || 0) + 1;
|
|
28760
28472
|
job.error = void 0;
|
|
@@ -28778,7 +28490,7 @@ var init_liteBackend = __esm({
|
|
|
28778
28490
|
const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
28779
28491
|
for (const file of files) {
|
|
28780
28492
|
try {
|
|
28781
|
-
const job = JSON.parse(
|
|
28493
|
+
const job = JSON.parse(readFileSync16(join23(failedDir, file), "utf-8"));
|
|
28782
28494
|
if ((job.attempts || 0) >= maxRetries) {
|
|
28783
28495
|
job.status = "dead";
|
|
28784
28496
|
results.push(job);
|
|
@@ -28812,7 +28524,7 @@ var init_liteBackend = __esm({
|
|
|
28812
28524
|
const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
|
|
28813
28525
|
for (const file of files) {
|
|
28814
28526
|
try {
|
|
28815
|
-
const job = JSON.parse(
|
|
28527
|
+
const job = JSON.parse(readFileSync16(join23(dir, file), "utf-8"));
|
|
28816
28528
|
if (job.status === status2) {
|
|
28817
28529
|
unlinkSync7(join23(dir, file));
|
|
28818
28530
|
count++;
|
|
@@ -28839,7 +28551,7 @@ var init_liteBackend = __esm({
|
|
|
28839
28551
|
for (const file of files) {
|
|
28840
28552
|
try {
|
|
28841
28553
|
const filePath = join23(failedDir, file);
|
|
28842
|
-
const job = JSON.parse(
|
|
28554
|
+
const job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28843
28555
|
if ((job.attempts || 0) >= maxRetries) {
|
|
28844
28556
|
continue;
|
|
28845
28557
|
}
|
|
@@ -28871,7 +28583,7 @@ var init_liteBackend = __esm({
|
|
|
28871
28583
|
const filePath = join23(dir, file);
|
|
28872
28584
|
let job;
|
|
28873
28585
|
try {
|
|
28874
|
-
job = JSON.parse(
|
|
28586
|
+
job = JSON.parse(readFileSync16(filePath, "utf-8"));
|
|
28875
28587
|
} catch {
|
|
28876
28588
|
continue;
|
|
28877
28589
|
}
|
|
@@ -30334,7 +30046,7 @@ function detectVersion(projectRoot3) {
|
|
|
30334
30046
|
}
|
|
30335
30047
|
return "0.0.0";
|
|
30336
30048
|
}
|
|
30337
|
-
function
|
|
30049
|
+
function relativePath(absPath, projectRoot3, frameworkRoots) {
|
|
30338
30050
|
const norm = path6.resolve(absPath);
|
|
30339
30051
|
for (const fw of frameworkRoots) {
|
|
30340
30052
|
const parent = path6.dirname(fw);
|
|
@@ -30799,7 +30511,7 @@ function buildEntriesForFile(absPath, source, fwRoots, projectRoot3, version, ou
|
|
|
30799
30511
|
} catch {
|
|
30800
30512
|
return;
|
|
30801
30513
|
}
|
|
30802
|
-
const rel =
|
|
30514
|
+
const rel = relativePath(absPath, projectRoot3, fwRoots);
|
|
30803
30515
|
for (const cls of parsed.classes) {
|
|
30804
30516
|
if (!cls.exported && source === "framework") {
|
|
30805
30517
|
continue;
|
|
@@ -31435,8 +31147,8 @@ ${end}
|
|
|
31435
31147
|
|
|
31436
31148
|
// ../core/src/devAdmin.ts
|
|
31437
31149
|
import { cpus as osCpus } from "node:os";
|
|
31438
|
-
import { readFileSync as
|
|
31439
|
-
import { join as join27, dirname as dirname13, resolve as resolve16, relative as
|
|
31150
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync15, existsSync as existsSync22, readdirSync as readdirSync15, mkdirSync as mkdirSync18, copyFileSync as copyFileSync2, statSync as statSync16 } from "node:fs";
|
|
31151
|
+
import { join as join27, dirname as dirname13, resolve as resolve16, relative as relative7 } from "node:path";
|
|
31440
31152
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
31441
31153
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
31442
31154
|
function escapeHtml(value) {
|
|
@@ -31555,7 +31267,7 @@ function readQueueDir(dir, topic, status2) {
|
|
|
31555
31267
|
for (const filename of readdirSync15(dir).sort()) {
|
|
31556
31268
|
if (!filename.endsWith(".queue-data")) continue;
|
|
31557
31269
|
try {
|
|
31558
|
-
jobs.push(mapQueueJob(JSON.parse(
|
|
31270
|
+
jobs.push(mapQueueJob(JSON.parse(readFileSync20(join27(dir, filename), "utf-8")), topic, status2));
|
|
31559
31271
|
} catch {
|
|
31560
31272
|
}
|
|
31561
31273
|
}
|
|
@@ -31674,7 +31386,7 @@ function resolveDevEnvVar(key) {
|
|
|
31674
31386
|
if (live !== void 0 && live !== "") return live;
|
|
31675
31387
|
const envPath = join27(process.cwd(), ".env");
|
|
31676
31388
|
if (!existsSync22(envPath)) return "";
|
|
31677
|
-
for (const line of
|
|
31389
|
+
for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
|
|
31678
31390
|
const t = line.trim();
|
|
31679
31391
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
31680
31392
|
const eq = t.indexOf("=");
|
|
@@ -31684,7 +31396,7 @@ function resolveDevEnvVar(key) {
|
|
|
31684
31396
|
}
|
|
31685
31397
|
function upsertDevEnvVar(key, value) {
|
|
31686
31398
|
const envPath = join27(process.cwd(), ".env");
|
|
31687
|
-
const lines = existsSync22(envPath) ?
|
|
31399
|
+
const lines = existsSync22(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
|
|
31688
31400
|
let found = false;
|
|
31689
31401
|
const out = [];
|
|
31690
31402
|
for (const line of lines) {
|
|
@@ -31717,7 +31429,7 @@ function parseEnvFile() {
|
|
|
31717
31429
|
const envPath = join27(process.cwd(), ".env");
|
|
31718
31430
|
const result = {};
|
|
31719
31431
|
if (!existsSync22(envPath)) return result;
|
|
31720
|
-
const lines =
|
|
31432
|
+
const lines = readFileSync20(envPath, "utf-8").split("\n");
|
|
31721
31433
|
for (const line of lines) {
|
|
31722
31434
|
const trimmed = line.trim();
|
|
31723
31435
|
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -31757,7 +31469,7 @@ function handleGalleryDeploy(router) {
|
|
|
31757
31469
|
const copied = [];
|
|
31758
31470
|
const allFiles = walkDirRecursive(gallerySrc);
|
|
31759
31471
|
for (const srcFile of allFiles) {
|
|
31760
|
-
const rel =
|
|
31472
|
+
const rel = relative7(gallerySrc, srcFile);
|
|
31761
31473
|
const dest = join27(projectSrc, rel);
|
|
31762
31474
|
mkdirSync18(dirname13(dest), { recursive: true });
|
|
31763
31475
|
copyFileSync2(srcFile, dest);
|
|
@@ -32419,9 +32131,6 @@ var init_devAdmin = __esm({
|
|
|
32419
32131
|
{ method: "GET", pattern: "/__dev/api/gallery", handler: handleGalleryList },
|
|
32420
32132
|
{ method: "POST", pattern: "/__dev/api/gallery/deploy", handler: handleGalleryDeploy(router) },
|
|
32421
32133
|
// Metrics
|
|
32422
|
-
{ method: "GET", pattern: "/__dev/api/metrics", handler: (_req, res) => {
|
|
32423
|
-
res.json(quickMetrics());
|
|
32424
|
-
} },
|
|
32425
32134
|
// No fallback (ADR-0002): a missing or stale CLI is a 503 naming the
|
|
32426
32135
|
// install command, never zeros that read as a healthy codebase.
|
|
32427
32136
|
{ method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req, res) => {
|
|
@@ -33230,7 +32939,7 @@ var init_devAdmin = __esm({
|
|
|
33230
32939
|
}
|
|
33231
32940
|
try {
|
|
33232
32941
|
const envPath = join27(process.cwd(), ".env");
|
|
33233
|
-
const lines = existsSync22(envPath) ?
|
|
32942
|
+
const lines = existsSync22(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
|
|
33234
32943
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
33235
32944
|
const newLines = [];
|
|
33236
32945
|
for (const line of lines) {
|
|
@@ -33276,12 +32985,12 @@ var init_devAdmin = __esm({
|
|
|
33276
32985
|
const metaFile = join27(entryPath, "meta.json");
|
|
33277
32986
|
if (statSync16(entryPath).isDirectory() && existsSync22(metaFile)) {
|
|
33278
32987
|
try {
|
|
33279
|
-
const meta = JSON.parse(
|
|
32988
|
+
const meta = JSON.parse(readFileSync20(metaFile, "utf-8"));
|
|
33280
32989
|
meta.id = entry;
|
|
33281
32990
|
const srcDir = join27(entryPath, "src");
|
|
33282
32991
|
if (existsSync22(srcDir)) {
|
|
33283
32992
|
const allFiles = walkDirRecursive(srcDir);
|
|
33284
|
-
meta.files = allFiles.map((f) =>
|
|
32993
|
+
meta.files = allFiles.map((f) => relative7(srcDir, f));
|
|
33285
32994
|
}
|
|
33286
32995
|
const projectSrc = resolve16(process.cwd(), "src");
|
|
33287
32996
|
if (existsSync22(srcDir) && meta.files) {
|
|
@@ -33399,7 +33108,7 @@ var init_devAdmin = __esm({
|
|
|
33399
33108
|
for (const name of readdirSync15(target).sort()) {
|
|
33400
33109
|
if (devFilesHidden(name)) continue;
|
|
33401
33110
|
const full = join27(target, name);
|
|
33402
|
-
const entryRel =
|
|
33111
|
+
const entryRel = relative7(root, full).replace(/\\/g, "/");
|
|
33403
33112
|
if (isSecretPath(entryRel)) continue;
|
|
33404
33113
|
let isDir = false;
|
|
33405
33114
|
let size = null;
|
|
@@ -33444,7 +33153,7 @@ var init_devAdmin = __esm({
|
|
|
33444
33153
|
size
|
|
33445
33154
|
});
|
|
33446
33155
|
}
|
|
33447
|
-
res.json({ path:
|
|
33156
|
+
res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
|
|
33448
33157
|
};
|
|
33449
33158
|
DEV_ADMIN_LANG_MAP = {
|
|
33450
33159
|
".py": "python",
|
|
@@ -33496,8 +33205,8 @@ var init_devAdmin = __esm({
|
|
|
33496
33205
|
return;
|
|
33497
33206
|
}
|
|
33498
33207
|
try {
|
|
33499
|
-
const content =
|
|
33500
|
-
const path8 =
|
|
33208
|
+
const content = readFileSync20(target, "utf-8");
|
|
33209
|
+
const path8 = relative7(root, target);
|
|
33501
33210
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
33502
33211
|
} catch (e) {
|
|
33503
33212
|
res.json({ error: e.message }, 500);
|
|
@@ -33519,10 +33228,10 @@ var init_devAdmin = __esm({
|
|
|
33519
33228
|
writeFileSync15(target, content, "utf-8");
|
|
33520
33229
|
try {
|
|
33521
33230
|
const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
33522
|
-
Plan2.recordAction(existed ? "patched" : "created",
|
|
33231
|
+
Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
|
|
33523
33232
|
} catch {
|
|
33524
33233
|
}
|
|
33525
|
-
res.json({ ok: true, path:
|
|
33234
|
+
res.json({ ok: true, path: relative7(root, target), bytes: Buffer.byteLength(content, "utf-8") });
|
|
33526
33235
|
} catch (e) {
|
|
33527
33236
|
res.json({ error: e.message }, 500);
|
|
33528
33237
|
}
|
|
@@ -33542,7 +33251,7 @@ var init_devAdmin = __esm({
|
|
|
33542
33251
|
return;
|
|
33543
33252
|
}
|
|
33544
33253
|
try {
|
|
33545
|
-
const buf =
|
|
33254
|
+
const buf = readFileSync20(target);
|
|
33546
33255
|
const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
33547
33256
|
const mime = {
|
|
33548
33257
|
js: "application/javascript",
|
|
@@ -33584,7 +33293,7 @@ var init_devAdmin = __esm({
|
|
|
33584
33293
|
const { renameSync: renameSync3 } = await import("node:fs");
|
|
33585
33294
|
mkdirSync18(dirname13(dst), { recursive: true });
|
|
33586
33295
|
renameSync3(src, dst);
|
|
33587
|
-
res.json({ ok: true, from:
|
|
33296
|
+
res.json({ ok: true, from: relative7(root, src), to: relative7(root, dst) });
|
|
33588
33297
|
} catch (e) {
|
|
33589
33298
|
res.json({ error: e.message }, 500);
|
|
33590
33299
|
}
|
|
@@ -33605,7 +33314,7 @@ var init_devAdmin = __esm({
|
|
|
33605
33314
|
try {
|
|
33606
33315
|
const { rmSync } = await import("node:fs");
|
|
33607
33316
|
rmSync(target, { recursive: true, force: true });
|
|
33608
|
-
res.json({ ok: true, deleted:
|
|
33317
|
+
res.json({ ok: true, deleted: relative7(root, target) });
|
|
33609
33318
|
} catch (e) {
|
|
33610
33319
|
res.json({ error: e.message }, 500);
|
|
33611
33320
|
}
|
|
@@ -33939,7 +33648,7 @@ var init_devAdmin = __esm({
|
|
|
33939
33648
|
});
|
|
33940
33649
|
};
|
|
33941
33650
|
handleDevAdminJs = async (_req, res) => {
|
|
33942
|
-
const { readFileSync:
|
|
33651
|
+
const { readFileSync: readFileSync29, existsSync: existsSync37 } = await import("node:fs");
|
|
33943
33652
|
const { dirname: dirname17, join: join40, resolve: resolve30 } = await import("node:path");
|
|
33944
33653
|
const { fileURLToPath: fileURLToPath10 } = await import("node:url");
|
|
33945
33654
|
const dir = dirname17(fileURLToPath10(import.meta.url));
|
|
@@ -33955,7 +33664,7 @@ var init_devAdmin = __esm({
|
|
|
33955
33664
|
for (const jsPath of candidates) {
|
|
33956
33665
|
if (existsSync37(jsPath)) {
|
|
33957
33666
|
try {
|
|
33958
|
-
const content =
|
|
33667
|
+
const content = readFileSync29(jsPath, "utf-8");
|
|
33959
33668
|
res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
33960
33669
|
res.raw.end(content);
|
|
33961
33670
|
return;
|
|
@@ -33970,7 +33679,7 @@ var init_devAdmin = __esm({
|
|
|
33970
33679
|
});
|
|
33971
33680
|
|
|
33972
33681
|
// ../core/src/i18n.ts
|
|
33973
|
-
import { readFileSync as
|
|
33682
|
+
import { readFileSync as readFileSync21, readdirSync as readdirSync16, existsSync as existsSync23 } from "node:fs";
|
|
33974
33683
|
import { join as join28, resolve as resolve17 } from "node:path";
|
|
33975
33684
|
var I18n;
|
|
33976
33685
|
var init_i18n = __esm({
|
|
@@ -34067,7 +33776,7 @@ var init_i18n = __esm({
|
|
|
34067
33776
|
const filePath = join28(this._localeDir, `${locale}.json`);
|
|
34068
33777
|
if (existsSync23(filePath)) {
|
|
34069
33778
|
try {
|
|
34070
|
-
const raw =
|
|
33779
|
+
const raw = readFileSync21(filePath, "utf-8");
|
|
34071
33780
|
const data = JSON.parse(raw);
|
|
34072
33781
|
this._translations.set(locale, _I18n._flatten(data));
|
|
34073
33782
|
return;
|
|
@@ -34080,7 +33789,7 @@ var init_i18n = __esm({
|
|
|
34080
33789
|
const yamlPath = join28(this._localeDir, `${locale}${ext}`);
|
|
34081
33790
|
if (existsSync23(yamlPath)) {
|
|
34082
33791
|
try {
|
|
34083
|
-
const raw =
|
|
33792
|
+
const raw = readFileSync21(yamlPath, "utf-8");
|
|
34084
33793
|
const data = _I18n._parseSimpleYaml(raw);
|
|
34085
33794
|
this._translations.set(locale, _I18n._flatten(data));
|
|
34086
33795
|
return;
|
|
@@ -34943,8 +34652,8 @@ var init_docsAutoDiscovery = __esm({
|
|
|
34943
34652
|
// ../core/src/server.ts
|
|
34944
34653
|
import { createServer as createServer2 } from "node:http";
|
|
34945
34654
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
34946
|
-
import { resolve as resolve19, dirname as dirname14, join as join30, relative as
|
|
34947
|
-
import { existsSync as existsSync25, readdirSync as readdirSync17, readFileSync as
|
|
34655
|
+
import { resolve as resolve19, dirname as dirname14, join as join30, relative as relative8 } from "node:path";
|
|
34656
|
+
import { existsSync as existsSync25, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
|
|
34948
34657
|
import { isatty } from "node:tty";
|
|
34949
34658
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
34950
34659
|
import { execFileSync as execFileSync3, exec } from "node:child_process";
|
|
@@ -35168,7 +34877,7 @@ function getGalleryDeployedState() {
|
|
|
35168
34877
|
if (existsSync25(srcDir)) {
|
|
35169
34878
|
const files = walkGalleryFiles(srcDir);
|
|
35170
34879
|
const projectSrc = resolve19(process.cwd(), "src");
|
|
35171
|
-
state[entry] = files.every((f) => existsSync25(join30(projectSrc,
|
|
34880
|
+
state[entry] = files.every((f) => existsSync25(join30(projectSrc, relative8(srcDir, f))));
|
|
35172
34881
|
} else {
|
|
35173
34882
|
state[entry] = false;
|
|
35174
34883
|
}
|
|
@@ -35605,7 +35314,7 @@ function serveTemplateFallback(ctx) {
|
|
|
35605
35314
|
if ((ctx.req.method ?? "GET") !== "GET") return false;
|
|
35606
35315
|
const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
|
|
35607
35316
|
if (!tplFile) return false;
|
|
35608
|
-
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) :
|
|
35317
|
+
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync23(resolve19(ctx.templatesDir, tplFile), "utf-8");
|
|
35609
35318
|
ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
35610
35319
|
ctx.res.raw.end(html);
|
|
35611
35320
|
return true;
|
|
@@ -36432,7 +36141,7 @@ var init_mqttMessage = __esm({
|
|
|
36432
36141
|
import net2 from "node:net";
|
|
36433
36142
|
import tls from "node:tls";
|
|
36434
36143
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
36435
|
-
import { existsSync as existsSync26, readFileSync as
|
|
36144
|
+
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
|
|
36436
36145
|
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;
|
|
36437
36146
|
var init_mqtt = __esm({
|
|
36438
36147
|
"../core/src/mqtt.ts"() {
|
|
@@ -36897,7 +36606,7 @@ var init_mqtt = __esm({
|
|
|
36897
36606
|
servername: this.host,
|
|
36898
36607
|
rejectUnauthorized: this.tlsVerify
|
|
36899
36608
|
};
|
|
36900
|
-
if (this.tlsVerify && this.caFile) opts.ca =
|
|
36609
|
+
if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
|
|
36901
36610
|
sock = tls.connect(opts, () => settle(() => resolve30(sock)));
|
|
36902
36611
|
} else {
|
|
36903
36612
|
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve30(sock)));
|
|
@@ -37118,7 +36827,7 @@ var init_mqtt = __esm({
|
|
|
37118
36827
|
|
|
37119
36828
|
// ../core/src/service.ts
|
|
37120
36829
|
import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
|
|
37121
|
-
import { join as join31, extname as
|
|
36830
|
+
import { join as join31, extname as extname7 } from "node:path";
|
|
37122
36831
|
import { pathToFileURL } from "node:url";
|
|
37123
36832
|
function matchCronField(field, value) {
|
|
37124
36833
|
if (field === "*") return true;
|
|
@@ -37289,7 +36998,7 @@ var init_service = __esm({
|
|
|
37289
36998
|
return discovered;
|
|
37290
36999
|
}
|
|
37291
37000
|
for (const entry of entries) {
|
|
37292
|
-
const ext =
|
|
37001
|
+
const ext = extname7(entry);
|
|
37293
37002
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
37294
37003
|
const fullPath = join31(dir, entry);
|
|
37295
37004
|
const stat = statSync18(fullPath);
|
|
@@ -37405,7 +37114,7 @@ var init_service = __esm({
|
|
|
37405
37114
|
return;
|
|
37406
37115
|
}
|
|
37407
37116
|
for (const entry of entries) {
|
|
37408
|
-
const ext =
|
|
37117
|
+
const ext = extname7(entry);
|
|
37409
37118
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
37410
37119
|
const fullPath = join31(dir, entry);
|
|
37411
37120
|
if (watchedFiles.has(fullPath)) continue;
|
|
@@ -38108,7 +37817,7 @@ var init_api = __esm({
|
|
|
38108
37817
|
// ../core/src/messenger.ts
|
|
38109
37818
|
import net3 from "node:net";
|
|
38110
37819
|
import tls2 from "node:tls";
|
|
38111
|
-
import { readFileSync as
|
|
37820
|
+
import { readFileSync as readFileSync25 } from "node:fs";
|
|
38112
37821
|
import { basename as basename7 } from "node:path";
|
|
38113
37822
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
38114
37823
|
function tlsRejectUnauthorized() {
|
|
@@ -38205,7 +37914,7 @@ function buildMimeMessage(options) {
|
|
|
38205
37914
|
}
|
|
38206
37915
|
for (const filePath of options.attachments) {
|
|
38207
37916
|
const fileName = basename7(filePath);
|
|
38208
|
-
const fileData =
|
|
37917
|
+
const fileData = readFileSync25(filePath);
|
|
38209
37918
|
const base64Data = fileData.toString("base64");
|
|
38210
37919
|
lines.push("");
|
|
38211
37920
|
lines.push(`--${boundary}`);
|
|
@@ -39672,6 +39381,7 @@ var ai_exports = {};
|
|
|
39672
39381
|
__export(ai_exports, {
|
|
39673
39382
|
AI_TOOLS: () => AI_TOOLS,
|
|
39674
39383
|
DEV_SKILL: () => DEV_SKILL,
|
|
39384
|
+
downloadSkillsSync: () => downloadSkillsSync,
|
|
39675
39385
|
generateContext: () => generateContext,
|
|
39676
39386
|
hasMarkers: () => hasMarkers,
|
|
39677
39387
|
installAll: () => installAll,
|
|
@@ -39685,9 +39395,9 @@ __export(ai_exports, {
|
|
|
39685
39395
|
skillBlock: () => skillBlock,
|
|
39686
39396
|
writeOrMerge: () => writeOrMerge
|
|
39687
39397
|
});
|
|
39688
|
-
import { existsSync as existsSync27, mkdirSync as mkdirSync21, writeFileSync as writeFileSync18, readFileSync as
|
|
39398
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync21, writeFileSync as writeFileSync18, readFileSync as readFileSync26 } from "node:fs";
|
|
39689
39399
|
import { homedir } from "node:os";
|
|
39690
|
-
import { join as join32, resolve as resolve20, relative as
|
|
39400
|
+
import { join as join32, resolve as resolve20, relative as relative9, dirname as dirname15 } from "node:path";
|
|
39691
39401
|
import { fileURLToPath as fileURLToPath8 } from "node:url";
|
|
39692
39402
|
import { execSync as execSync2, execFileSync as execFileSync4 } from "node:child_process";
|
|
39693
39403
|
import { createInterface } from "node:readline";
|
|
@@ -39695,7 +39405,7 @@ function readVersion() {
|
|
|
39695
39405
|
try {
|
|
39696
39406
|
const thisDir = dirname15(fileURLToPath8(import.meta.url));
|
|
39697
39407
|
const rootPkg = resolve20(thisDir, "..", "..", "..", "package.json");
|
|
39698
|
-
const pkg = JSON.parse(
|
|
39408
|
+
const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
|
|
39699
39409
|
return pkg.version ?? "0.0.0";
|
|
39700
39410
|
} catch {
|
|
39701
39411
|
return "0.0.0";
|
|
@@ -39713,14 +39423,16 @@ function downloadSkillsSync(jobs) {
|
|
|
39713
39423
|
const jobs = JSON.parse(process.argv[1]);
|
|
39714
39424
|
const fs = require("node:fs");
|
|
39715
39425
|
const path = require("node:path");
|
|
39426
|
+
const transientStatuses = new Set([429, 500, 502, 503, 504]);
|
|
39716
39427
|
async function fetchOne(job) {
|
|
39717
39428
|
const resp = await fetch(job.url, { signal: AbortSignal.timeout(15000) });
|
|
39718
|
-
if (!resp.ok)
|
|
39429
|
+
if (!resp.ok) return { ok: false, retry: transientStatuses.has(resp.status) };
|
|
39719
39430
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
39720
39431
|
for (const dest of job.dests) {
|
|
39721
39432
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
39722
39433
|
fs.writeFileSync(dest, buf);
|
|
39723
39434
|
}
|
|
39435
|
+
return { ok: true, retry: false };
|
|
39724
39436
|
}
|
|
39725
39437
|
(async () => {
|
|
39726
39438
|
const ok = [];
|
|
@@ -39729,9 +39441,11 @@ function downloadSkillsSync(jobs) {
|
|
|
39729
39441
|
const failed = [];
|
|
39730
39442
|
await Promise.all(pending.map(async (job) => {
|
|
39731
39443
|
try {
|
|
39732
|
-
await fetchOne(job);
|
|
39733
|
-
ok.push(job.url);
|
|
39444
|
+
const result = await fetchOne(job);
|
|
39445
|
+
if (result.ok) ok.push(job.url);
|
|
39446
|
+
else if (result.retry) failed.push(job);
|
|
39734
39447
|
} catch {
|
|
39448
|
+
// DNS, TLS, timeout and connection failures are transient.
|
|
39735
39449
|
failed.push(job);
|
|
39736
39450
|
}
|
|
39737
39451
|
}));
|
|
@@ -39914,7 +39628,7 @@ function writeOrMerge(contextPath, contextFile, frameworkGuide) {
|
|
|
39914
39628
|
writeFileSync18(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
39915
39629
|
return "Installed";
|
|
39916
39630
|
}
|
|
39917
|
-
const existing =
|
|
39631
|
+
const existing = readFileSync26(contextPath, "utf-8");
|
|
39918
39632
|
if (hasMarkers(existing, start2, end)) {
|
|
39919
39633
|
writeFileSync18(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
|
|
39920
39634
|
return "Refreshed skill block in";
|
|
@@ -39938,7 +39652,7 @@ function installForTool(root, tool, context) {
|
|
|
39938
39652
|
const parentDir = dirname15(contextPath);
|
|
39939
39653
|
mkdirSync21(parentDir, { recursive: true });
|
|
39940
39654
|
const action = writeOrMerge(contextPath, tool.contextFile, context);
|
|
39941
|
-
const rel =
|
|
39655
|
+
const rel = relative9(root, contextPath);
|
|
39942
39656
|
created.push(rel);
|
|
39943
39657
|
console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
|
|
39944
39658
|
if (tool.name === "claude-code") {
|
|
@@ -40316,7 +40030,7 @@ function generateClaudeCodeContext() {
|
|
|
40316
40030
|
const repoRoot = resolve20(thisDir, "..", "..", "..");
|
|
40317
40031
|
const claudeMdPath = join32(repoRoot, "CLAUDE.md");
|
|
40318
40032
|
if (existsSync27(claudeMdPath)) {
|
|
40319
|
-
return
|
|
40033
|
+
return readFileSync26(claudeMdPath, "utf-8");
|
|
40320
40034
|
}
|
|
40321
40035
|
} catch {
|
|
40322
40036
|
}
|
|
@@ -40510,6 +40224,292 @@ export default class User {
|
|
|
40510
40224
|
}
|
|
40511
40225
|
});
|
|
40512
40226
|
|
|
40227
|
+
// ../core/src/aiClient.ts
|
|
40228
|
+
import http2 from "node:http";
|
|
40229
|
+
import https2 from "node:https";
|
|
40230
|
+
var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai;
|
|
40231
|
+
var init_aiClient = __esm({
|
|
40232
|
+
"../core/src/aiClient.ts"() {
|
|
40233
|
+
"use strict";
|
|
40234
|
+
AiError = class extends Error {
|
|
40235
|
+
};
|
|
40236
|
+
AiConfigError = class extends AiError {
|
|
40237
|
+
};
|
|
40238
|
+
AiTimeoutError = class extends AiError {
|
|
40239
|
+
};
|
|
40240
|
+
AiParseError = class extends AiError {
|
|
40241
|
+
};
|
|
40242
|
+
AiHTTPError = class extends AiError {
|
|
40243
|
+
constructor(message, status2 = null) {
|
|
40244
|
+
super(message);
|
|
40245
|
+
this.status = status2;
|
|
40246
|
+
}
|
|
40247
|
+
};
|
|
40248
|
+
Ai = class {
|
|
40249
|
+
static chat(messages, options = {}) {
|
|
40250
|
+
this.validateMessages(messages);
|
|
40251
|
+
const config = this.config("chat", options);
|
|
40252
|
+
const body = this.chatBody(config, messages, options);
|
|
40253
|
+
const headers = this.headers(config);
|
|
40254
|
+
return options.stream ? this.streamRequest(config, headers, body) : this.chatResponse(config, headers, body);
|
|
40255
|
+
}
|
|
40256
|
+
static async complete(prompt, options = {}) {
|
|
40257
|
+
if (typeof prompt !== "string") throw new AiConfigError("AI prompt must be a string");
|
|
40258
|
+
return (await this.chat([{ role: "user", content: prompt }], { ...options, stream: false })).text;
|
|
40259
|
+
}
|
|
40260
|
+
static async embed(textOrTexts, options = {}) {
|
|
40261
|
+
const single = typeof textOrTexts === "string";
|
|
40262
|
+
if (!single && (!Array.isArray(textOrTexts) || textOrTexts.length === 0 || !textOrTexts.every((item) => typeof item === "string"))) {
|
|
40263
|
+
throw new AiConfigError("AI embedding input must be a string or a non-empty list of strings");
|
|
40264
|
+
}
|
|
40265
|
+
const config = this.config("embed", options);
|
|
40266
|
+
if (config.provider === "anthropic") throw new AiConfigError("Anthropic does not provide the embedding endpoint in this contract");
|
|
40267
|
+
const raw = await this.requestJson(config, this.headers(config), { model: config.model, input: textOrTexts });
|
|
40268
|
+
try {
|
|
40269
|
+
const data = raw.data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
40270
|
+
const vectors = data.map((item) => item.embedding);
|
|
40271
|
+
const expected = single ? 1 : textOrTexts.length;
|
|
40272
|
+
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();
|
|
40273
|
+
return single ? vectors[0] : vectors;
|
|
40274
|
+
} catch {
|
|
40275
|
+
throw new AiParseError("AI provider returned a malformed embedding response");
|
|
40276
|
+
}
|
|
40277
|
+
}
|
|
40278
|
+
static validateMessages(messages) {
|
|
40279
|
+
if (!Array.isArray(messages) || messages.length === 0 || !messages.every((message) => message && ["system", "user", "assistant"].includes(message.role) && typeof message.content === "string")) {
|
|
40280
|
+
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
40281
|
+
}
|
|
40282
|
+
}
|
|
40283
|
+
static number(name, fallback, minimum) {
|
|
40284
|
+
const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
|
|
40285
|
+
if (!Number.isFinite(value) || value < minimum) throw new AiConfigError(`${name} must be numeric and at least ${minimum}`);
|
|
40286
|
+
return value;
|
|
40287
|
+
}
|
|
40288
|
+
static config(capability, options) {
|
|
40289
|
+
const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
|
|
40290
|
+
if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
|
|
40291
|
+
const key = process.env.TINA4_AI_KEY || null;
|
|
40292
|
+
if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
40293
|
+
const defaults = {
|
|
40294
|
+
local: ["http://localhost:11437", "llama3.2"],
|
|
40295
|
+
openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
|
|
40296
|
+
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
|
|
40297
|
+
};
|
|
40298
|
+
const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
|
|
40299
|
+
const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
|
|
40300
|
+
if (!model) throw new AiConfigError("AI model must be a non-empty string");
|
|
40301
|
+
const totalTimeout = options.timeout === void 0 ? this.number("TINA4_AI_TIMEOUT", 60, 1e-3) : Number(options.timeout);
|
|
40302
|
+
if (!Number.isFinite(totalTimeout) || totalTimeout <= 0) throw new AiConfigError("AI timeout must be greater than zero");
|
|
40303
|
+
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)) };
|
|
40304
|
+
}
|
|
40305
|
+
static endpoint(value, capability, provider) {
|
|
40306
|
+
let url;
|
|
40307
|
+
try {
|
|
40308
|
+
url = new URL(value);
|
|
40309
|
+
} catch {
|
|
40310
|
+
throw new AiConfigError("AI URL must be an http or https URL");
|
|
40311
|
+
}
|
|
40312
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
|
|
40313
|
+
const path8 = url.pathname.replace(/\/+$/, "");
|
|
40314
|
+
if (path8 === "" || path8 === "/v1" || path8 === "/api") {
|
|
40315
|
+
const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
|
|
40316
|
+
url.pathname = (path8 || "/v1") + suffix;
|
|
40317
|
+
}
|
|
40318
|
+
return url.toString();
|
|
40319
|
+
}
|
|
40320
|
+
static headers(config) {
|
|
40321
|
+
const headers = { "content-type": "application/json", accept: "application/json" };
|
|
40322
|
+
if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
|
|
40323
|
+
if (config.provider === "anthropic") {
|
|
40324
|
+
headers["x-api-key"] = config.key;
|
|
40325
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
40326
|
+
}
|
|
40327
|
+
return headers;
|
|
40328
|
+
}
|
|
40329
|
+
static chatBody(config, messages, options) {
|
|
40330
|
+
const body = { model: config.model, messages, stream: options.stream ?? false };
|
|
40331
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
40332
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
40333
|
+
if (config.provider === "anthropic") {
|
|
40334
|
+
const system = messages.filter((message) => message.role === "system").map((message) => message.content);
|
|
40335
|
+
body.messages = messages.filter((message) => message.role !== "system");
|
|
40336
|
+
body.max_tokens = options.maxTokens ?? 1024;
|
|
40337
|
+
if (system.length) body.system = system.join("\n\n");
|
|
40338
|
+
}
|
|
40339
|
+
return body;
|
|
40340
|
+
}
|
|
40341
|
+
static open(config, deadline, headers, body) {
|
|
40342
|
+
const remainingMs = deadline - performance.now();
|
|
40343
|
+
if (remainingMs <= 0) return Promise.reject(new AiTimeoutError("AI total request timeout expired"));
|
|
40344
|
+
const url = new URL(config.url);
|
|
40345
|
+
const payload = JSON.stringify(body);
|
|
40346
|
+
const controller = new AbortController();
|
|
40347
|
+
const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
|
|
40348
|
+
return new Promise((resolve30, reject) => {
|
|
40349
|
+
const client = url.protocol === "https:" ? https2 : http2;
|
|
40350
|
+
const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
|
|
40351
|
+
clearTimeout(connectTimer);
|
|
40352
|
+
resolve30({ response, cleanup: () => {
|
|
40353
|
+
clearTimeout(totalTimer);
|
|
40354
|
+
clearTimeout(connectTimer);
|
|
40355
|
+
} });
|
|
40356
|
+
});
|
|
40357
|
+
const connectTimer = setTimeout(() => request.destroy(new AiTimeoutError("AI connection timeout expired")), Math.min(config.connectTimeout * 1e3, remainingMs));
|
|
40358
|
+
request.on("socket", (socket) => {
|
|
40359
|
+
if (!socket.connecting) clearTimeout(connectTimer);
|
|
40360
|
+
socket.once(url.protocol === "https:" ? "secureConnect" : "connect", () => clearTimeout(connectTimer));
|
|
40361
|
+
});
|
|
40362
|
+
request.once("error", (error) => {
|
|
40363
|
+
clearTimeout(totalTimer);
|
|
40364
|
+
clearTimeout(connectTimer);
|
|
40365
|
+
if (error instanceof AiError) reject(error);
|
|
40366
|
+
else if (controller.signal.aborted) reject(new AiTimeoutError("AI total request timeout expired"));
|
|
40367
|
+
else reject(new AiHTTPError(`AI transport failed (${error.name})`));
|
|
40368
|
+
});
|
|
40369
|
+
request.end(payload);
|
|
40370
|
+
});
|
|
40371
|
+
}
|
|
40372
|
+
static async readBody(response) {
|
|
40373
|
+
const chunks = [];
|
|
40374
|
+
for await (const chunk of response) chunks.push(Buffer.from(chunk));
|
|
40375
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
40376
|
+
}
|
|
40377
|
+
static retryDelay(headers, deadline) {
|
|
40378
|
+
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
40379
|
+
const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
|
|
40380
|
+
const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
|
|
40381
|
+
return new Promise((resolve30) => setTimeout(resolve30, delay));
|
|
40382
|
+
}
|
|
40383
|
+
static async requestJson(config, headers, body) {
|
|
40384
|
+
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
40385
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
40386
|
+
let opened = null;
|
|
40387
|
+
try {
|
|
40388
|
+
opened = await this.open(config, deadline, headers, body);
|
|
40389
|
+
const status2 = opened.response.statusCode ?? 0;
|
|
40390
|
+
const responseHeaders = opened.response.headers;
|
|
40391
|
+
const raw = await this.readBody(opened.response);
|
|
40392
|
+
opened.cleanup();
|
|
40393
|
+
opened = null;
|
|
40394
|
+
if (status2 < 200 || status2 >= 300) {
|
|
40395
|
+
if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
|
|
40396
|
+
await this.retryDelay(responseHeaders, deadline);
|
|
40397
|
+
continue;
|
|
40398
|
+
}
|
|
40399
|
+
throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
|
|
40400
|
+
}
|
|
40401
|
+
let parsed;
|
|
40402
|
+
try {
|
|
40403
|
+
parsed = JSON.parse(raw);
|
|
40404
|
+
} catch {
|
|
40405
|
+
throw new AiParseError("AI provider returned malformed JSON");
|
|
40406
|
+
}
|
|
40407
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new AiParseError("AI provider returned a non-object JSON response");
|
|
40408
|
+
return parsed;
|
|
40409
|
+
} catch (error) {
|
|
40410
|
+
opened?.cleanup();
|
|
40411
|
+
if (error instanceof AiParseError || error instanceof AiHTTPError && error.status !== null) throw error;
|
|
40412
|
+
if (attempt >= config.maxRetries) throw error;
|
|
40413
|
+
}
|
|
40414
|
+
}
|
|
40415
|
+
throw new AiHTTPError("AI request failed");
|
|
40416
|
+
}
|
|
40417
|
+
static normalizeChat(provider, raw) {
|
|
40418
|
+
try {
|
|
40419
|
+
if (provider === "anthropic") {
|
|
40420
|
+
const content = raw.content;
|
|
40421
|
+
const parts = content.filter((item) => (item.type ?? "text") === "text").map((item) => item.text);
|
|
40422
|
+
if (!parts.length || !parts.every((part) => typeof part === "string")) throw new Error();
|
|
40423
|
+
const usage2 = raw.usage ?? {};
|
|
40424
|
+
const promptTokens = Number(usage2.input_tokens ?? 0);
|
|
40425
|
+
const completionTokens = Number(usage2.output_tokens ?? 0);
|
|
40426
|
+
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 };
|
|
40427
|
+
}
|
|
40428
|
+
const choice = raw.choices[0];
|
|
40429
|
+
const text = choice.message.content;
|
|
40430
|
+
if (typeof text !== "string") throw new Error();
|
|
40431
|
+
const usage = raw.usage ?? {};
|
|
40432
|
+
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 };
|
|
40433
|
+
} catch {
|
|
40434
|
+
throw new AiParseError("AI provider returned a malformed chat response");
|
|
40435
|
+
}
|
|
40436
|
+
}
|
|
40437
|
+
static async chatResponse(config, headers, body) {
|
|
40438
|
+
return this.normalizeChat(config.provider, await this.requestJson(config, headers, body));
|
|
40439
|
+
}
|
|
40440
|
+
static streamDelta(provider, data) {
|
|
40441
|
+
if (data === "[DONE]") return { completed: true };
|
|
40442
|
+
let event;
|
|
40443
|
+
try {
|
|
40444
|
+
event = JSON.parse(data);
|
|
40445
|
+
} catch {
|
|
40446
|
+
throw new AiParseError("AI provider returned malformed stream data");
|
|
40447
|
+
}
|
|
40448
|
+
const text = provider === "anthropic" ? event.type === "content_block_delta" ? event.delta?.text : void 0 : event.choices?.[0]?.delta?.content;
|
|
40449
|
+
if (text !== void 0 && text !== null && typeof text !== "string") throw new AiParseError("AI provider returned malformed stream data");
|
|
40450
|
+
return { completed: false, text };
|
|
40451
|
+
}
|
|
40452
|
+
static async *streamData(response) {
|
|
40453
|
+
let buffer = "";
|
|
40454
|
+
for await (const chunk of response) {
|
|
40455
|
+
buffer += Buffer.from(chunk).toString("utf8");
|
|
40456
|
+
let newline;
|
|
40457
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
40458
|
+
const line = buffer.slice(0, newline).trim();
|
|
40459
|
+
buffer = buffer.slice(newline + 1);
|
|
40460
|
+
if (line.startsWith("data:")) yield line.slice(5).trim();
|
|
40461
|
+
}
|
|
40462
|
+
}
|
|
40463
|
+
}
|
|
40464
|
+
static streamError(error) {
|
|
40465
|
+
if (error instanceof AiError) return error;
|
|
40466
|
+
if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
|
|
40467
|
+
return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
|
|
40468
|
+
}
|
|
40469
|
+
static async *streamRequest(config, headers, body) {
|
|
40470
|
+
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
40471
|
+
let yielded = false;
|
|
40472
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
40473
|
+
let opened = null;
|
|
40474
|
+
try {
|
|
40475
|
+
opened = await this.open(config, deadline, { ...headers, accept: "text/event-stream" }, body);
|
|
40476
|
+
const status2 = opened.response.statusCode ?? 0;
|
|
40477
|
+
if (status2 < 200 || status2 >= 300) {
|
|
40478
|
+
await this.readBody(opened.response);
|
|
40479
|
+
if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
|
|
40480
|
+
await this.retryDelay(opened.response.headers, deadline);
|
|
40481
|
+
opened.cleanup();
|
|
40482
|
+
opened = null;
|
|
40483
|
+
continue;
|
|
40484
|
+
}
|
|
40485
|
+
throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
|
|
40486
|
+
}
|
|
40487
|
+
let completed = false;
|
|
40488
|
+
for await (const data of this.streamData(opened.response)) {
|
|
40489
|
+
const delta = this.streamDelta(config.provider, data);
|
|
40490
|
+
if (delta.completed) {
|
|
40491
|
+
completed = true;
|
|
40492
|
+
break;
|
|
40493
|
+
}
|
|
40494
|
+
if (delta.text === void 0) continue;
|
|
40495
|
+
yielded = true;
|
|
40496
|
+
yield delta.text;
|
|
40497
|
+
}
|
|
40498
|
+
opened.cleanup();
|
|
40499
|
+
opened = null;
|
|
40500
|
+
if (completed) return;
|
|
40501
|
+
throw new AiParseError("AI provider stream ended before [DONE]");
|
|
40502
|
+
} catch (error) {
|
|
40503
|
+
opened?.cleanup();
|
|
40504
|
+
const failure = this.streamError(error);
|
|
40505
|
+
if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || yielded || attempt >= config.maxRetries) throw failure;
|
|
40506
|
+
}
|
|
40507
|
+
}
|
|
40508
|
+
}
|
|
40509
|
+
};
|
|
40510
|
+
}
|
|
40511
|
+
});
|
|
40512
|
+
|
|
40513
40513
|
// ../core/src/queueBackends/rabbitmqBackend.ts
|
|
40514
40514
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
40515
40515
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
@@ -42213,6 +42213,12 @@ __export(src_exports3, {
|
|
|
42213
42213
|
APPLICATION_JSON: () => APPLICATION_JSON,
|
|
42214
42214
|
APPLICATION_OCTET: () => APPLICATION_OCTET,
|
|
42215
42215
|
APPLICATION_XML: () => APPLICATION_XML,
|
|
42216
|
+
Ai: () => Ai,
|
|
42217
|
+
AiConfigError: () => AiConfigError,
|
|
42218
|
+
AiError: () => AiError,
|
|
42219
|
+
AiHTTPError: () => AiHTTPError,
|
|
42220
|
+
AiParseError: () => AiParseError,
|
|
42221
|
+
AiTimeoutError: () => AiTimeoutError,
|
|
42216
42222
|
Api: () => Api,
|
|
42217
42223
|
Auth: () => Auth,
|
|
42218
42224
|
CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
|
|
@@ -42540,6 +42546,7 @@ var init_src3 = __esm({
|
|
|
42540
42546
|
init_htmlElement();
|
|
42541
42547
|
init_errorOverlay();
|
|
42542
42548
|
init_ai();
|
|
42549
|
+
init_aiClient();
|
|
42543
42550
|
init_liteBackend();
|
|
42544
42551
|
init_rabbitmqBackend();
|
|
42545
42552
|
init_kafkaBackend();
|
|
@@ -43053,7 +43060,7 @@ async function listRoutes() {
|
|
|
43053
43060
|
}
|
|
43054
43061
|
|
|
43055
43062
|
// src/commands/test.ts
|
|
43056
|
-
import { existsSync as existsSync32, readdirSync as readdirSync20, readFileSync as
|
|
43063
|
+
import { existsSync as existsSync32, readdirSync as readdirSync20, readFileSync as readFileSync27, statSync as statSync19 } from "node:fs";
|
|
43057
43064
|
import { resolve as resolve27, join as join34 } from "node:path";
|
|
43058
43065
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
43059
43066
|
import { execSync as execSync3 } from "node:child_process";
|
|
@@ -43090,7 +43097,7 @@ async function runInlineTests(cwd) {
|
|
|
43090
43097
|
for (const file of walkSource(srcDir)) {
|
|
43091
43098
|
let text;
|
|
43092
43099
|
try {
|
|
43093
|
-
text =
|
|
43100
|
+
text = readFileSync27(file, "utf-8");
|
|
43094
43101
|
} catch {
|
|
43095
43102
|
continue;
|
|
43096
43103
|
}
|
|
@@ -43154,8 +43161,8 @@ async function runTests(testPath) {
|
|
|
43154
43161
|
console.log(` Found ${testFiles.length} test file(s)
|
|
43155
43162
|
`);
|
|
43156
43163
|
for (const file of testFiles) {
|
|
43157
|
-
const
|
|
43158
|
-
console.log(` Running: ${
|
|
43164
|
+
const relative10 = file.replace(cwd + "/", "");
|
|
43165
|
+
console.log(` Running: ${relative10}`);
|
|
43159
43166
|
try {
|
|
43160
43167
|
execSync3(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
|
|
43161
43168
|
} catch {
|
|
@@ -44804,8 +44811,8 @@ async function runSeeds(seedPath) {
|
|
|
44804
44811
|
`);
|
|
44805
44812
|
let failed = false;
|
|
44806
44813
|
for (const file of seedFiles) {
|
|
44807
|
-
const
|
|
44808
|
-
console.log(` Seeding: ${
|
|
44814
|
+
const relative10 = file.replace(cwd + "/", "");
|
|
44815
|
+
console.log(` Seeding: ${relative10}`);
|
|
44809
44816
|
try {
|
|
44810
44817
|
execSync4(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
|
|
44811
44818
|
} catch {
|
|
@@ -44819,121 +44826,14 @@ async function runSeeds(seedPath) {
|
|
|
44819
44826
|
console.log("\n All seeds completed.");
|
|
44820
44827
|
}
|
|
44821
44828
|
|
|
44822
|
-
// src/commands/metrics.ts
|
|
44823
|
-
init_metrics();
|
|
44824
|
-
function parseFlags(args) {
|
|
44825
|
-
const flags = { top: 20, json: false, path: "src", failOn: null };
|
|
44826
|
-
for (let i = 0; i < args.length; i++) {
|
|
44827
|
-
const a = args[i];
|
|
44828
|
-
switch (a) {
|
|
44829
|
-
case "--json":
|
|
44830
|
-
flags.json = true;
|
|
44831
|
-
break;
|
|
44832
|
-
case "--top": {
|
|
44833
|
-
const v = args[++i];
|
|
44834
|
-
if (v === void 0 || !/^\d+$/.test(v)) {
|
|
44835
|
-
return { error: `--top expects a number (got '${v ?? ""}')` };
|
|
44836
|
-
}
|
|
44837
|
-
flags.top = parseInt(v, 10);
|
|
44838
|
-
break;
|
|
44839
|
-
}
|
|
44840
|
-
case "--path": {
|
|
44841
|
-
const v = args[++i];
|
|
44842
|
-
if (v === void 0) return { error: "--path expects a directory" };
|
|
44843
|
-
flags.path = v;
|
|
44844
|
-
break;
|
|
44845
|
-
}
|
|
44846
|
-
case "--fail-on": {
|
|
44847
|
-
const v = args[++i];
|
|
44848
|
-
if (v !== "warn" && v !== "error") {
|
|
44849
|
-
return { error: `invalid --fail-on '${v ?? ""}' (use warn or error)` };
|
|
44850
|
-
}
|
|
44851
|
-
flags.failOn = v;
|
|
44852
|
-
break;
|
|
44853
|
-
}
|
|
44854
|
-
default:
|
|
44855
|
-
return { error: `unknown option '${a}'` };
|
|
44856
|
-
}
|
|
44857
|
-
}
|
|
44858
|
-
return flags;
|
|
44859
|
-
}
|
|
44860
|
-
function runMetrics(args = []) {
|
|
44861
|
-
const parsed = parseFlags(args);
|
|
44862
|
-
if ("error" in parsed) {
|
|
44863
|
-
console.log(` ${parsed.error}`);
|
|
44864
|
-
return 2;
|
|
44865
|
-
}
|
|
44866
|
-
const { top, json, path: path8, failOn } = parsed;
|
|
44867
|
-
let result;
|
|
44868
|
-
try {
|
|
44869
|
-
result = offenders(path8, Number.MAX_SAFE_INTEGER);
|
|
44870
|
-
} catch (e) {
|
|
44871
|
-
if (e instanceof MetricsEngineError) {
|
|
44872
|
-
console.error(` metrics error: ${e.message}`);
|
|
44873
|
-
return 2;
|
|
44874
|
-
}
|
|
44875
|
-
throw e;
|
|
44876
|
-
}
|
|
44877
|
-
const summary = result.summary;
|
|
44878
|
-
const allOffenders = result.offenders;
|
|
44879
|
-
const found = allOffenders.slice(0, top);
|
|
44880
|
-
const severities = new Set(allOffenders.map((o) => o.severity));
|
|
44881
|
-
let exitCode = 0;
|
|
44882
|
-
if (failOn === "warn" && (severities.has("warn") || severities.has("error"))) {
|
|
44883
|
-
exitCode = 1;
|
|
44884
|
-
} else if (failOn === "error" && severities.has("error")) {
|
|
44885
|
-
exitCode = 1;
|
|
44886
|
-
}
|
|
44887
|
-
if (json) {
|
|
44888
|
-
console.log(JSON.stringify({ summary, offenders: found }, null, 2));
|
|
44889
|
-
return exitCode;
|
|
44890
|
-
}
|
|
44891
|
-
const useColor = Boolean(process.stdout.isTTY);
|
|
44892
|
-
const c = (text, code) => useColor ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
44893
|
-
const sevColor = { error: "31", warn: "33", info: "2" };
|
|
44894
|
-
console.log("");
|
|
44895
|
-
console.log(` Tina4 Metrics \u2014 ${summary.scan_mode} scan (${summary.scan_root})`);
|
|
44896
|
-
console.log(
|
|
44897
|
-
` files: ${summary.files_analyzed} functions: ${summary.total_functions} avg complexity: ${summary.avg_complexity} avg maintainability: ${summary.avg_maintainability}`
|
|
44898
|
-
);
|
|
44899
|
-
console.log(
|
|
44900
|
-
` offenders: ${summary.total_offenders} total` + (found.length ? ` (showing top ${found.length})` : "")
|
|
44901
|
-
);
|
|
44902
|
-
console.log("");
|
|
44903
|
-
if (found.length === 0) {
|
|
44904
|
-
console.log(" " + c("\u2713 no offenders \u2014 clean", "32"));
|
|
44905
|
-
console.log("");
|
|
44906
|
-
return exitCode;
|
|
44907
|
-
}
|
|
44908
|
-
const locs = found.map((o) => `${o.file}:${o.line}`);
|
|
44909
|
-
const locW = Math.max("FILE:LINE".length, ...locs.map((s) => s.length));
|
|
44910
|
-
const kindW = Math.max("KIND".length, ...found.map((o) => o.kind.length));
|
|
44911
|
-
const pad = (s, w) => s.padEnd(w);
|
|
44912
|
-
const header = ` ${pad("#", 3)} ${pad("SEVERITY", 8)} ${pad("KIND", kindW)} ${pad(
|
|
44913
|
-
"FILE:LINE",
|
|
44914
|
-
locW
|
|
44915
|
-
)} DETAIL`;
|
|
44916
|
-
console.log(c(header, "1"));
|
|
44917
|
-
console.log(" " + "-".repeat(header.length - 2));
|
|
44918
|
-
found.forEach((o, idx) => {
|
|
44919
|
-
const i = idx + 1;
|
|
44920
|
-
const sevCell = c(pad(o.severity, 8), sevColor[o.severity]);
|
|
44921
|
-
console.log(
|
|
44922
|
-
` ${String(i).padStart(3)} ${sevCell} ${pad(o.kind, kindW)} ${pad(locs[idx], locW)} ${o.detail}`
|
|
44923
|
-
);
|
|
44924
|
-
});
|
|
44925
|
-
console.log("");
|
|
44926
|
-
return exitCode;
|
|
44927
|
-
}
|
|
44928
|
-
|
|
44929
44829
|
// src/commands/queue.ts
|
|
44930
44830
|
init_dotenv();
|
|
44931
44831
|
init_queue();
|
|
44932
44832
|
import { readdirSync as readdirSync22, statSync as statSync20 } from "node:fs";
|
|
44933
|
-
import { extname as
|
|
44833
|
+
import { extname as extname8, join as join37 } from "node:path";
|
|
44934
44834
|
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
44935
44835
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["once", "json"]);
|
|
44936
|
-
function
|
|
44836
|
+
function parseFlags(args) {
|
|
44937
44837
|
const flags = {};
|
|
44938
44838
|
const positional = [];
|
|
44939
44839
|
let i = 0;
|
|
@@ -44968,7 +44868,7 @@ async function resolveQueueHandler(servicesDir, topic) {
|
|
|
44968
44868
|
}
|
|
44969
44869
|
for (const entry of entries.sort()) {
|
|
44970
44870
|
if (entry.startsWith("_")) continue;
|
|
44971
|
-
const ext =
|
|
44871
|
+
const ext = extname8(entry);
|
|
44972
44872
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
44973
44873
|
const fullPath = join37(servicesDir, entry);
|
|
44974
44874
|
try {
|
|
@@ -44989,7 +44889,7 @@ function firstJob(yielded) {
|
|
|
44989
44889
|
}
|
|
44990
44890
|
async function queueWork(args) {
|
|
44991
44891
|
loadEnv();
|
|
44992
|
-
const { flags, positional } =
|
|
44892
|
+
const { flags, positional } = parseFlags(args);
|
|
44993
44893
|
const topic = positional[0] ?? "default";
|
|
44994
44894
|
const once2 = Boolean(flags.once);
|
|
44995
44895
|
let pollSeconds;
|
|
@@ -45041,7 +44941,7 @@ async function queueWork(args) {
|
|
|
45041
44941
|
}
|
|
45042
44942
|
async function queueStats(args) {
|
|
45043
44943
|
loadEnv();
|
|
45044
|
-
const { flags, positional } =
|
|
44944
|
+
const { flags, positional } = parseFlags(args);
|
|
45045
44945
|
const topic = positional[0] ?? "default";
|
|
45046
44946
|
const queue = new Queue({ topic });
|
|
45047
44947
|
const stats = {
|
|
@@ -45072,7 +44972,7 @@ async function queueStats(args) {
|
|
|
45072
44972
|
}
|
|
45073
44973
|
async function queueRetry(args) {
|
|
45074
44974
|
loadEnv();
|
|
45075
|
-
const { positional } =
|
|
44975
|
+
const { positional } = parseFlags(args);
|
|
45076
44976
|
const topic = positional[0] ?? "default";
|
|
45077
44977
|
const queue = new Queue({ topic });
|
|
45078
44978
|
const dead = queue.deadLetters(0);
|
|
@@ -45088,7 +44988,7 @@ async function queueRetry(args) {
|
|
|
45088
44988
|
}
|
|
45089
44989
|
async function queueClear(args) {
|
|
45090
44990
|
loadEnv();
|
|
45091
|
-
const { positional } =
|
|
44991
|
+
const { positional } = parseFlags(args);
|
|
45092
44992
|
const status2 = positional[0] ?? "completed";
|
|
45093
44993
|
const topic = positional[1] ?? "default";
|
|
45094
44994
|
const queue = new Queue({ topic });
|
|
@@ -45125,7 +45025,7 @@ async function queueCommand(args = []) {
|
|
|
45125
45025
|
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync35, statSync as statSync21 } from "node:fs";
|
|
45126
45026
|
import { basename as basename8, delimiter as delimiter2, join as join38 } from "node:path";
|
|
45127
45027
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
45128
|
-
function
|
|
45028
|
+
function parseFlags2(args) {
|
|
45129
45029
|
const flags = {};
|
|
45130
45030
|
let i = 0;
|
|
45131
45031
|
while (i < args.length) {
|
|
@@ -45163,7 +45063,7 @@ function whichDocker() {
|
|
|
45163
45063
|
return null;
|
|
45164
45064
|
}
|
|
45165
45065
|
function buildImage(args) {
|
|
45166
|
-
const flags =
|
|
45066
|
+
const flags = parseFlags2(args);
|
|
45167
45067
|
let tag = typeof flags.tag === "string" ? flags.tag : "";
|
|
45168
45068
|
if (!tag) {
|
|
45169
45069
|
const dirName = basename8(process.cwd()).toLowerCase();
|
|
@@ -45198,7 +45098,7 @@ function buildImage(args) {
|
|
|
45198
45098
|
|
|
45199
45099
|
// src/bin.ts
|
|
45200
45100
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
45201
|
-
import { existsSync as existsSync36, readFileSync as
|
|
45101
|
+
import { existsSync as existsSync36, readFileSync as readFileSync28, statSync as statSync22 } from "node:fs";
|
|
45202
45102
|
import { delimiter as delimiter3, dirname as dirname16, join as join39 } from "node:path";
|
|
45203
45103
|
import { fileURLToPath as fileURLToPath9, pathToFileURL as pathToFileURL4 } from "node:url";
|
|
45204
45104
|
function readCliVersion() {
|
|
@@ -45207,7 +45107,7 @@ function readCliVersion() {
|
|
|
45207
45107
|
const pkgPath = join39(dir, "package.json");
|
|
45208
45108
|
if (existsSync36(pkgPath)) {
|
|
45209
45109
|
try {
|
|
45210
|
-
const pkg = JSON.parse(
|
|
45110
|
+
const pkg = JSON.parse(readFileSync28(pkgPath, "utf-8"));
|
|
45211
45111
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
45212
45112
|
} catch {
|
|
45213
45113
|
}
|
|
@@ -45443,13 +45343,6 @@ var COMMANDS = {
|
|
|
45443
45343
|
usage: "[file]",
|
|
45444
45344
|
summary: "Run database seed files from src/seeds/"
|
|
45445
45345
|
},
|
|
45446
|
-
metrics: {
|
|
45447
|
-
handler: (a) => {
|
|
45448
|
-
process.exit(runMetrics(a));
|
|
45449
|
-
},
|
|
45450
|
-
usage: "[--top N] [--json] [--fail-on warn|error] [--path DIR]",
|
|
45451
|
-
summary: "Rank top code-quality offenders"
|
|
45452
|
-
},
|
|
45453
45346
|
console: {
|
|
45454
45347
|
handler: async () => {
|
|
45455
45348
|
await openConsole();
|