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