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
|
@@ -2620,7 +2620,10 @@ var init_trustedProxy = __esm({
|
|
|
2620
2620
|
var engine_exports = {};
|
|
2621
2621
|
__export(engine_exports, {
|
|
2622
2622
|
Frond: () => Frond,
|
|
2623
|
+
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
2623
2624
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
2625
|
+
filterChainCache: () => filterChainCache,
|
|
2626
|
+
pathParseCache: () => pathParseCache,
|
|
2624
2627
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
2625
2628
|
});
|
|
2626
2629
|
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -2724,6 +2727,12 @@ function capCache(cache, maxEntries) {
|
|
|
2724
2727
|
if (--drop <= 0) break;
|
|
2725
2728
|
}
|
|
2726
2729
|
}
|
|
2730
|
+
function sweepExpiredCache(cache) {
|
|
2731
|
+
const now = Date.now();
|
|
2732
|
+
for (const [key, [, expiresAt]] of cache) {
|
|
2733
|
+
if (expiresAt <= now) cache.delete(key);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2727
2736
|
function tokenize(source) {
|
|
2728
2737
|
const rawBlocks = [];
|
|
2729
2738
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -2787,6 +2796,16 @@ function stripTag(raw) {
|
|
|
2787
2796
|
}
|
|
2788
2797
|
return [inner.trim(), stripBefore, stripAfter];
|
|
2789
2798
|
}
|
|
2799
|
+
function extendsTarget(source) {
|
|
2800
|
+
const matches = source.match(EXTENDS_RE_GLOBAL);
|
|
2801
|
+
if (matches && matches.length > 1) {
|
|
2802
|
+
throw new Error(
|
|
2803
|
+
`Frond: template has ${matches.length} "{% extends %}" tags -- a template can extend only one parent`
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
const match = source.match(EXTENDS_RE);
|
|
2807
|
+
return match ? match[1] : "";
|
|
2808
|
+
}
|
|
2790
2809
|
function resolveVar(expr, context) {
|
|
2791
2810
|
expr = expr.trim();
|
|
2792
2811
|
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
@@ -2867,6 +2886,7 @@ function resolveVar(expr, context) {
|
|
|
2867
2886
|
fromBracket.push(false);
|
|
2868
2887
|
}
|
|
2869
2888
|
}
|
|
2889
|
+
capCache(pathParseCache, MEMO_CACHE_MAX);
|
|
2870
2890
|
pathParseCache.set(expr, [parts, fromBracket]);
|
|
2871
2891
|
}
|
|
2872
2892
|
let value = context;
|
|
@@ -3454,6 +3474,7 @@ function parseFilterChain(expr) {
|
|
|
3454
3474
|
}
|
|
3455
3475
|
}
|
|
3456
3476
|
const result = [variable, filters];
|
|
3477
|
+
capCache(filterChainCache, MEMO_CACHE_MAX);
|
|
3457
3478
|
filterChainCache.set(expr, result);
|
|
3458
3479
|
return result;
|
|
3459
3480
|
}
|
|
@@ -3633,7 +3654,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
3633
3654
|
function _generateFormTokenValue(descriptor = "") {
|
|
3634
3655
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
3635
3656
|
}
|
|
3636
|
-
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;
|
|
3657
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
3637
3658
|
var init_engine = __esm({
|
|
3638
3659
|
"../frond/src/engine.ts"() {
|
|
3639
3660
|
"use strict";
|
|
@@ -3727,9 +3748,12 @@ var init_engine = __esm({
|
|
|
3727
3748
|
LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
|
|
3728
3749
|
LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
|
|
3729
3750
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
3751
|
+
EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
|
|
3752
|
+
EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
|
|
3730
3753
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
3731
3754
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
3732
3755
|
TEMPLATE_CACHE_MAX = 256;
|
|
3756
|
+
MEMO_CACHE_MAX = 1024;
|
|
3733
3757
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
3734
3758
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
3735
3759
|
VarRef = class {
|
|
@@ -4082,29 +4106,22 @@ var init_engine = __esm({
|
|
|
4082
4106
|
return this;
|
|
4083
4107
|
}
|
|
4084
4108
|
/**
|
|
4085
|
-
* Register a custom filter
|
|
4086
|
-
*
|
|
4087
|
-
* the live instance's local filter map also receives the addition
|
|
4088
|
-
* immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
|
|
4109
|
+
* Register a custom filter on this instance only. Use the static method
|
|
4110
|
+
* for process-global registration. tina4: ADR-0052.
|
|
4089
4111
|
*/
|
|
4090
4112
|
addFilter(name, fn) {
|
|
4091
|
-
_Frond.classFilters.set(name, fn);
|
|
4092
4113
|
this.filters[name] = fn;
|
|
4093
4114
|
}
|
|
4094
4115
|
/**
|
|
4095
|
-
* Register a global variable
|
|
4096
|
-
* at class level — see ``addFilter`` for the dual-call semantics.
|
|
4116
|
+
* Register a global variable on this instance only.
|
|
4097
4117
|
*/
|
|
4098
4118
|
addGlobal(name, value) {
|
|
4099
|
-
_Frond.classGlobals.set(name, value);
|
|
4100
4119
|
this.globals[name] = value;
|
|
4101
4120
|
}
|
|
4102
4121
|
/**
|
|
4103
|
-
* Register a custom test
|
|
4104
|
-
* ``addFilter`` for the dual-call semantics.
|
|
4122
|
+
* Register a custom test on this instance only.
|
|
4105
4123
|
*/
|
|
4106
4124
|
addTest(name, fn) {
|
|
4107
|
-
_Frond.classTests.set(name, fn);
|
|
4108
4125
|
this.tests[name] = fn;
|
|
4109
4126
|
}
|
|
4110
4127
|
/**
|
|
@@ -4219,9 +4236,8 @@ var init_engine = __esm({
|
|
|
4219
4236
|
if (Object.keys(this.tests).length > 0) {
|
|
4220
4237
|
context.__frond_tests__ = this.tests;
|
|
4221
4238
|
}
|
|
4222
|
-
const
|
|
4223
|
-
if (
|
|
4224
|
-
const parentName = extendsMatch[1];
|
|
4239
|
+
const parentName = extendsTarget(source);
|
|
4240
|
+
if (parentName) {
|
|
4225
4241
|
const parentSource = this.load(parentName);
|
|
4226
4242
|
const childBlocks = this.extractBlocks(source);
|
|
4227
4243
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -4232,9 +4248,8 @@ var init_engine = __esm({
|
|
|
4232
4248
|
if (Object.keys(this.tests).length > 0) {
|
|
4233
4249
|
context.__frond_tests__ = this.tests;
|
|
4234
4250
|
}
|
|
4235
|
-
const
|
|
4236
|
-
if (
|
|
4237
|
-
const parentName = extendsMatch[1];
|
|
4251
|
+
const parentName = extendsTarget(source);
|
|
4252
|
+
if (parentName) {
|
|
4238
4253
|
const parentSource = this.load(parentName);
|
|
4239
4254
|
const childBlocks = this.extractBlocks(source);
|
|
4240
4255
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -4279,10 +4294,93 @@ var init_engine = __esm({
|
|
|
4279
4294
|
}
|
|
4280
4295
|
return blocks;
|
|
4281
4296
|
}
|
|
4297
|
+
/**
|
|
4298
|
+
* Depth-aware block substitution against `source` (typically the
|
|
4299
|
+
* fully-resolved root template).
|
|
4300
|
+
*
|
|
4301
|
+
* A single regex `.replace()` pass (the flat `pattern` this replaces in
|
|
4302
|
+
* renderWithBlocks) pairs an OUTER block's open tag with the FIRST
|
|
4303
|
+
* `{% endblock %}` found -- which, when the outer block wraps a NESTED
|
|
4304
|
+
* `{% block %}`, is the nested block's own close tag, not the outer's.
|
|
4305
|
+
* That silently truncates the outer block's captured content and drops
|
|
4306
|
+
* everything after the inner endblock (the root-nested-block
|
|
4307
|
+
* content-loss bug). This scans with an open/close depth counter
|
|
4308
|
+
* instead (mirroring extractBlocks), so an outer block always captures
|
|
4309
|
+
* its FULL body, nested child blocks included.
|
|
4310
|
+
*
|
|
4311
|
+
* The content chosen for each block -- the child override in `blocks`
|
|
4312
|
+
* if present, else the block's own default body -- is then recursively
|
|
4313
|
+
* substituted against the SAME `blocks` map before being tokenized and
|
|
4314
|
+
* rendered, so a block nested inside another block resolves correctly
|
|
4315
|
+
* regardless of which template in the inheritance chain declared the
|
|
4316
|
+
* nesting (the root, an intermediate, however many levels deep).
|
|
4317
|
+
*
|
|
4318
|
+
* `{{ parent() }}` / `{{ super() }}` inside a block still render that
|
|
4319
|
+
* block's OWN default content at this level (lazy, on first call).
|
|
4320
|
+
*/
|
|
4321
|
+
substituteBlocks(source, blocks, context) {
|
|
4322
|
+
const blockOpen = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}/g;
|
|
4323
|
+
const blockClose = /\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
4324
|
+
const engine = this;
|
|
4325
|
+
const pieces = [];
|
|
4326
|
+
let pos = 0;
|
|
4327
|
+
while (pos < source.length) {
|
|
4328
|
+
blockOpen.lastIndex = pos;
|
|
4329
|
+
const mOpen = blockOpen.exec(source);
|
|
4330
|
+
if (!mOpen) {
|
|
4331
|
+
pieces.push(source.slice(pos));
|
|
4332
|
+
break;
|
|
4333
|
+
}
|
|
4334
|
+
pieces.push(source.slice(pos, mOpen.index));
|
|
4335
|
+
const name = mOpen[1];
|
|
4336
|
+
const contentStart = mOpen.index + mOpen[0].length;
|
|
4337
|
+
let depth = 1;
|
|
4338
|
+
let scan = contentStart;
|
|
4339
|
+
let closeMatch = null;
|
|
4340
|
+
while (depth > 0 && scan < source.length) {
|
|
4341
|
+
blockOpen.lastIndex = scan;
|
|
4342
|
+
blockClose.lastIndex = scan;
|
|
4343
|
+
const nextOpen = blockOpen.exec(source);
|
|
4344
|
+
const nextClose = blockClose.exec(source);
|
|
4345
|
+
if (!nextClose) break;
|
|
4346
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
4347
|
+
depth++;
|
|
4348
|
+
scan = nextOpen.index + nextOpen[0].length;
|
|
4349
|
+
} else {
|
|
4350
|
+
depth--;
|
|
4351
|
+
if (depth === 0) {
|
|
4352
|
+
closeMatch = nextClose;
|
|
4353
|
+
} else {
|
|
4354
|
+
scan = nextClose.index + nextClose[0].length;
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
if (!closeMatch) {
|
|
4359
|
+
pieces.push(source.slice(mOpen.index));
|
|
4360
|
+
pos = source.length;
|
|
4361
|
+
break;
|
|
4362
|
+
}
|
|
4363
|
+
const parentContent = source.slice(contentStart, closeMatch.index);
|
|
4364
|
+
const blockSource = blocks[name] ?? parentContent;
|
|
4365
|
+
const resolvedSource = engine.substituteBlocks(blockSource, blocks, context);
|
|
4366
|
+
let renderedParent = null;
|
|
4367
|
+
const getParent = () => {
|
|
4368
|
+
if (renderedParent === null) {
|
|
4369
|
+
renderedParent = new SafeString(
|
|
4370
|
+
engine.renderTokens(tokenize(parentContent), context)
|
|
4371
|
+
);
|
|
4372
|
+
}
|
|
4373
|
+
return renderedParent;
|
|
4374
|
+
};
|
|
4375
|
+
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
4376
|
+
pieces.push(engine.renderTokens(tokenize(resolvedSource), blockCtx));
|
|
4377
|
+
pos = closeMatch.index + closeMatch[0].length;
|
|
4378
|
+
}
|
|
4379
|
+
return pieces.join("");
|
|
4380
|
+
}
|
|
4282
4381
|
renderWithBlocks(parentSource, context, childBlocks) {
|
|
4283
|
-
const
|
|
4284
|
-
if (
|
|
4285
|
-
const grandparentName = extendsMatch[1];
|
|
4382
|
+
const grandparentName = extendsTarget(parentSource);
|
|
4383
|
+
if (grandparentName) {
|
|
4286
4384
|
const grandparentSource = this.load(grandparentName);
|
|
4287
4385
|
const parentBlocks = this.extractBlocks(parentSource);
|
|
4288
4386
|
const mergedBlocks = { ...parentBlocks, ...childBlocks };
|
|
@@ -4302,22 +4400,7 @@ var init_engine = __esm({
|
|
|
4302
4400
|
}
|
|
4303
4401
|
return this.renderWithBlocks(grandparentSource, context, mergedBlocks);
|
|
4304
4402
|
}
|
|
4305
|
-
const
|
|
4306
|
-
const engine = this;
|
|
4307
|
-
const result = parentSource.replace(pattern, (_match, name, parentContent) => {
|
|
4308
|
-
const blockSource = childBlocks[name] ?? parentContent;
|
|
4309
|
-
let renderedParent = null;
|
|
4310
|
-
const getParent = () => {
|
|
4311
|
-
if (renderedParent === null) {
|
|
4312
|
-
renderedParent = new SafeString(
|
|
4313
|
-
engine.renderTokens(tokenize(parentContent), context)
|
|
4314
|
-
);
|
|
4315
|
-
}
|
|
4316
|
-
return renderedParent;
|
|
4317
|
-
};
|
|
4318
|
-
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
4319
|
-
return this.renderTokens(tokenize(blockSource), blockCtx);
|
|
4320
|
-
});
|
|
4403
|
+
const result = this.substituteBlocks(parentSource, childBlocks, context);
|
|
4321
4404
|
return this.renderTokens(tokenize(result), context);
|
|
4322
4405
|
}
|
|
4323
4406
|
renderTokens(tokens, context) {
|
|
@@ -5140,6 +5223,7 @@ var init_engine = __esm({
|
|
|
5140
5223
|
const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
|
|
5141
5224
|
const cacheKey = m ? m[1] : "default";
|
|
5142
5225
|
const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
|
|
5226
|
+
sweepExpiredCache(this.fragmentCache);
|
|
5143
5227
|
const cached = this.fragmentCache.get(cacheKey);
|
|
5144
5228
|
if (cached) {
|
|
5145
5229
|
const [htmlContent, expiresAt] = cached;
|
|
@@ -5187,6 +5271,7 @@ var init_engine = __esm({
|
|
|
5187
5271
|
i++;
|
|
5188
5272
|
}
|
|
5189
5273
|
const rendered = this.renderTokens([...bodyTokens], context);
|
|
5274
|
+
capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
|
|
5190
5275
|
this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
|
|
5191
5276
|
return [rendered, i];
|
|
5192
5277
|
}
|
|
@@ -8734,14 +8819,14 @@ async function discoverRoutes(routesDir) {
|
|
|
8734
8819
|
const currentMtime = statSync5(filePath).mtimeMs;
|
|
8735
8820
|
if (_seenFiles.has(filePath) && _seenMtimes.get(filePath) === currentMtime) continue;
|
|
8736
8821
|
const method = name.toUpperCase();
|
|
8737
|
-
const
|
|
8738
|
-
const pattern = filePathToPattern(
|
|
8822
|
+
const relativePath2 = relative(routesDir, filePath);
|
|
8823
|
+
const pattern = filePathToPattern(relativePath2);
|
|
8739
8824
|
try {
|
|
8740
8825
|
const moduleUrl = `file://${filePath}?t=${currentMtime}`;
|
|
8741
8826
|
const mod = await import(moduleUrl);
|
|
8742
8827
|
const handler = mod.default ?? mod.handler;
|
|
8743
8828
|
if (typeof handler !== "function") {
|
|
8744
|
-
console.warn(` Warning: ${
|
|
8829
|
+
console.warn(` Warning: ${relativePath2} does not export a handler function, skipping`);
|
|
8745
8830
|
continue;
|
|
8746
8831
|
}
|
|
8747
8832
|
const meta = mod.meta;
|
|
@@ -8753,7 +8838,7 @@ async function discoverRoutes(routesDir) {
|
|
|
8753
8838
|
_seenMtimes.set(filePath, currentMtime);
|
|
8754
8839
|
registeredFromThisScan++;
|
|
8755
8840
|
} catch (err) {
|
|
8756
|
-
console.error(` Error loading route ${
|
|
8841
|
+
console.error(` Error loading route ${relativePath2}:`, err);
|
|
8757
8842
|
recordBrokenImport(filePath, err);
|
|
8758
8843
|
}
|
|
8759
8844
|
}
|
|
@@ -8787,8 +8872,8 @@ function recordBrokenImport(filePath, error) {
|
|
|
8787
8872
|
} catch {
|
|
8788
8873
|
}
|
|
8789
8874
|
}
|
|
8790
|
-
function filePathToPattern(
|
|
8791
|
-
const parts =
|
|
8875
|
+
function filePathToPattern(relativePath2) {
|
|
8876
|
+
const parts = relativePath2.replace(/\\/g, "/").split("/").slice(0, -1);
|
|
8792
8877
|
const urlParts = parts.map((part) => {
|
|
8793
8878
|
if (part.startsWith("[...") && part.endsWith("]")) {
|
|
8794
8879
|
const name = part.slice(4, -1);
|
|
@@ -12025,489 +12110,127 @@ import * as fs3 from "node:fs";
|
|
|
12025
12110
|
import * as path2 from "node:path";
|
|
12026
12111
|
import { spawnSync } from "node:child_process";
|
|
12027
12112
|
import { fileURLToPath } from "node:url";
|
|
12028
|
-
function
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
if (entry.isDirectory()) {
|
|
12035
|
-
if (!exclude.includes(entry.name)) {
|
|
12036
|
-
results.push(...walkFiles(fullPath, extensions, exclude));
|
|
12037
|
-
}
|
|
12038
|
-
} else if (entry.isFile()) {
|
|
12039
|
-
const ext = path2.extname(entry.name);
|
|
12040
|
-
if (extensions.includes(ext) && !entry.name.endsWith(".d.ts")) {
|
|
12041
|
-
results.push(fullPath);
|
|
12042
|
-
}
|
|
12043
|
-
}
|
|
12044
|
-
}
|
|
12045
|
-
return results;
|
|
12046
|
-
}
|
|
12047
|
-
function readFileSafe(filePath) {
|
|
12048
|
-
try {
|
|
12049
|
-
return fs3.readFileSync(filePath, "utf-8");
|
|
12050
|
-
} catch {
|
|
12051
|
-
return null;
|
|
12052
|
-
}
|
|
12053
|
-
}
|
|
12054
|
-
function relativePath(filePath, root = ".") {
|
|
12055
|
-
return path2.relative(root, filePath);
|
|
12056
|
-
}
|
|
12057
|
-
function countLines(source) {
|
|
12058
|
-
const lines = source.split("\n");
|
|
12059
|
-
let loc = 0;
|
|
12060
|
-
let blank = 0;
|
|
12061
|
-
let comment = 0;
|
|
12062
|
-
let inBlockComment = false;
|
|
12063
|
-
for (const line of lines) {
|
|
12064
|
-
const stripped = line.trim();
|
|
12065
|
-
if (!stripped) {
|
|
12066
|
-
blank++;
|
|
12067
|
-
continue;
|
|
12068
|
-
}
|
|
12069
|
-
if (inBlockComment) {
|
|
12070
|
-
comment++;
|
|
12071
|
-
if (stripped.includes("*/")) {
|
|
12072
|
-
inBlockComment = false;
|
|
12073
|
-
}
|
|
12074
|
-
continue;
|
|
12075
|
-
}
|
|
12076
|
-
if (stripped.startsWith("/*")) {
|
|
12077
|
-
comment++;
|
|
12078
|
-
if (!stripped.includes("*/") || stripped.endsWith("/*")) {
|
|
12079
|
-
inBlockComment = true;
|
|
12080
|
-
}
|
|
12081
|
-
continue;
|
|
12082
|
-
}
|
|
12083
|
-
if (stripped.startsWith("//")) {
|
|
12084
|
-
comment++;
|
|
12085
|
-
continue;
|
|
12086
|
-
}
|
|
12087
|
-
loc++;
|
|
12088
|
-
}
|
|
12089
|
-
return { loc, blank, comment };
|
|
12090
|
-
}
|
|
12091
|
-
function stripLiterals(source) {
|
|
12092
|
-
const out = [];
|
|
12093
|
-
const n = source.length;
|
|
12094
|
-
let i = 0;
|
|
12095
|
-
let prevSignificant = "";
|
|
12096
|
-
let prevWord = "";
|
|
12097
|
-
const regexKeywords = /* @__PURE__ */ new Set([
|
|
12098
|
-
"return",
|
|
12099
|
-
"typeof",
|
|
12100
|
-
"instanceof",
|
|
12101
|
-
"in",
|
|
12102
|
-
"of",
|
|
12103
|
-
"new",
|
|
12104
|
-
"delete",
|
|
12105
|
-
"void",
|
|
12106
|
-
"throw",
|
|
12107
|
-
"case",
|
|
12108
|
-
"do",
|
|
12109
|
-
"else",
|
|
12110
|
-
"yield",
|
|
12111
|
-
"await"
|
|
12112
|
-
]);
|
|
12113
|
-
function prevEndsExpression() {
|
|
12114
|
-
if (prevSignificant === "") return false;
|
|
12115
|
-
if (/[A-Za-z0-9_$]/.test(prevSignificant)) {
|
|
12116
|
-
return !regexKeywords.has(prevWord);
|
|
12117
|
-
}
|
|
12118
|
-
if (prevSignificant === ")" || prevSignificant === "]") return true;
|
|
12119
|
-
if (prevSignificant === ".") return true;
|
|
12120
|
-
return false;
|
|
12121
|
-
}
|
|
12122
|
-
while (i < n) {
|
|
12123
|
-
const ch = source[i];
|
|
12124
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
12125
|
-
if (ch === "/" && next === "/") {
|
|
12126
|
-
out.push("//");
|
|
12127
|
-
i += 2;
|
|
12128
|
-
while (i < n && source[i] !== "\n") {
|
|
12129
|
-
out.push(" ");
|
|
12130
|
-
i++;
|
|
12131
|
-
}
|
|
12132
|
-
continue;
|
|
12133
|
-
}
|
|
12134
|
-
if (ch === "/" && next === "*") {
|
|
12135
|
-
out.push("/*");
|
|
12136
|
-
i += 2;
|
|
12137
|
-
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) {
|
|
12138
|
-
out.push(source[i] === "\n" ? "\n" : " ");
|
|
12139
|
-
i++;
|
|
12140
|
-
}
|
|
12141
|
-
if (i < n) {
|
|
12142
|
-
out.push("*/");
|
|
12143
|
-
i += 2;
|
|
12144
|
-
}
|
|
12145
|
-
continue;
|
|
12146
|
-
}
|
|
12147
|
-
if (ch === '"' || ch === "'") {
|
|
12148
|
-
const quote = ch;
|
|
12149
|
-
out.push(quote);
|
|
12150
|
-
i++;
|
|
12151
|
-
while (i < n && source[i] !== quote) {
|
|
12152
|
-
if (source[i] === "\\" && i + 1 < n) {
|
|
12153
|
-
out.push(" ");
|
|
12154
|
-
i += 2;
|
|
12155
|
-
continue;
|
|
12156
|
-
}
|
|
12157
|
-
if (source[i] === "\n") {
|
|
12158
|
-
out.push("\n");
|
|
12159
|
-
i++;
|
|
12160
|
-
break;
|
|
12161
|
-
}
|
|
12162
|
-
out.push(" ");
|
|
12163
|
-
i++;
|
|
12164
|
-
}
|
|
12165
|
-
if (i < n && source[i] === quote) {
|
|
12166
|
-
out.push(quote);
|
|
12167
|
-
i++;
|
|
12168
|
-
}
|
|
12169
|
-
prevSignificant = quote;
|
|
12170
|
-
prevWord = "";
|
|
12171
|
-
continue;
|
|
12172
|
-
}
|
|
12173
|
-
if (ch === "`") {
|
|
12174
|
-
out.push("`");
|
|
12175
|
-
i++;
|
|
12176
|
-
while (i < n && source[i] !== "`") {
|
|
12177
|
-
if (source[i] === "\\" && i + 1 < n) {
|
|
12178
|
-
out.push(source[i + 1] === "\n" ? " \n" : " ");
|
|
12179
|
-
i += 2;
|
|
12180
|
-
continue;
|
|
12181
|
-
}
|
|
12182
|
-
if (source[i] === "$" && source[i + 1] === "{") {
|
|
12183
|
-
out.push("${");
|
|
12184
|
-
i += 2;
|
|
12185
|
-
let depth = 1;
|
|
12186
|
-
const exprStart = i;
|
|
12187
|
-
while (i < n && depth > 0) {
|
|
12188
|
-
if (source[i] === "{") depth++;
|
|
12189
|
-
else if (source[i] === "}") depth--;
|
|
12190
|
-
if (depth === 0) break;
|
|
12191
|
-
i++;
|
|
12192
|
-
}
|
|
12193
|
-
out.push(stripLiterals(source.slice(exprStart, i)));
|
|
12194
|
-
if (i < n && source[i] === "}") {
|
|
12195
|
-
out.push("}");
|
|
12196
|
-
i++;
|
|
12197
|
-
}
|
|
12198
|
-
continue;
|
|
12199
|
-
}
|
|
12200
|
-
out.push(source[i] === "\n" ? "\n" : " ");
|
|
12201
|
-
i++;
|
|
12202
|
-
}
|
|
12203
|
-
if (i < n && source[i] === "`") {
|
|
12204
|
-
out.push("`");
|
|
12205
|
-
i++;
|
|
12206
|
-
}
|
|
12207
|
-
prevSignificant = "`";
|
|
12208
|
-
prevWord = "";
|
|
12209
|
-
continue;
|
|
12210
|
-
}
|
|
12211
|
-
if (ch === "/" && !prevEndsExpression()) {
|
|
12212
|
-
let j = i + 1;
|
|
12213
|
-
let ok = false;
|
|
12214
|
-
let inClass = false;
|
|
12215
|
-
while (j < n) {
|
|
12216
|
-
const c = source[j];
|
|
12217
|
-
if (c === "\\") {
|
|
12218
|
-
j += 2;
|
|
12219
|
-
continue;
|
|
12220
|
-
}
|
|
12221
|
-
if (c === "\n") break;
|
|
12222
|
-
if (c === "[") inClass = true;
|
|
12223
|
-
else if (c === "]") inClass = false;
|
|
12224
|
-
else if (c === "/" && !inClass) {
|
|
12225
|
-
ok = true;
|
|
12226
|
-
break;
|
|
12227
|
-
}
|
|
12228
|
-
j++;
|
|
12229
|
-
}
|
|
12230
|
-
if (ok) {
|
|
12231
|
-
out.push("/");
|
|
12232
|
-
for (let k = i + 1; k < j; k++) out.push(" ");
|
|
12233
|
-
out.push("/");
|
|
12234
|
-
i = j + 1;
|
|
12235
|
-
while (i < n && /[a-z]/i.test(source[i])) {
|
|
12236
|
-
out.push(source[i]);
|
|
12237
|
-
i++;
|
|
12238
|
-
}
|
|
12239
|
-
prevSignificant = "/";
|
|
12240
|
-
prevWord = "";
|
|
12241
|
-
continue;
|
|
12242
|
-
}
|
|
12243
|
-
}
|
|
12244
|
-
out.push(ch);
|
|
12245
|
-
if (!/\s/.test(ch)) {
|
|
12246
|
-
prevSignificant = ch;
|
|
12247
|
-
if (/[A-Za-z0-9_$]/.test(ch)) {
|
|
12248
|
-
prevWord = /[A-Za-z0-9_$]/.test(source[i - 1] ?? "") ? prevWord + ch : ch;
|
|
12249
|
-
} else {
|
|
12250
|
-
prevWord = "";
|
|
12251
|
-
}
|
|
12252
|
-
}
|
|
12253
|
-
i++;
|
|
12113
|
+
function containsTypeScript(directory) {
|
|
12114
|
+
if (!fs3.existsSync(directory) || !fs3.statSync(directory).isDirectory()) return false;
|
|
12115
|
+
for (const entry of fs3.readdirSync(directory, { withFileTypes: true })) {
|
|
12116
|
+
if (["node_modules", ".git", "dist", "build"].includes(entry.name)) continue;
|
|
12117
|
+
const target = path2.join(directory, entry.name);
|
|
12118
|
+
if (entry.isDirectory() ? containsTypeScript(target) : /\.[cm]?[jt]sx?$/.test(entry.name)) return true;
|
|
12254
12119
|
}
|
|
12255
|
-
return
|
|
12256
|
-
}
|
|
12257
|
-
function countClassesQuick(source) {
|
|
12258
|
-
const matches = source.match(
|
|
12259
|
-
/(?:^|\n)\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/g
|
|
12260
|
-
);
|
|
12261
|
-
return matches ? matches.length : 0;
|
|
12262
|
-
}
|
|
12263
|
-
function countFunctionsQuick(source) {
|
|
12264
|
-
const clean = stripLiterals(source);
|
|
12265
|
-
let count = 0;
|
|
12266
|
-
const funcDecls = clean.match(
|
|
12267
|
-
/(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(/g
|
|
12268
|
-
);
|
|
12269
|
-
if (funcDecls) count += funcDecls.length;
|
|
12270
|
-
const methods = clean.match(
|
|
12271
|
-
/(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*\([^)]*\)\s*(?::\s*\S+)?\s*\{/g
|
|
12272
|
-
);
|
|
12273
|
-
if (methods) count += methods.length;
|
|
12274
|
-
const arrows = clean.match(
|
|
12275
|
-
/(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\(/g
|
|
12276
|
-
);
|
|
12277
|
-
if (arrows) count += arrows.length;
|
|
12278
|
-
return count;
|
|
12279
|
-
}
|
|
12280
|
-
function resolveRoot(root = "src") {
|
|
12281
|
-
const rootPath = path2.resolve(root);
|
|
12282
|
-
if (fs3.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
|
|
12283
|
-
_lastScanRoot = rootPath;
|
|
12284
|
-
return root;
|
|
12285
|
-
}
|
|
12286
|
-
const fwDir = path2.resolve(path2.dirname(new URL(import.meta.url).pathname));
|
|
12287
|
-
_lastScanRoot = fwDir;
|
|
12288
|
-
return fwDir;
|
|
12289
|
-
}
|
|
12290
|
-
function quickMetrics(root = "src") {
|
|
12291
|
-
root = resolveRoot(root);
|
|
12292
|
-
const rootPath = path2.resolve(root);
|
|
12293
|
-
if (!fs3.existsSync(rootPath)) {
|
|
12294
|
-
return { error: `Directory not found: ${root}` };
|
|
12295
|
-
}
|
|
12296
|
-
const tsFiles = walkFiles(rootPath, [".ts", ".js"]);
|
|
12297
|
-
const twigFiles = walkFiles(rootPath, [".twig", ".html"]);
|
|
12298
|
-
const migrationsDir = path2.resolve("migrations");
|
|
12299
|
-
const migrationFiles = [
|
|
12300
|
-
...walkFiles(migrationsDir, [".sql"]),
|
|
12301
|
-
...walkFiles(migrationsDir, [".ts"])
|
|
12302
|
-
];
|
|
12303
|
-
const scssFiles = walkFiles(rootPath, [".scss", ".css"]);
|
|
12304
|
-
let totalLoc = 0;
|
|
12305
|
-
let totalBlank = 0;
|
|
12306
|
-
let totalComment = 0;
|
|
12307
|
-
let totalClasses = 0;
|
|
12308
|
-
let totalFunctions = 0;
|
|
12309
|
-
const fileDetails = [];
|
|
12310
|
-
for (const f of tsFiles) {
|
|
12311
|
-
const source = readFileSafe(f);
|
|
12312
|
-
if (source === null) continue;
|
|
12313
|
-
const counts = countLines(source);
|
|
12314
|
-
const classes = countClassesQuick(source);
|
|
12315
|
-
const functions = countFunctionsQuick(source);
|
|
12316
|
-
totalLoc += counts.loc;
|
|
12317
|
-
totalBlank += counts.blank;
|
|
12318
|
-
totalComment += counts.comment;
|
|
12319
|
-
totalClasses += classes;
|
|
12320
|
-
totalFunctions += functions;
|
|
12321
|
-
fileDetails.push({
|
|
12322
|
-
path: relativePath(f, rootPath),
|
|
12323
|
-
loc: counts.loc,
|
|
12324
|
-
blank: counts.blank,
|
|
12325
|
-
comment: counts.comment,
|
|
12326
|
-
classes,
|
|
12327
|
-
functions
|
|
12328
|
-
});
|
|
12329
|
-
}
|
|
12330
|
-
fileDetails.sort((a, b) => b.loc - a.loc);
|
|
12331
|
-
let routeCount = 0;
|
|
12332
|
-
let ormCount = 0;
|
|
12333
|
-
for (const f of tsFiles) {
|
|
12334
|
-
const source = readFileSafe(f);
|
|
12335
|
-
if (source === null) continue;
|
|
12336
|
-
const routes = source.match(
|
|
12337
|
-
/(?:router\s*\.\s*(?:get|post|put|delete|patch|any)\s*\(|@(?:get|post|put|delete|patch)\s*\()/g
|
|
12338
|
-
);
|
|
12339
|
-
if (routes) routeCount += routes.length;
|
|
12340
|
-
const orms = source.match(
|
|
12341
|
-
/class\s+\w+\s+extends\s+(?:ORM|Model)\b/g
|
|
12342
|
-
);
|
|
12343
|
-
if (orms) ormCount += orms.length;
|
|
12344
|
-
}
|
|
12345
|
-
const breakdown = {
|
|
12346
|
-
typescript: tsFiles.filter((f) => f.endsWith(".ts")).length,
|
|
12347
|
-
javascript: tsFiles.filter((f) => f.endsWith(".js")).length,
|
|
12348
|
-
templates: twigFiles.length,
|
|
12349
|
-
migrations: migrationFiles.length,
|
|
12350
|
-
stylesheets: scssFiles.length
|
|
12351
|
-
};
|
|
12352
|
-
return {
|
|
12353
|
-
file_count: tsFiles.length,
|
|
12354
|
-
total_loc: totalLoc,
|
|
12355
|
-
total_blank: totalBlank,
|
|
12356
|
-
total_comment: totalComment,
|
|
12357
|
-
lloc: totalLoc,
|
|
12358
|
-
classes: totalClasses,
|
|
12359
|
-
functions: totalFunctions,
|
|
12360
|
-
route_count: routeCount,
|
|
12361
|
-
orm_count: ormCount,
|
|
12362
|
-
template_count: twigFiles.length,
|
|
12363
|
-
migration_count: migrationFiles.length,
|
|
12364
|
-
avg_file_size: tsFiles.length > 0 ? Math.round(totalLoc / tsFiles.length * 10) / 10 : 0,
|
|
12365
|
-
largest_files: fileDetails.slice(0, 10),
|
|
12366
|
-
breakdown
|
|
12367
|
-
};
|
|
12120
|
+
return false;
|
|
12368
12121
|
}
|
|
12369
|
-
function
|
|
12370
|
-
const resolved =
|
|
12371
|
-
const
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
return [resolved, scanningFramework ? "framework" : "project"];
|
|
12122
|
+
function resolveTarget(root = "src") {
|
|
12123
|
+
const resolved = containsTypeScript(root) ? path2.resolve(root) : path2.dirname(fileURLToPath(import.meta.url));
|
|
12124
|
+
const mode = containsTypeScript(root) ? "project" : "framework";
|
|
12125
|
+
lastScanRoot = resolved;
|
|
12126
|
+
return [resolved, mode];
|
|
12375
12127
|
}
|
|
12376
12128
|
function enginePath() {
|
|
12377
|
-
const names = process.platform === "win32" ? ["tina4.exe", "tina4
|
|
12378
|
-
for (const
|
|
12379
|
-
if (!dir) continue;
|
|
12129
|
+
const names = process.platform === "win32" ? ["tina4.exe", "tina4"] : ["tina4"];
|
|
12130
|
+
for (const directory of (process.env.PATH || "").split(path2.delimiter)) {
|
|
12380
12131
|
for (const name of names) {
|
|
12381
|
-
const candidate = path2.join(
|
|
12132
|
+
const candidate = path2.join(directory, name);
|
|
12382
12133
|
try {
|
|
12383
|
-
if (!fs3.statSync(candidate).isFile()) continue;
|
|
12384
12134
|
fs3.accessSync(candidate, fs3.constants.X_OK);
|
|
12135
|
+
if (!fs3.statSync(candidate).isFile()) continue;
|
|
12136
|
+
const descriptor = fs3.openSync(candidate, "r");
|
|
12137
|
+
const header = Buffer.alloc(2);
|
|
12138
|
+
fs3.readSync(descriptor, header, 0, 2, 0);
|
|
12139
|
+
fs3.closeSync(descriptor);
|
|
12140
|
+
if (header.toString("latin1") !== "#!") return candidate;
|
|
12385
12141
|
} catch {
|
|
12386
12142
|
continue;
|
|
12387
12143
|
}
|
|
12388
|
-
try {
|
|
12389
|
-
const fd = fs3.openSync(candidate, "r");
|
|
12390
|
-
const buf = Buffer.alloc(2);
|
|
12391
|
-
fs3.readSync(fd, buf, 0, 2, 0);
|
|
12392
|
-
fs3.closeSync(fd);
|
|
12393
|
-
if (buf.toString("latin1") === "#!") continue;
|
|
12394
|
-
} catch {
|
|
12395
|
-
}
|
|
12396
|
-
return candidate;
|
|
12397
12144
|
}
|
|
12398
12145
|
}
|
|
12399
12146
|
return null;
|
|
12400
12147
|
}
|
|
12401
12148
|
function runEngine(target) {
|
|
12402
12149
|
const binary = enginePath();
|
|
12403
|
-
if (binary
|
|
12404
|
-
|
|
12405
|
-
}
|
|
12406
|
-
const proc = spawnSync(binary, ["metrics", "--path", target, "--json"], {
|
|
12150
|
+
if (!binary) throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
|
|
12151
|
+
const processResult = spawnSync(binary, ["metrics", "--path", target, "--json"], {
|
|
12407
12152
|
encoding: "utf8",
|
|
12408
|
-
timeout:
|
|
12153
|
+
timeout: 6e4,
|
|
12409
12154
|
maxBuffer: 64 * 1024 * 1024
|
|
12410
12155
|
});
|
|
12411
|
-
if (
|
|
12412
|
-
|
|
12413
|
-
if (err.code === "ETIMEDOUT") {
|
|
12414
|
-
throw new MetricsEngineError(`tina4 metrics timed out after ${TIMEOUT_MS / 1e3}s on ${target}`);
|
|
12415
|
-
}
|
|
12416
|
-
throw new MetricsEngineError(`could not run ${binary}: ${err.message}`);
|
|
12156
|
+
if (processResult.error) {
|
|
12157
|
+
throw new MetricsEngineError(`could not run ${binary}: ${processResult.error.message}`);
|
|
12417
12158
|
}
|
|
12418
|
-
if (
|
|
12419
|
-
const detail = (
|
|
12420
|
-
throw new MetricsEngineError(
|
|
12421
|
-
`tina4 metrics failed on ${target}: ${detail || `exit code ${proc.status}`}`
|
|
12422
|
-
);
|
|
12423
|
-
}
|
|
12424
|
-
if (!proc.stdout || !proc.stdout.trim()) {
|
|
12425
|
-
throw new MetricsEngineError(`tina4 metrics produced no output for ${target}`);
|
|
12159
|
+
if (processResult.status !== 0) {
|
|
12160
|
+
const detail = (processResult.stderr || processResult.stdout || "").trim().split("\n")[0];
|
|
12161
|
+
throw new MetricsEngineError(`tina4 metrics failed on ${target}: ${detail || processResult.status}`);
|
|
12426
12162
|
}
|
|
12427
|
-
let payload;
|
|
12428
12163
|
try {
|
|
12429
|
-
payload = JSON.parse(
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12164
|
+
const payload = JSON.parse(processResult.stdout);
|
|
12165
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
12166
|
+
throw new Error("non-object payload");
|
|
12167
|
+
}
|
|
12168
|
+
return payload;
|
|
12169
|
+
} catch (error) {
|
|
12170
|
+
throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${error.message}`);
|
|
12435
12171
|
}
|
|
12436
|
-
return payload;
|
|
12437
12172
|
}
|
|
12438
|
-
function
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
if (!ok) {
|
|
12442
|
-
throw new MetricsEngineError(
|
|
12443
|
-
`engine payload has no usable '${key}' - the installed tina4 CLI predates a field the dashboard renders. Update it: ${INSTALL_HINT}`
|
|
12444
|
-
);
|
|
12173
|
+
function requireArray(payload, key) {
|
|
12174
|
+
if (!Array.isArray(payload[key])) {
|
|
12175
|
+
throw new MetricsEngineError(`engine payload has no usable '${key}' - ${INSTALL_HINT}`);
|
|
12445
12176
|
}
|
|
12446
|
-
return
|
|
12177
|
+
return payload[key];
|
|
12447
12178
|
}
|
|
12448
12179
|
function fullAnalysis(root = "src") {
|
|
12449
|
-
const [resolved, scanMode] =
|
|
12180
|
+
const [resolved, scanMode] = resolveTarget(root);
|
|
12450
12181
|
const payload = runEngine(resolved);
|
|
12451
|
-
const summary =
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
12455
|
-
|
|
12456
|
-
|
|
12457
|
-
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
if (
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
if (functions.length) {
|
|
12465
|
-
const absent = FUNCTION_KEYS.filter((k) => !(k in functions[0]));
|
|
12466
|
-
if (absent.length) throw new MetricsEngineError(`engine function metrics are missing ${absent.join(", ")}`);
|
|
12182
|
+
const summary = payload.summary;
|
|
12183
|
+
if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
|
|
12184
|
+
throw new MetricsEngineError(`engine payload has no usable 'summary' - ${INSTALL_HINT}`);
|
|
12185
|
+
}
|
|
12186
|
+
const fileMetrics = requireArray(payload, "file_metrics");
|
|
12187
|
+
const functions = requireArray(payload, "most_complex_functions");
|
|
12188
|
+
const missingSummary = SUMMARY_KEYS.filter((key) => !(key in summary));
|
|
12189
|
+
if (missingSummary.length) throw new MetricsEngineError(`engine summary is missing ${missingSummary.join(", ")}`);
|
|
12190
|
+
const missingFile = fileMetrics.length ? FILE_KEYS.filter((key) => !(key in fileMetrics[0])) : [];
|
|
12191
|
+
if (missingFile.length) throw new MetricsEngineError(`engine file_metrics is missing ${missingFile.join(", ")}`);
|
|
12192
|
+
const missingFunction = functions.length ? FUNCTION_KEYS.filter((key) => !(key in functions[0])) : [];
|
|
12193
|
+
if (missingFunction.length) {
|
|
12194
|
+
throw new MetricsEngineError(`engine function metrics are missing ${missingFunction.join(", ")}`);
|
|
12467
12195
|
}
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12196
|
+
return {
|
|
12197
|
+
...Object.fromEntries(SUMMARY_KEYS.map((key) => [key, summary[key]])),
|
|
12198
|
+
file_metrics: fileMetrics,
|
|
12199
|
+
most_complex_functions: functions.slice(0, 15),
|
|
12200
|
+
dependency_graph: payload.dependency_graph || {},
|
|
12201
|
+
scan_mode: scanMode,
|
|
12202
|
+
scan_root: resolved,
|
|
12203
|
+
engine: "tina4-cli"
|
|
12204
|
+
};
|
|
12477
12205
|
}
|
|
12478
12206
|
function fileDetail(filePath) {
|
|
12479
12207
|
if (!filePath) throw new MetricsEngineError("fileDetail needs a path");
|
|
12480
12208
|
let target = filePath;
|
|
12481
|
-
if (!fs3.existsSync(target) &&
|
|
12482
|
-
const candidate = path2.join(_lastScanRoot, filePath);
|
|
12483
|
-
if (fs3.existsSync(candidate)) target = candidate;
|
|
12484
|
-
}
|
|
12209
|
+
if (!fs3.existsSync(target) && lastScanRoot) target = path2.join(lastScanRoot, filePath);
|
|
12485
12210
|
if (!fs3.existsSync(target)) throw new MetricsEngineError(`no such file: ${filePath}`);
|
|
12486
12211
|
if (fs3.statSync(target).isDirectory()) throw new MetricsEngineError(`not a file: ${filePath}`);
|
|
12487
12212
|
const payload = runEngine(target);
|
|
12488
|
-
const
|
|
12489
|
-
if (!
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12213
|
+
const files = requireArray(payload, "file_metrics");
|
|
12214
|
+
if (!files.length) throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
|
|
12215
|
+
return {
|
|
12216
|
+
...files[0],
|
|
12217
|
+
function_count: files[0].functions || 0,
|
|
12218
|
+
functions: requireArray(payload, "most_complex_functions"),
|
|
12219
|
+
engine: "tina4-cli"
|
|
12220
|
+
};
|
|
12493
12221
|
}
|
|
12494
|
-
var
|
|
12222
|
+
var lastScanRoot, MetricsEngineError, INSTALL_HINT, SUMMARY_KEYS, FILE_KEYS, FUNCTION_KEYS;
|
|
12495
12223
|
var init_metrics = __esm({
|
|
12496
12224
|
"../core/src/metrics.ts"() {
|
|
12497
12225
|
"use strict";
|
|
12498
|
-
|
|
12226
|
+
lastScanRoot = "";
|
|
12499
12227
|
MetricsEngineError = class extends Error {
|
|
12500
12228
|
constructor(message) {
|
|
12501
12229
|
super(message);
|
|
12502
12230
|
this.name = "MetricsEngineError";
|
|
12503
12231
|
}
|
|
12504
12232
|
};
|
|
12505
|
-
|
|
12506
|
-
INSTALL_HINT = [
|
|
12507
|
-
"the tina4 CLI provides the metrics engine (ADR-0002). Install it with",
|
|
12508
|
-
" curl -fsSL https://tina4.com/install.sh | sh",
|
|
12509
|
-
"or see https://tina4.com/cli"
|
|
12510
|
-
].join("\n");
|
|
12233
|
+
INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
|
|
12511
12234
|
SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
|
|
12512
12235
|
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_tests"];
|
|
12513
12236
|
FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
|
|
@@ -12515,7 +12238,7 @@ var init_metrics = __esm({
|
|
|
12515
12238
|
});
|
|
12516
12239
|
|
|
12517
12240
|
// ../core/src/feedback.ts
|
|
12518
|
-
import { readFileSync as
|
|
12241
|
+
import { readFileSync as readFileSync10, existsSync as existsSync11 } from "node:fs";
|
|
12519
12242
|
import { dirname as dirname5, join as join13, resolve as resolve5 } from "node:path";
|
|
12520
12243
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
12521
12244
|
function feedbackEnabled() {
|
|
@@ -12656,7 +12379,7 @@ var init_feedback = __esm({
|
|
|
12656
12379
|
handleFeedbackWidgetJs = (_req, res) => {
|
|
12657
12380
|
let body;
|
|
12658
12381
|
if (existsSync11(WIDGET_BUNDLE_PATH)) {
|
|
12659
|
-
body =
|
|
12382
|
+
body = readFileSync10(WIDGET_BUNDLE_PATH);
|
|
12660
12383
|
} else {
|
|
12661
12384
|
body = "console.warn('tina4-feedback-widget bundle not built yet');";
|
|
12662
12385
|
}
|
|
@@ -12671,7 +12394,7 @@ var init_feedback = __esm({
|
|
|
12671
12394
|
});
|
|
12672
12395
|
|
|
12673
12396
|
// ../core/src/version.ts
|
|
12674
|
-
import { existsSync as existsSync12, readFileSync as
|
|
12397
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
|
|
12675
12398
|
import { dirname as dirname6, join as join14 } from "node:path";
|
|
12676
12399
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
12677
12400
|
function resolveFrameworkVersion() {
|
|
@@ -12680,7 +12403,7 @@ function resolveFrameworkVersion() {
|
|
|
12680
12403
|
const pkgPath = join14(dir, "package.json");
|
|
12681
12404
|
if (existsSync12(pkgPath)) {
|
|
12682
12405
|
try {
|
|
12683
|
-
const pkg = JSON.parse(
|
|
12406
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
12684
12407
|
if (typeof pkg.version === "string" && pkg.version) return pkg.version;
|
|
12685
12408
|
} catch {
|
|
12686
12409
|
}
|
|
@@ -15468,8 +15191,8 @@ __export(context_exports, {
|
|
|
15468
15191
|
fts5Supported: () => fts5Supported
|
|
15469
15192
|
});
|
|
15470
15193
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
15471
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as
|
|
15472
|
-
import { basename as basename4, dirname as dirname8, extname as
|
|
15194
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync5 } from "node:fs";
|
|
15195
|
+
import { basename as basename4, dirname as dirname8, extname as extname4, isAbsolute as isAbsolute4, join as join16, relative as relative3, resolve as resolve7 } from "node:path";
|
|
15473
15196
|
function fts5Supported() {
|
|
15474
15197
|
try {
|
|
15475
15198
|
const conn = new DatabaseSync2(":memory:");
|
|
@@ -15604,7 +15327,7 @@ var init_context = __esm({
|
|
|
15604
15327
|
}
|
|
15605
15328
|
// ── indexing ───────────────────────────────────────────────
|
|
15606
15329
|
static chunksFor(label, text) {
|
|
15607
|
-
const ext =
|
|
15330
|
+
const ext = extname4(label).toLowerCase();
|
|
15608
15331
|
const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
|
|
15609
15332
|
if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
|
|
15610
15333
|
return chunkCode(text, label);
|
|
@@ -15622,7 +15345,7 @@ var init_context = __esm({
|
|
|
15622
15345
|
const stored = label != null ? String(label) : String(file);
|
|
15623
15346
|
let text;
|
|
15624
15347
|
try {
|
|
15625
|
-
text =
|
|
15348
|
+
text = readFileSync13(file, "utf-8");
|
|
15626
15349
|
} catch {
|
|
15627
15350
|
return 0;
|
|
15628
15351
|
}
|
|
@@ -15643,7 +15366,7 @@ var init_context = __esm({
|
|
|
15643
15366
|
static eligible(filename) {
|
|
15644
15367
|
const fn = filename.toLowerCase();
|
|
15645
15368
|
if (fn.endsWith(".min.js")) return false;
|
|
15646
|
-
const ext =
|
|
15369
|
+
const ext = extname4(fn);
|
|
15647
15370
|
return CODE_EXTS.has(ext) || DOC_EXTS.has(ext) || CONFIG_EXTS.has(ext) || SPECIAL_FILES.has(fn);
|
|
15648
15371
|
}
|
|
15649
15372
|
/**
|
|
@@ -15672,7 +15395,7 @@ var init_context = __esm({
|
|
|
15672
15395
|
for (const fn of files) {
|
|
15673
15396
|
if (!_Context.eligible(fn)) continue;
|
|
15674
15397
|
const full = join16(dir, fn);
|
|
15675
|
-
const rel =
|
|
15398
|
+
const rel = relative3(rootAbs, full);
|
|
15676
15399
|
total += this.indexPath(full, rel);
|
|
15677
15400
|
}
|
|
15678
15401
|
for (const d of subdirs) walk2(join16(dir, d));
|
|
@@ -15693,7 +15416,7 @@ var init_context = __esm({
|
|
|
15693
15416
|
const raw = String(changedPath);
|
|
15694
15417
|
const abs = isAbsolute4(raw) ? raw : join16(process.cwd(), raw);
|
|
15695
15418
|
const resolved = realResolve(resolve7(abs));
|
|
15696
|
-
const rel =
|
|
15419
|
+
const rel = relative3(this.root, resolved);
|
|
15697
15420
|
if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) {
|
|
15698
15421
|
return -1;
|
|
15699
15422
|
}
|
|
@@ -17615,7 +17338,7 @@ var init_job = __esm({
|
|
|
17615
17338
|
});
|
|
17616
17339
|
|
|
17617
17340
|
// ../core/src/queueBackends/liteBackend.ts
|
|
17618
|
-
import { mkdirSync as mkdirSync10, readdirSync as readdirSync8, readFileSync as
|
|
17341
|
+
import { mkdirSync as mkdirSync10, readdirSync as readdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8, unlinkSync as unlinkSync6, existsSync as existsSync15 } from "node:fs";
|
|
17619
17342
|
import { join as join17 } from "node:path";
|
|
17620
17343
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
17621
17344
|
var LiteBackend;
|
|
@@ -17715,7 +17438,7 @@ var init_liteBackend = __esm({
|
|
|
17715
17438
|
const filePath = join17(dir, filename);
|
|
17716
17439
|
let job;
|
|
17717
17440
|
try {
|
|
17718
|
-
job = JSON.parse(
|
|
17441
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
17719
17442
|
} catch {
|
|
17720
17443
|
continue;
|
|
17721
17444
|
}
|
|
@@ -17779,7 +17502,7 @@ var init_liteBackend = __esm({
|
|
|
17779
17502
|
const filePath = join17(reservedDir, filename);
|
|
17780
17503
|
let record;
|
|
17781
17504
|
try {
|
|
17782
|
-
record = JSON.parse(
|
|
17505
|
+
record = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
17783
17506
|
} catch {
|
|
17784
17507
|
continue;
|
|
17785
17508
|
}
|
|
@@ -17892,7 +17615,7 @@ var init_liteBackend = __esm({
|
|
|
17892
17615
|
let count = 0;
|
|
17893
17616
|
for (const file of files) {
|
|
17894
17617
|
try {
|
|
17895
|
-
const job = JSON.parse(
|
|
17618
|
+
const job = JSON.parse(readFileSync14(join17(scanDir, file), "utf-8"));
|
|
17896
17619
|
if (job.status === status2) count++;
|
|
17897
17620
|
} catch {
|
|
17898
17621
|
}
|
|
@@ -17949,7 +17672,7 @@ var init_liteBackend = __esm({
|
|
|
17949
17672
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
17950
17673
|
for (const file of files) {
|
|
17951
17674
|
try {
|
|
17952
|
-
const job = JSON.parse(
|
|
17675
|
+
const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
|
|
17953
17676
|
const attempts = job.attempts || 0;
|
|
17954
17677
|
if (attempts > 0 && attempts < maxRetries) {
|
|
17955
17678
|
results.push(job);
|
|
@@ -17975,7 +17698,7 @@ var init_liteBackend = __esm({
|
|
|
17975
17698
|
const failedDir = join17(this.basePath, q, "failed");
|
|
17976
17699
|
const filePath = join17(failedDir, `${jobId}.queue-data`);
|
|
17977
17700
|
if (existsSync15(filePath)) {
|
|
17978
|
-
const job = JSON.parse(
|
|
17701
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
17979
17702
|
job.status = "pending";
|
|
17980
17703
|
job.attempts = (job.attempts || 0) + 1;
|
|
17981
17704
|
job.error = void 0;
|
|
@@ -17999,7 +17722,7 @@ var init_liteBackend = __esm({
|
|
|
17999
17722
|
const files = readdirSync8(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
18000
17723
|
for (const file of files) {
|
|
18001
17724
|
try {
|
|
18002
|
-
const job = JSON.parse(
|
|
17725
|
+
const job = JSON.parse(readFileSync14(join17(failedDir, file), "utf-8"));
|
|
18003
17726
|
if ((job.attempts || 0) >= maxRetries) {
|
|
18004
17727
|
job.status = "dead";
|
|
18005
17728
|
results.push(job);
|
|
@@ -18033,7 +17756,7 @@ var init_liteBackend = __esm({
|
|
|
18033
17756
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".queue-data"));
|
|
18034
17757
|
for (const file of files) {
|
|
18035
17758
|
try {
|
|
18036
|
-
const job = JSON.parse(
|
|
17759
|
+
const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
|
|
18037
17760
|
if (job.status === status2) {
|
|
18038
17761
|
unlinkSync6(join17(dir, file));
|
|
18039
17762
|
count++;
|
|
@@ -18060,7 +17783,7 @@ var init_liteBackend = __esm({
|
|
|
18060
17783
|
for (const file of files) {
|
|
18061
17784
|
try {
|
|
18062
17785
|
const filePath = join17(failedDir, file);
|
|
18063
|
-
const job = JSON.parse(
|
|
17786
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
18064
17787
|
if ((job.attempts || 0) >= maxRetries) {
|
|
18065
17788
|
continue;
|
|
18066
17789
|
}
|
|
@@ -18092,7 +17815,7 @@ var init_liteBackend = __esm({
|
|
|
18092
17815
|
const filePath = join17(dir, file);
|
|
18093
17816
|
let job;
|
|
18094
17817
|
try {
|
|
18095
|
-
job = JSON.parse(
|
|
17818
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
18096
17819
|
} catch {
|
|
18097
17820
|
continue;
|
|
18098
17821
|
}
|
|
@@ -19555,7 +19278,7 @@ function detectVersion(projectRoot3) {
|
|
|
19555
19278
|
}
|
|
19556
19279
|
return "0.0.0";
|
|
19557
19280
|
}
|
|
19558
|
-
function
|
|
19281
|
+
function relativePath(absPath, projectRoot3, frameworkRoots) {
|
|
19559
19282
|
const norm = path6.resolve(absPath);
|
|
19560
19283
|
for (const fw of frameworkRoots) {
|
|
19561
19284
|
const parent = path6.dirname(fw);
|
|
@@ -20020,7 +19743,7 @@ function buildEntriesForFile(absPath, source, fwRoots, projectRoot3, version, ou
|
|
|
20020
19743
|
} catch {
|
|
20021
19744
|
return;
|
|
20022
19745
|
}
|
|
20023
|
-
const rel =
|
|
19746
|
+
const rel = relativePath(absPath, projectRoot3, fwRoots);
|
|
20024
19747
|
for (const cls of parsed.classes) {
|
|
20025
19748
|
if (!cls.exported && source === "framework") {
|
|
20026
19749
|
continue;
|
|
@@ -20656,8 +20379,8 @@ ${end}
|
|
|
20656
20379
|
|
|
20657
20380
|
// ../core/src/devAdmin.ts
|
|
20658
20381
|
import { cpus as osCpus } from "node:os";
|
|
20659
|
-
import { readFileSync as
|
|
20660
|
-
import { join as join21, dirname as dirname10, resolve as resolve11, relative as
|
|
20382
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, existsSync as existsSync19, readdirSync as readdirSync12, mkdirSync as mkdirSync13, copyFileSync, statSync as statSync14 } from "node:fs";
|
|
20383
|
+
import { join as join21, dirname as dirname10, resolve as resolve11, relative as relative7 } from "node:path";
|
|
20661
20384
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
20662
20385
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
20663
20386
|
function escapeHtml(value) {
|
|
@@ -20776,7 +20499,7 @@ function readQueueDir(dir, topic, status2) {
|
|
|
20776
20499
|
for (const filename of readdirSync12(dir).sort()) {
|
|
20777
20500
|
if (!filename.endsWith(".queue-data")) continue;
|
|
20778
20501
|
try {
|
|
20779
|
-
jobs.push(mapQueueJob(JSON.parse(
|
|
20502
|
+
jobs.push(mapQueueJob(JSON.parse(readFileSync18(join21(dir, filename), "utf-8")), topic, status2));
|
|
20780
20503
|
} catch {
|
|
20781
20504
|
}
|
|
20782
20505
|
}
|
|
@@ -20895,7 +20618,7 @@ function resolveDevEnvVar(key) {
|
|
|
20895
20618
|
if (live !== void 0 && live !== "") return live;
|
|
20896
20619
|
const envPath = join21(process.cwd(), ".env");
|
|
20897
20620
|
if (!existsSync19(envPath)) return "";
|
|
20898
|
-
for (const line of
|
|
20621
|
+
for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
|
|
20899
20622
|
const t = line.trim();
|
|
20900
20623
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
20901
20624
|
const eq = t.indexOf("=");
|
|
@@ -20905,7 +20628,7 @@ function resolveDevEnvVar(key) {
|
|
|
20905
20628
|
}
|
|
20906
20629
|
function upsertDevEnvVar(key, value) {
|
|
20907
20630
|
const envPath = join21(process.cwd(), ".env");
|
|
20908
|
-
const lines = existsSync19(envPath) ?
|
|
20631
|
+
const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
20909
20632
|
let found = false;
|
|
20910
20633
|
const out = [];
|
|
20911
20634
|
for (const line of lines) {
|
|
@@ -20938,7 +20661,7 @@ function parseEnvFile() {
|
|
|
20938
20661
|
const envPath = join21(process.cwd(), ".env");
|
|
20939
20662
|
const result = {};
|
|
20940
20663
|
if (!existsSync19(envPath)) return result;
|
|
20941
|
-
const lines =
|
|
20664
|
+
const lines = readFileSync18(envPath, "utf-8").split("\n");
|
|
20942
20665
|
for (const line of lines) {
|
|
20943
20666
|
const trimmed = line.trim();
|
|
20944
20667
|
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -20978,7 +20701,7 @@ function handleGalleryDeploy(router) {
|
|
|
20978
20701
|
const copied = [];
|
|
20979
20702
|
const allFiles = walkDirRecursive(gallerySrc);
|
|
20980
20703
|
for (const srcFile of allFiles) {
|
|
20981
|
-
const rel =
|
|
20704
|
+
const rel = relative7(gallerySrc, srcFile);
|
|
20982
20705
|
const dest = join21(projectSrc, rel);
|
|
20983
20706
|
mkdirSync13(dirname10(dest), { recursive: true });
|
|
20984
20707
|
copyFileSync(srcFile, dest);
|
|
@@ -21640,9 +21363,6 @@ var init_devAdmin = __esm({
|
|
|
21640
21363
|
{ method: "GET", pattern: "/__dev/api/gallery", handler: handleGalleryList },
|
|
21641
21364
|
{ method: "POST", pattern: "/__dev/api/gallery/deploy", handler: handleGalleryDeploy(router) },
|
|
21642
21365
|
// Metrics
|
|
21643
|
-
{ method: "GET", pattern: "/__dev/api/metrics", handler: (_req, res) => {
|
|
21644
|
-
res.json(quickMetrics());
|
|
21645
|
-
} },
|
|
21646
21366
|
// No fallback (ADR-0002): a missing or stale CLI is a 503 naming the
|
|
21647
21367
|
// install command, never zeros that read as a healthy codebase.
|
|
21648
21368
|
{ method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req, res) => {
|
|
@@ -22451,7 +22171,7 @@ var init_devAdmin = __esm({
|
|
|
22451
22171
|
}
|
|
22452
22172
|
try {
|
|
22453
22173
|
const envPath = join21(process.cwd(), ".env");
|
|
22454
|
-
const lines = existsSync19(envPath) ?
|
|
22174
|
+
const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
22455
22175
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
22456
22176
|
const newLines = [];
|
|
22457
22177
|
for (const line of lines) {
|
|
@@ -22497,12 +22217,12 @@ var init_devAdmin = __esm({
|
|
|
22497
22217
|
const metaFile = join21(entryPath, "meta.json");
|
|
22498
22218
|
if (statSync14(entryPath).isDirectory() && existsSync19(metaFile)) {
|
|
22499
22219
|
try {
|
|
22500
|
-
const meta = JSON.parse(
|
|
22220
|
+
const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
|
|
22501
22221
|
meta.id = entry;
|
|
22502
22222
|
const srcDir = join21(entryPath, "src");
|
|
22503
22223
|
if (existsSync19(srcDir)) {
|
|
22504
22224
|
const allFiles = walkDirRecursive(srcDir);
|
|
22505
|
-
meta.files = allFiles.map((f) =>
|
|
22225
|
+
meta.files = allFiles.map((f) => relative7(srcDir, f));
|
|
22506
22226
|
}
|
|
22507
22227
|
const projectSrc = resolve11(process.cwd(), "src");
|
|
22508
22228
|
if (existsSync19(srcDir) && meta.files) {
|
|
@@ -22620,7 +22340,7 @@ var init_devAdmin = __esm({
|
|
|
22620
22340
|
for (const name of readdirSync12(target).sort()) {
|
|
22621
22341
|
if (devFilesHidden(name)) continue;
|
|
22622
22342
|
const full = join21(target, name);
|
|
22623
|
-
const entryRel =
|
|
22343
|
+
const entryRel = relative7(root, full).replace(/\\/g, "/");
|
|
22624
22344
|
if (isSecretPath(entryRel)) continue;
|
|
22625
22345
|
let isDir = false;
|
|
22626
22346
|
let size = null;
|
|
@@ -22665,7 +22385,7 @@ var init_devAdmin = __esm({
|
|
|
22665
22385
|
size
|
|
22666
22386
|
});
|
|
22667
22387
|
}
|
|
22668
|
-
res.json({ path:
|
|
22388
|
+
res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
|
|
22669
22389
|
};
|
|
22670
22390
|
DEV_ADMIN_LANG_MAP = {
|
|
22671
22391
|
".py": "python",
|
|
@@ -22717,8 +22437,8 @@ var init_devAdmin = __esm({
|
|
|
22717
22437
|
return;
|
|
22718
22438
|
}
|
|
22719
22439
|
try {
|
|
22720
|
-
const content =
|
|
22721
|
-
const path8 =
|
|
22440
|
+
const content = readFileSync18(target, "utf-8");
|
|
22441
|
+
const path8 = relative7(root, target);
|
|
22722
22442
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
22723
22443
|
} catch (e) {
|
|
22724
22444
|
res.json({ error: e.message }, 500);
|
|
@@ -22740,10 +22460,10 @@ var init_devAdmin = __esm({
|
|
|
22740
22460
|
writeFileSync12(target, content, "utf-8");
|
|
22741
22461
|
try {
|
|
22742
22462
|
const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
22743
|
-
Plan2.recordAction(existed ? "patched" : "created",
|
|
22463
|
+
Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
|
|
22744
22464
|
} catch {
|
|
22745
22465
|
}
|
|
22746
|
-
res.json({ ok: true, path:
|
|
22466
|
+
res.json({ ok: true, path: relative7(root, target), bytes: Buffer.byteLength(content, "utf-8") });
|
|
22747
22467
|
} catch (e) {
|
|
22748
22468
|
res.json({ error: e.message }, 500);
|
|
22749
22469
|
}
|
|
@@ -22763,7 +22483,7 @@ var init_devAdmin = __esm({
|
|
|
22763
22483
|
return;
|
|
22764
22484
|
}
|
|
22765
22485
|
try {
|
|
22766
|
-
const buf =
|
|
22486
|
+
const buf = readFileSync18(target);
|
|
22767
22487
|
const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
22768
22488
|
const mime = {
|
|
22769
22489
|
js: "application/javascript",
|
|
@@ -22805,7 +22525,7 @@ var init_devAdmin = __esm({
|
|
|
22805
22525
|
const { renameSync: renameSync3 } = await import("node:fs");
|
|
22806
22526
|
mkdirSync13(dirname10(dst), { recursive: true });
|
|
22807
22527
|
renameSync3(src, dst);
|
|
22808
|
-
res.json({ ok: true, from:
|
|
22528
|
+
res.json({ ok: true, from: relative7(root, src), to: relative7(root, dst) });
|
|
22809
22529
|
} catch (e) {
|
|
22810
22530
|
res.json({ error: e.message }, 500);
|
|
22811
22531
|
}
|
|
@@ -22826,7 +22546,7 @@ var init_devAdmin = __esm({
|
|
|
22826
22546
|
try {
|
|
22827
22547
|
const { rmSync } = await import("node:fs");
|
|
22828
22548
|
rmSync(target, { recursive: true, force: true });
|
|
22829
|
-
res.json({ ok: true, deleted:
|
|
22549
|
+
res.json({ ok: true, deleted: relative7(root, target) });
|
|
22830
22550
|
} catch (e) {
|
|
22831
22551
|
res.json({ error: e.message }, 500);
|
|
22832
22552
|
}
|
|
@@ -23160,7 +22880,7 @@ var init_devAdmin = __esm({
|
|
|
23160
22880
|
});
|
|
23161
22881
|
};
|
|
23162
22882
|
handleDevAdminJs = async (_req, res) => {
|
|
23163
|
-
const { readFileSync:
|
|
22883
|
+
const { readFileSync: readFileSync27, existsSync: existsSync27 } = await import("node:fs");
|
|
23164
22884
|
const { dirname: dirname15, join: join32, resolve: resolve20 } = await import("node:path");
|
|
23165
22885
|
const { fileURLToPath: fileURLToPath8 } = await import("node:url");
|
|
23166
22886
|
const dir = dirname15(fileURLToPath8(import.meta.url));
|
|
@@ -23176,7 +22896,7 @@ var init_devAdmin = __esm({
|
|
|
23176
22896
|
for (const jsPath of candidates) {
|
|
23177
22897
|
if (existsSync27(jsPath)) {
|
|
23178
22898
|
try {
|
|
23179
|
-
const content =
|
|
22899
|
+
const content = readFileSync27(jsPath, "utf-8");
|
|
23180
22900
|
res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
|
|
23181
22901
|
res.raw.end(content);
|
|
23182
22902
|
return;
|
|
@@ -23191,7 +22911,7 @@ var init_devAdmin = __esm({
|
|
|
23191
22911
|
});
|
|
23192
22912
|
|
|
23193
22913
|
// ../core/src/i18n.ts
|
|
23194
|
-
import { readFileSync as
|
|
22914
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync13, existsSync as existsSync20 } from "node:fs";
|
|
23195
22915
|
import { join as join22, resolve as resolve12 } from "node:path";
|
|
23196
22916
|
var I18n;
|
|
23197
22917
|
var init_i18n = __esm({
|
|
@@ -23288,7 +23008,7 @@ var init_i18n = __esm({
|
|
|
23288
23008
|
const filePath = join22(this._localeDir, `${locale}.json`);
|
|
23289
23009
|
if (existsSync20(filePath)) {
|
|
23290
23010
|
try {
|
|
23291
|
-
const raw =
|
|
23011
|
+
const raw = readFileSync19(filePath, "utf-8");
|
|
23292
23012
|
const data = JSON.parse(raw);
|
|
23293
23013
|
this._translations.set(locale, _I18n._flatten(data));
|
|
23294
23014
|
return;
|
|
@@ -23301,7 +23021,7 @@ var init_i18n = __esm({
|
|
|
23301
23021
|
const yamlPath = join22(this._localeDir, `${locale}${ext}`);
|
|
23302
23022
|
if (existsSync20(yamlPath)) {
|
|
23303
23023
|
try {
|
|
23304
|
-
const raw =
|
|
23024
|
+
const raw = readFileSync19(yamlPath, "utf-8");
|
|
23305
23025
|
const data = _I18n._parseSimpleYaml(raw);
|
|
23306
23026
|
this._translations.set(locale, _I18n._flatten(data));
|
|
23307
23027
|
return;
|
|
@@ -24164,8 +23884,8 @@ var init_docsAutoDiscovery = __esm({
|
|
|
24164
23884
|
// ../core/src/server.ts
|
|
24165
23885
|
import { createServer as createServer2 } from "node:http";
|
|
24166
23886
|
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
24167
|
-
import { resolve as resolve14, dirname as dirname11, join as join24, relative as
|
|
24168
|
-
import { existsSync as existsSync22, readdirSync as readdirSync14, readFileSync as
|
|
23887
|
+
import { resolve as resolve14, dirname as dirname11, join as join24, relative as relative8 } from "node:path";
|
|
23888
|
+
import { existsSync as existsSync22, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
|
|
24169
23889
|
import { isatty } from "node:tty";
|
|
24170
23890
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
24171
23891
|
import { execFileSync as execFileSync3, exec } from "node:child_process";
|
|
@@ -24389,7 +24109,7 @@ function getGalleryDeployedState() {
|
|
|
24389
24109
|
if (existsSync22(srcDir)) {
|
|
24390
24110
|
const files = walkGalleryFiles(srcDir);
|
|
24391
24111
|
const projectSrc = resolve14(process.cwd(), "src");
|
|
24392
|
-
state[entry] = files.every((f) => existsSync22(join24(projectSrc,
|
|
24112
|
+
state[entry] = files.every((f) => existsSync22(join24(projectSrc, relative8(srcDir, f))));
|
|
24393
24113
|
} else {
|
|
24394
24114
|
state[entry] = false;
|
|
24395
24115
|
}
|
|
@@ -24826,7 +24546,7 @@ function serveTemplateFallback(ctx) {
|
|
|
24826
24546
|
if ((ctx.req.method ?? "GET") !== "GET") return false;
|
|
24827
24547
|
const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
|
|
24828
24548
|
if (!tplFile) return false;
|
|
24829
|
-
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) :
|
|
24549
|
+
const html = ctx.frondEngine ? ctx.frondEngine.render(tplFile, {}) : readFileSync21(resolve14(ctx.templatesDir, tplFile), "utf-8");
|
|
24830
24550
|
ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
24831
24551
|
ctx.res.raw.end(html);
|
|
24832
24552
|
return true;
|
|
@@ -26121,7 +25841,7 @@ var init_mqttMessage = __esm({
|
|
|
26121
25841
|
import net2 from "node:net";
|
|
26122
25842
|
import tls from "node:tls";
|
|
26123
25843
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
26124
|
-
import { existsSync as existsSync24, readFileSync as
|
|
25844
|
+
import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
|
|
26125
25845
|
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;
|
|
26126
25846
|
var init_mqtt = __esm({
|
|
26127
25847
|
"../core/src/mqtt.ts"() {
|
|
@@ -26586,7 +26306,7 @@ var init_mqtt = __esm({
|
|
|
26586
26306
|
servername: this.host,
|
|
26587
26307
|
rejectUnauthorized: this.tlsVerify
|
|
26588
26308
|
};
|
|
26589
|
-
if (this.tlsVerify && this.caFile) opts.ca =
|
|
26309
|
+
if (this.tlsVerify && this.caFile) opts.ca = readFileSync22(this.caFile);
|
|
26590
26310
|
sock = tls.connect(opts, () => settle(() => resolve20(sock)));
|
|
26591
26311
|
} else {
|
|
26592
26312
|
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve20(sock)));
|
|
@@ -26807,7 +26527,7 @@ var init_mqtt = __esm({
|
|
|
26807
26527
|
|
|
26808
26528
|
// ../core/src/service.ts
|
|
26809
26529
|
import { readdirSync as readdirSync16, statSync as statSync16, watchFile, unwatchFile } from "node:fs";
|
|
26810
|
-
import { join as join26, extname as
|
|
26530
|
+
import { join as join26, extname as extname6 } from "node:path";
|
|
26811
26531
|
import { pathToFileURL } from "node:url";
|
|
26812
26532
|
function matchCronField(field, value) {
|
|
26813
26533
|
if (field === "*") return true;
|
|
@@ -26978,7 +26698,7 @@ var init_service = __esm({
|
|
|
26978
26698
|
return discovered;
|
|
26979
26699
|
}
|
|
26980
26700
|
for (const entry of entries) {
|
|
26981
|
-
const ext =
|
|
26701
|
+
const ext = extname6(entry);
|
|
26982
26702
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
26983
26703
|
const fullPath = join26(dir, entry);
|
|
26984
26704
|
const stat = statSync16(fullPath);
|
|
@@ -27094,7 +26814,7 @@ var init_service = __esm({
|
|
|
27094
26814
|
return;
|
|
27095
26815
|
}
|
|
27096
26816
|
for (const entry of entries) {
|
|
27097
|
-
const ext =
|
|
26817
|
+
const ext = extname6(entry);
|
|
27098
26818
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
27099
26819
|
const fullPath = join26(dir, entry);
|
|
27100
26820
|
if (watchedFiles.has(fullPath)) continue;
|
|
@@ -27797,7 +27517,7 @@ var init_api = __esm({
|
|
|
27797
27517
|
// ../core/src/messenger.ts
|
|
27798
27518
|
import net3 from "node:net";
|
|
27799
27519
|
import tls2 from "node:tls";
|
|
27800
|
-
import { readFileSync as
|
|
27520
|
+
import { readFileSync as readFileSync23 } from "node:fs";
|
|
27801
27521
|
import { basename as basename6 } from "node:path";
|
|
27802
27522
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
27803
27523
|
function tlsRejectUnauthorized() {
|
|
@@ -27894,7 +27614,7 @@ function buildMimeMessage(options) {
|
|
|
27894
27614
|
}
|
|
27895
27615
|
for (const filePath of options.attachments) {
|
|
27896
27616
|
const fileName = basename6(filePath);
|
|
27897
|
-
const fileData =
|
|
27617
|
+
const fileData = readFileSync23(filePath);
|
|
27898
27618
|
const base64Data = fileData.toString("base64");
|
|
27899
27619
|
lines.push("");
|
|
27900
27620
|
lines.push(`--${boundary}`);
|
|
@@ -29357,9 +29077,9 @@ var init_htmlElement = __esm({
|
|
|
29357
29077
|
});
|
|
29358
29078
|
|
|
29359
29079
|
// ../core/src/ai.ts
|
|
29360
|
-
import { existsSync as existsSync25, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15, readFileSync as
|
|
29080
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync16, writeFileSync as writeFileSync15, readFileSync as readFileSync24 } from "node:fs";
|
|
29361
29081
|
import { homedir } from "node:os";
|
|
29362
|
-
import { join as join27, resolve as resolve16, relative as
|
|
29082
|
+
import { join as join27, resolve as resolve16, relative as relative9, dirname as dirname12 } from "node:path";
|
|
29363
29083
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
29364
29084
|
import { execSync, execFileSync as execFileSync4 } from "node:child_process";
|
|
29365
29085
|
import { createInterface } from "node:readline";
|
|
@@ -29367,7 +29087,7 @@ function readVersion() {
|
|
|
29367
29087
|
try {
|
|
29368
29088
|
const thisDir = dirname12(fileURLToPath7(import.meta.url));
|
|
29369
29089
|
const rootPkg = resolve16(thisDir, "..", "..", "..", "package.json");
|
|
29370
|
-
const pkg = JSON.parse(
|
|
29090
|
+
const pkg = JSON.parse(readFileSync24(rootPkg, "utf-8"));
|
|
29371
29091
|
return pkg.version ?? "0.0.0";
|
|
29372
29092
|
} catch {
|
|
29373
29093
|
return "0.0.0";
|
|
@@ -29385,14 +29105,16 @@ function downloadSkillsSync(jobs) {
|
|
|
29385
29105
|
const jobs = JSON.parse(process.argv[1]);
|
|
29386
29106
|
const fs = require("node:fs");
|
|
29387
29107
|
const path = require("node:path");
|
|
29108
|
+
const transientStatuses = new Set([429, 500, 502, 503, 504]);
|
|
29388
29109
|
async function fetchOne(job) {
|
|
29389
29110
|
const resp = await fetch(job.url, { signal: AbortSignal.timeout(15000) });
|
|
29390
|
-
if (!resp.ok)
|
|
29111
|
+
if (!resp.ok) return { ok: false, retry: transientStatuses.has(resp.status) };
|
|
29391
29112
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
29392
29113
|
for (const dest of job.dests) {
|
|
29393
29114
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
29394
29115
|
fs.writeFileSync(dest, buf);
|
|
29395
29116
|
}
|
|
29117
|
+
return { ok: true, retry: false };
|
|
29396
29118
|
}
|
|
29397
29119
|
(async () => {
|
|
29398
29120
|
const ok = [];
|
|
@@ -29401,9 +29123,11 @@ function downloadSkillsSync(jobs) {
|
|
|
29401
29123
|
const failed = [];
|
|
29402
29124
|
await Promise.all(pending.map(async (job) => {
|
|
29403
29125
|
try {
|
|
29404
|
-
await fetchOne(job);
|
|
29405
|
-
ok.push(job.url);
|
|
29126
|
+
const result = await fetchOne(job);
|
|
29127
|
+
if (result.ok) ok.push(job.url);
|
|
29128
|
+
else if (result.retry) failed.push(job);
|
|
29406
29129
|
} catch {
|
|
29130
|
+
// DNS, TLS, timeout and connection failures are transient.
|
|
29407
29131
|
failed.push(job);
|
|
29408
29132
|
}
|
|
29409
29133
|
}));
|
|
@@ -29586,7 +29310,7 @@ function writeOrMerge(contextPath, contextFile, frameworkGuide) {
|
|
|
29586
29310
|
writeFileSync15(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
29587
29311
|
return "Installed";
|
|
29588
29312
|
}
|
|
29589
|
-
const existing =
|
|
29313
|
+
const existing = readFileSync24(contextPath, "utf-8");
|
|
29590
29314
|
if (hasMarkers(existing, start2, end)) {
|
|
29591
29315
|
writeFileSync15(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
|
|
29592
29316
|
return "Refreshed skill block in";
|
|
@@ -29610,7 +29334,7 @@ function installForTool(root, tool, context) {
|
|
|
29610
29334
|
const parentDir = dirname12(contextPath);
|
|
29611
29335
|
mkdirSync16(parentDir, { recursive: true });
|
|
29612
29336
|
const action = writeOrMerge(contextPath, tool.contextFile, context);
|
|
29613
|
-
const rel =
|
|
29337
|
+
const rel = relative9(root, contextPath);
|
|
29614
29338
|
created.push(rel);
|
|
29615
29339
|
console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
|
|
29616
29340
|
if (tool.name === "claude-code") {
|
|
@@ -29988,7 +29712,7 @@ function generateClaudeCodeContext() {
|
|
|
29988
29712
|
const repoRoot = resolve16(thisDir, "..", "..", "..");
|
|
29989
29713
|
const claudeMdPath = join27(repoRoot, "CLAUDE.md");
|
|
29990
29714
|
if (existsSync25(claudeMdPath)) {
|
|
29991
|
-
return
|
|
29715
|
+
return readFileSync24(claudeMdPath, "utf-8");
|
|
29992
29716
|
}
|
|
29993
29717
|
} catch {
|
|
29994
29718
|
}
|
|
@@ -30182,6 +29906,292 @@ export default class User {
|
|
|
30182
29906
|
}
|
|
30183
29907
|
});
|
|
30184
29908
|
|
|
29909
|
+
// ../core/src/aiClient.ts
|
|
29910
|
+
import http2 from "node:http";
|
|
29911
|
+
import https2 from "node:https";
|
|
29912
|
+
var AiError, AiConfigError, AiTimeoutError, AiParseError, AiHTTPError, Ai;
|
|
29913
|
+
var init_aiClient = __esm({
|
|
29914
|
+
"../core/src/aiClient.ts"() {
|
|
29915
|
+
"use strict";
|
|
29916
|
+
AiError = class extends Error {
|
|
29917
|
+
};
|
|
29918
|
+
AiConfigError = class extends AiError {
|
|
29919
|
+
};
|
|
29920
|
+
AiTimeoutError = class extends AiError {
|
|
29921
|
+
};
|
|
29922
|
+
AiParseError = class extends AiError {
|
|
29923
|
+
};
|
|
29924
|
+
AiHTTPError = class extends AiError {
|
|
29925
|
+
constructor(message, status2 = null) {
|
|
29926
|
+
super(message);
|
|
29927
|
+
this.status = status2;
|
|
29928
|
+
}
|
|
29929
|
+
};
|
|
29930
|
+
Ai = class {
|
|
29931
|
+
static chat(messages, options = {}) {
|
|
29932
|
+
this.validateMessages(messages);
|
|
29933
|
+
const config = this.config("chat", options);
|
|
29934
|
+
const body = this.chatBody(config, messages, options);
|
|
29935
|
+
const headers = this.headers(config);
|
|
29936
|
+
return options.stream ? this.streamRequest(config, headers, body) : this.chatResponse(config, headers, body);
|
|
29937
|
+
}
|
|
29938
|
+
static async complete(prompt, options = {}) {
|
|
29939
|
+
if (typeof prompt !== "string") throw new AiConfigError("AI prompt must be a string");
|
|
29940
|
+
return (await this.chat([{ role: "user", content: prompt }], { ...options, stream: false })).text;
|
|
29941
|
+
}
|
|
29942
|
+
static async embed(textOrTexts, options = {}) {
|
|
29943
|
+
const single = typeof textOrTexts === "string";
|
|
29944
|
+
if (!single && (!Array.isArray(textOrTexts) || textOrTexts.length === 0 || !textOrTexts.every((item) => typeof item === "string"))) {
|
|
29945
|
+
throw new AiConfigError("AI embedding input must be a string or a non-empty list of strings");
|
|
29946
|
+
}
|
|
29947
|
+
const config = this.config("embed", options);
|
|
29948
|
+
if (config.provider === "anthropic") throw new AiConfigError("Anthropic does not provide the embedding endpoint in this contract");
|
|
29949
|
+
const raw = await this.requestJson(config, this.headers(config), { model: config.model, input: textOrTexts });
|
|
29950
|
+
try {
|
|
29951
|
+
const data = raw.data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
29952
|
+
const vectors = data.map((item) => item.embedding);
|
|
29953
|
+
const expected = single ? 1 : textOrTexts.length;
|
|
29954
|
+
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();
|
|
29955
|
+
return single ? vectors[0] : vectors;
|
|
29956
|
+
} catch {
|
|
29957
|
+
throw new AiParseError("AI provider returned a malformed embedding response");
|
|
29958
|
+
}
|
|
29959
|
+
}
|
|
29960
|
+
static validateMessages(messages) {
|
|
29961
|
+
if (!Array.isArray(messages) || messages.length === 0 || !messages.every((message) => message && ["system", "user", "assistant"].includes(message.role) && typeof message.content === "string")) {
|
|
29962
|
+
throw new AiConfigError("AI messages must contain supported roles and string content");
|
|
29963
|
+
}
|
|
29964
|
+
}
|
|
29965
|
+
static number(name, fallback, minimum) {
|
|
29966
|
+
const value = process.env[name] === void 0 ? fallback : Number(process.env[name]);
|
|
29967
|
+
if (!Number.isFinite(value) || value < minimum) throw new AiConfigError(`${name} must be numeric and at least ${minimum}`);
|
|
29968
|
+
return value;
|
|
29969
|
+
}
|
|
29970
|
+
static config(capability, options) {
|
|
29971
|
+
const provider = (options.provider ?? process.env.TINA4_AI_PROVIDER ?? "local").trim().toLowerCase();
|
|
29972
|
+
if (provider !== "local" && provider !== "openai" && provider !== "anthropic") throw new AiConfigError("TINA4_AI_PROVIDER must be local, openai, or anthropic");
|
|
29973
|
+
const key = process.env.TINA4_AI_KEY || null;
|
|
29974
|
+
if ((provider === "openai" || provider === "anthropic") && !key) throw new AiConfigError(`TINA4_AI_KEY is required for the ${provider} provider`);
|
|
29975
|
+
const defaults = {
|
|
29976
|
+
local: ["http://localhost:11437", "llama3.2"],
|
|
29977
|
+
openai: ["https://api.openai.com/v1", "gpt-4o-mini"],
|
|
29978
|
+
anthropic: ["https://api.anthropic.com/v1", "claude-3-5-haiku-latest"]
|
|
29979
|
+
};
|
|
29980
|
+
const rawUrl = capability === "embed" && process.env.TINA4_EMBED_URL ? process.env.TINA4_EMBED_URL : process.env.TINA4_AI_URL ?? defaults[provider][0];
|
|
29981
|
+
const model = (options.model ?? process.env.TINA4_AI_MODEL ?? defaults[provider][1]).trim();
|
|
29982
|
+
if (!model) throw new AiConfigError("AI model must be a non-empty string");
|
|
29983
|
+
const totalTimeout = options.timeout === void 0 ? this.number("TINA4_AI_TIMEOUT", 60, 1e-3) : Number(options.timeout);
|
|
29984
|
+
if (!Number.isFinite(totalTimeout) || totalTimeout <= 0) throw new AiConfigError("AI timeout must be greater than zero");
|
|
29985
|
+
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)) };
|
|
29986
|
+
}
|
|
29987
|
+
static endpoint(value, capability, provider) {
|
|
29988
|
+
let url;
|
|
29989
|
+
try {
|
|
29990
|
+
url = new URL(value);
|
|
29991
|
+
} catch {
|
|
29992
|
+
throw new AiConfigError("AI URL must be an http or https URL");
|
|
29993
|
+
}
|
|
29994
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new AiConfigError("AI URL must be an http or https URL");
|
|
29995
|
+
const path8 = url.pathname.replace(/\/+$/, "");
|
|
29996
|
+
if (path8 === "" || path8 === "/v1" || path8 === "/api") {
|
|
29997
|
+
const suffix = provider === "anthropic" ? "/messages" : capability === "embed" ? "/embeddings" : "/chat/completions";
|
|
29998
|
+
url.pathname = (path8 || "/v1") + suffix;
|
|
29999
|
+
}
|
|
30000
|
+
return url.toString();
|
|
30001
|
+
}
|
|
30002
|
+
static headers(config) {
|
|
30003
|
+
const headers = { "content-type": "application/json", accept: "application/json" };
|
|
30004
|
+
if (config.provider === "openai") headers.authorization = `Bearer ${config.key}`;
|
|
30005
|
+
if (config.provider === "anthropic") {
|
|
30006
|
+
headers["x-api-key"] = config.key;
|
|
30007
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
30008
|
+
}
|
|
30009
|
+
return headers;
|
|
30010
|
+
}
|
|
30011
|
+
static chatBody(config, messages, options) {
|
|
30012
|
+
const body = { model: config.model, messages, stream: options.stream ?? false };
|
|
30013
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
30014
|
+
if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
|
|
30015
|
+
if (config.provider === "anthropic") {
|
|
30016
|
+
const system = messages.filter((message) => message.role === "system").map((message) => message.content);
|
|
30017
|
+
body.messages = messages.filter((message) => message.role !== "system");
|
|
30018
|
+
body.max_tokens = options.maxTokens ?? 1024;
|
|
30019
|
+
if (system.length) body.system = system.join("\n\n");
|
|
30020
|
+
}
|
|
30021
|
+
return body;
|
|
30022
|
+
}
|
|
30023
|
+
static open(config, deadline, headers, body) {
|
|
30024
|
+
const remainingMs = deadline - performance.now();
|
|
30025
|
+
if (remainingMs <= 0) return Promise.reject(new AiTimeoutError("AI total request timeout expired"));
|
|
30026
|
+
const url = new URL(config.url);
|
|
30027
|
+
const payload = JSON.stringify(body);
|
|
30028
|
+
const controller = new AbortController();
|
|
30029
|
+
const totalTimer = setTimeout(() => controller.abort(new AiTimeoutError("AI total request timeout expired")), remainingMs);
|
|
30030
|
+
return new Promise((resolve20, reject) => {
|
|
30031
|
+
const client = url.protocol === "https:" ? https2 : http2;
|
|
30032
|
+
const request = client.request(url, { method: "POST", headers: { ...headers, "content-length": Buffer.byteLength(payload) }, signal: controller.signal }, (response) => {
|
|
30033
|
+
clearTimeout(connectTimer);
|
|
30034
|
+
resolve20({ response, cleanup: () => {
|
|
30035
|
+
clearTimeout(totalTimer);
|
|
30036
|
+
clearTimeout(connectTimer);
|
|
30037
|
+
} });
|
|
30038
|
+
});
|
|
30039
|
+
const connectTimer = setTimeout(() => request.destroy(new AiTimeoutError("AI connection timeout expired")), Math.min(config.connectTimeout * 1e3, remainingMs));
|
|
30040
|
+
request.on("socket", (socket) => {
|
|
30041
|
+
if (!socket.connecting) clearTimeout(connectTimer);
|
|
30042
|
+
socket.once(url.protocol === "https:" ? "secureConnect" : "connect", () => clearTimeout(connectTimer));
|
|
30043
|
+
});
|
|
30044
|
+
request.once("error", (error) => {
|
|
30045
|
+
clearTimeout(totalTimer);
|
|
30046
|
+
clearTimeout(connectTimer);
|
|
30047
|
+
if (error instanceof AiError) reject(error);
|
|
30048
|
+
else if (controller.signal.aborted) reject(new AiTimeoutError("AI total request timeout expired"));
|
|
30049
|
+
else reject(new AiHTTPError(`AI transport failed (${error.name})`));
|
|
30050
|
+
});
|
|
30051
|
+
request.end(payload);
|
|
30052
|
+
});
|
|
30053
|
+
}
|
|
30054
|
+
static async readBody(response) {
|
|
30055
|
+
const chunks = [];
|
|
30056
|
+
for await (const chunk of response) chunks.push(Buffer.from(chunk));
|
|
30057
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
30058
|
+
}
|
|
30059
|
+
static retryDelay(headers, deadline) {
|
|
30060
|
+
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
30061
|
+
const requested = value !== void 0 && Number.isFinite(Number(value)) ? Math.max(0, Number(value) * 1e3) : 100;
|
|
30062
|
+
const delay = Math.min(requested, Math.max(0, deadline - performance.now()));
|
|
30063
|
+
return new Promise((resolve20) => setTimeout(resolve20, delay));
|
|
30064
|
+
}
|
|
30065
|
+
static async requestJson(config, headers, body) {
|
|
30066
|
+
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
30067
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
30068
|
+
let opened = null;
|
|
30069
|
+
try {
|
|
30070
|
+
opened = await this.open(config, deadline, headers, body);
|
|
30071
|
+
const status2 = opened.response.statusCode ?? 0;
|
|
30072
|
+
const responseHeaders = opened.response.headers;
|
|
30073
|
+
const raw = await this.readBody(opened.response);
|
|
30074
|
+
opened.cleanup();
|
|
30075
|
+
opened = null;
|
|
30076
|
+
if (status2 < 200 || status2 >= 300) {
|
|
30077
|
+
if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
|
|
30078
|
+
await this.retryDelay(responseHeaders, deadline);
|
|
30079
|
+
continue;
|
|
30080
|
+
}
|
|
30081
|
+
throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
|
|
30082
|
+
}
|
|
30083
|
+
let parsed;
|
|
30084
|
+
try {
|
|
30085
|
+
parsed = JSON.parse(raw);
|
|
30086
|
+
} catch {
|
|
30087
|
+
throw new AiParseError("AI provider returned malformed JSON");
|
|
30088
|
+
}
|
|
30089
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new AiParseError("AI provider returned a non-object JSON response");
|
|
30090
|
+
return parsed;
|
|
30091
|
+
} catch (error) {
|
|
30092
|
+
opened?.cleanup();
|
|
30093
|
+
if (error instanceof AiParseError || error instanceof AiHTTPError && error.status !== null) throw error;
|
|
30094
|
+
if (attempt >= config.maxRetries) throw error;
|
|
30095
|
+
}
|
|
30096
|
+
}
|
|
30097
|
+
throw new AiHTTPError("AI request failed");
|
|
30098
|
+
}
|
|
30099
|
+
static normalizeChat(provider, raw) {
|
|
30100
|
+
try {
|
|
30101
|
+
if (provider === "anthropic") {
|
|
30102
|
+
const content = raw.content;
|
|
30103
|
+
const parts = content.filter((item) => (item.type ?? "text") === "text").map((item) => item.text);
|
|
30104
|
+
if (!parts.length || !parts.every((part) => typeof part === "string")) throw new Error();
|
|
30105
|
+
const usage2 = raw.usage ?? {};
|
|
30106
|
+
const promptTokens = Number(usage2.input_tokens ?? 0);
|
|
30107
|
+
const completionTokens = Number(usage2.output_tokens ?? 0);
|
|
30108
|
+
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 };
|
|
30109
|
+
}
|
|
30110
|
+
const choice = raw.choices[0];
|
|
30111
|
+
const text = choice.message.content;
|
|
30112
|
+
if (typeof text !== "string") throw new Error();
|
|
30113
|
+
const usage = raw.usage ?? {};
|
|
30114
|
+
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 };
|
|
30115
|
+
} catch {
|
|
30116
|
+
throw new AiParseError("AI provider returned a malformed chat response");
|
|
30117
|
+
}
|
|
30118
|
+
}
|
|
30119
|
+
static async chatResponse(config, headers, body) {
|
|
30120
|
+
return this.normalizeChat(config.provider, await this.requestJson(config, headers, body));
|
|
30121
|
+
}
|
|
30122
|
+
static streamDelta(provider, data) {
|
|
30123
|
+
if (data === "[DONE]") return { completed: true };
|
|
30124
|
+
let event;
|
|
30125
|
+
try {
|
|
30126
|
+
event = JSON.parse(data);
|
|
30127
|
+
} catch {
|
|
30128
|
+
throw new AiParseError("AI provider returned malformed stream data");
|
|
30129
|
+
}
|
|
30130
|
+
const text = provider === "anthropic" ? event.type === "content_block_delta" ? event.delta?.text : void 0 : event.choices?.[0]?.delta?.content;
|
|
30131
|
+
if (text !== void 0 && text !== null && typeof text !== "string") throw new AiParseError("AI provider returned malformed stream data");
|
|
30132
|
+
return { completed: false, text };
|
|
30133
|
+
}
|
|
30134
|
+
static async *streamData(response) {
|
|
30135
|
+
let buffer = "";
|
|
30136
|
+
for await (const chunk of response) {
|
|
30137
|
+
buffer += Buffer.from(chunk).toString("utf8");
|
|
30138
|
+
let newline;
|
|
30139
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
30140
|
+
const line = buffer.slice(0, newline).trim();
|
|
30141
|
+
buffer = buffer.slice(newline + 1);
|
|
30142
|
+
if (line.startsWith("data:")) yield line.slice(5).trim();
|
|
30143
|
+
}
|
|
30144
|
+
}
|
|
30145
|
+
}
|
|
30146
|
+
static streamError(error) {
|
|
30147
|
+
if (error instanceof AiError) return error;
|
|
30148
|
+
if (error instanceof Error && error.name === "AbortError") return new AiTimeoutError("AI total request timeout expired");
|
|
30149
|
+
return new AiHTTPError(`AI transport failed (${error instanceof Error ? error.name : "Error"})`);
|
|
30150
|
+
}
|
|
30151
|
+
static async *streamRequest(config, headers, body) {
|
|
30152
|
+
const deadline = performance.now() + config.totalTimeout * 1e3;
|
|
30153
|
+
let yielded = false;
|
|
30154
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
30155
|
+
let opened = null;
|
|
30156
|
+
try {
|
|
30157
|
+
opened = await this.open(config, deadline, { ...headers, accept: "text/event-stream" }, body);
|
|
30158
|
+
const status2 = opened.response.statusCode ?? 0;
|
|
30159
|
+
if (status2 < 200 || status2 >= 300) {
|
|
30160
|
+
await this.readBody(opened.response);
|
|
30161
|
+
if ((status2 === 429 || status2 >= 500) && attempt < config.maxRetries) {
|
|
30162
|
+
await this.retryDelay(opened.response.headers, deadline);
|
|
30163
|
+
opened.cleanup();
|
|
30164
|
+
opened = null;
|
|
30165
|
+
continue;
|
|
30166
|
+
}
|
|
30167
|
+
throw new AiHTTPError(`AI provider returned HTTP ${status2}`, status2);
|
|
30168
|
+
}
|
|
30169
|
+
let completed = false;
|
|
30170
|
+
for await (const data of this.streamData(opened.response)) {
|
|
30171
|
+
const delta = this.streamDelta(config.provider, data);
|
|
30172
|
+
if (delta.completed) {
|
|
30173
|
+
completed = true;
|
|
30174
|
+
break;
|
|
30175
|
+
}
|
|
30176
|
+
if (delta.text === void 0) continue;
|
|
30177
|
+
yielded = true;
|
|
30178
|
+
yield delta.text;
|
|
30179
|
+
}
|
|
30180
|
+
opened.cleanup();
|
|
30181
|
+
opened = null;
|
|
30182
|
+
if (completed) return;
|
|
30183
|
+
throw new AiParseError("AI provider stream ended before [DONE]");
|
|
30184
|
+
} catch (error) {
|
|
30185
|
+
opened?.cleanup();
|
|
30186
|
+
const failure = this.streamError(error);
|
|
30187
|
+
if (failure instanceof AiParseError || failure instanceof AiHTTPError && failure.status !== null || yielded || attempt >= config.maxRetries) throw failure;
|
|
30188
|
+
}
|
|
30189
|
+
}
|
|
30190
|
+
}
|
|
30191
|
+
};
|
|
30192
|
+
}
|
|
30193
|
+
});
|
|
30194
|
+
|
|
30185
30195
|
// ../core/src/queueBackends/rabbitmqBackend.ts
|
|
30186
30196
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
30187
30197
|
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
@@ -31885,6 +31895,12 @@ __export(src_exports2, {
|
|
|
31885
31895
|
APPLICATION_JSON: () => APPLICATION_JSON,
|
|
31886
31896
|
APPLICATION_OCTET: () => APPLICATION_OCTET,
|
|
31887
31897
|
APPLICATION_XML: () => APPLICATION_XML,
|
|
31898
|
+
Ai: () => Ai,
|
|
31899
|
+
AiConfigError: () => AiConfigError,
|
|
31900
|
+
AiError: () => AiError,
|
|
31901
|
+
AiHTTPError: () => AiHTTPError,
|
|
31902
|
+
AiParseError: () => AiParseError,
|
|
31903
|
+
AiTimeoutError: () => AiTimeoutError,
|
|
31888
31904
|
Api: () => Api,
|
|
31889
31905
|
Auth: () => Auth,
|
|
31890
31906
|
CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
|
|
@@ -32212,6 +32228,7 @@ var init_src2 = __esm({
|
|
|
32212
32228
|
init_htmlElement();
|
|
32213
32229
|
init_errorOverlay();
|
|
32214
32230
|
init_ai();
|
|
32231
|
+
init_aiClient();
|
|
32215
32232
|
init_liteBackend();
|
|
32216
32233
|
init_rabbitmqBackend();
|
|
32217
32234
|
init_kafkaBackend();
|
|
@@ -37650,7 +37667,7 @@ var init_database = __esm({
|
|
|
37650
37667
|
|
|
37651
37668
|
// src/model.ts
|
|
37652
37669
|
import { readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
|
|
37653
|
-
import { join as join29, extname as
|
|
37670
|
+
import { join as join29, extname as extname7 } from "node:path";
|
|
37654
37671
|
async function discoverModels(modelsDir) {
|
|
37655
37672
|
const models = [];
|
|
37656
37673
|
let files;
|
|
@@ -37663,7 +37680,7 @@ async function discoverModels(modelsDir) {
|
|
|
37663
37680
|
const filePath = join29(modelsDir, file);
|
|
37664
37681
|
const stat = statSync17(filePath);
|
|
37665
37682
|
if (!stat.isFile()) continue;
|
|
37666
|
-
const ext =
|
|
37683
|
+
const ext = extname7(file);
|
|
37667
37684
|
if (ext !== ".ts" && ext !== ".js") continue;
|
|
37668
37685
|
try {
|
|
37669
37686
|
const moduleUrl = `file://${filePath}?t=${Date.now()}`;
|
|
@@ -37702,7 +37719,7 @@ var init_model = __esm({
|
|
|
37702
37719
|
});
|
|
37703
37720
|
|
|
37704
37721
|
// src/migration.ts
|
|
37705
|
-
import { existsSync as existsSync26, readdirSync as readdirSync18, readFileSync as
|
|
37722
|
+
import { existsSync as existsSync26, readdirSync as readdirSync18, readFileSync as readFileSync25, mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "node:fs";
|
|
37706
37723
|
import { join as join30, resolve as resolve18 } from "node:path";
|
|
37707
37724
|
function unwrapAdapter(db) {
|
|
37708
37725
|
let cur = db;
|
|
@@ -38030,7 +38047,7 @@ async function rollback(migrationsDir, delimiter2) {
|
|
|
38030
38047
|
`Cannot rollback ${migration.migration_name}: no .down.sql file found`
|
|
38031
38048
|
);
|
|
38032
38049
|
}
|
|
38033
|
-
const sqlContent =
|
|
38050
|
+
const sqlContent = readFileSync25(downPath, "utf-8").trim();
|
|
38034
38051
|
if (sqlContent) {
|
|
38035
38052
|
const statements = splitStatements(sqlContent, delim);
|
|
38036
38053
|
try {
|
|
@@ -38228,7 +38245,7 @@ async function migrate(adapter, options) {
|
|
|
38228
38245
|
result.skipped.push(file);
|
|
38229
38246
|
continue;
|
|
38230
38247
|
}
|
|
38231
|
-
const sqlContent =
|
|
38248
|
+
const sqlContent = readFileSync25(join30(dir, file), "utf-8").trim();
|
|
38232
38249
|
if (!sqlContent) {
|
|
38233
38250
|
result.skipped.push(file);
|
|
38234
38251
|
continue;
|
|
@@ -41919,7 +41936,7 @@ var init_attachment = __esm({
|
|
|
41919
41936
|
|
|
41920
41937
|
// src/realtime/storage.ts
|
|
41921
41938
|
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
41922
|
-
import { mkdirSync as mkdirSync20, readFileSync as
|
|
41939
|
+
import { mkdirSync as mkdirSync20, readFileSync as readFileSync26, writeFileSync as writeFileSync17, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
|
|
41923
41940
|
import { resolve as resolve19, sep as sep5 } from "node:path";
|
|
41924
41941
|
import { createRequire as createRequire8 } from "node:module";
|
|
41925
41942
|
function storageKey(filename = "") {
|
|
@@ -41972,7 +41989,7 @@ var init_storage = __esm({
|
|
|
41972
41989
|
}
|
|
41973
41990
|
get(key) {
|
|
41974
41991
|
try {
|
|
41975
|
-
return
|
|
41992
|
+
return readFileSync26(this.pathFor(key));
|
|
41976
41993
|
} catch {
|
|
41977
41994
|
return null;
|
|
41978
41995
|
}
|