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