tina4-nodejs 3.13.100 → 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.
@@ -19567,14 +19567,14 @@ async function discoverRoutes(routesDir) {
19567
19567
  const currentMtime = statSync7(filePath).mtimeMs;
19568
19568
  if (_seenFiles.has(filePath) && _seenMtimes.get(filePath) === currentMtime) continue;
19569
19569
  const method = name.toUpperCase();
19570
- const relativePath3 = relative(routesDir, filePath);
19571
- const pattern = filePathToPattern(relativePath3);
19570
+ const relativePath2 = relative(routesDir, filePath);
19571
+ const pattern = filePathToPattern(relativePath2);
19572
19572
  try {
19573
19573
  const moduleUrl = `file://${filePath}?t=${currentMtime}`;
19574
19574
  const mod = await import(moduleUrl);
19575
19575
  const handler = mod.default ?? mod.handler;
19576
19576
  if (typeof handler !== "function") {
19577
- console.warn(` Warning: ${relativePath3} does not export a handler function, skipping`);
19577
+ console.warn(` Warning: ${relativePath2} does not export a handler function, skipping`);
19578
19578
  continue;
19579
19579
  }
19580
19580
  const meta = mod.meta;
@@ -19586,7 +19586,7 @@ async function discoverRoutes(routesDir) {
19586
19586
  _seenMtimes.set(filePath, currentMtime);
19587
19587
  registeredFromThisScan++;
19588
19588
  } catch (err) {
19589
- console.error(` Error loading route ${relativePath3}:`, err);
19589
+ console.error(` Error loading route ${relativePath2}:`, err);
19590
19590
  recordBrokenImport(filePath, err);
19591
19591
  }
19592
19592
  }
@@ -19620,8 +19620,8 @@ function recordBrokenImport(filePath, error) {
19620
19620
  } catch {
19621
19621
  }
19622
19622
  }
19623
- function filePathToPattern(relativePath3) {
19624
- const parts = relativePath3.replace(/\\/g, "/").split("/").slice(0, -1);
19623
+ function filePathToPattern(relativePath2) {
19624
+ const parts = relativePath2.replace(/\\/g, "/").split("/").slice(0, -1);
19625
19625
  const urlParts = parts.map((part) => {
19626
19626
  if (part.startsWith("[...") && part.endsWith("]")) {
19627
19627
  const name = part.slice(4, -1);
@@ -22878,500 +22878,127 @@ import * as fs3 from "node:fs";
22878
22878
  import * as path2 from "node:path";
22879
22879
  import { spawnSync } from "node:child_process";
22880
22880
  import { fileURLToPath as fileURLToPath2 } from "node:url";
22881
- function walkFiles(dir, extensions, exclude = ["node_modules", ".git", "dist", "build"]) {
22882
- const results = [];
22883
- if (!fs3.existsSync(dir)) return results;
22884
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
22885
- for (const entry of entries) {
22886
- const fullPath = path2.join(dir, entry.name);
22887
- if (entry.isDirectory()) {
22888
- if (!exclude.includes(entry.name)) {
22889
- results.push(...walkFiles(fullPath, extensions, exclude));
22890
- }
22891
- } else if (entry.isFile()) {
22892
- const ext = path2.extname(entry.name);
22893
- if (extensions.includes(ext) && !entry.name.endsWith(".d.ts")) {
22894
- results.push(fullPath);
22895
- }
22896
- }
22897
- }
22898
- return results;
22899
- }
22900
- function readFileSafe(filePath) {
22901
- try {
22902
- return fs3.readFileSync(filePath, "utf-8");
22903
- } catch {
22904
- return null;
22905
- }
22906
- }
22907
- function relativePath(filePath, root = ".") {
22908
- return path2.relative(root, filePath);
22909
- }
22910
- function countLines(source) {
22911
- const lines = source.split("\n");
22912
- let loc = 0;
22913
- let blank = 0;
22914
- let comment = 0;
22915
- let inBlockComment = false;
22916
- for (const line of lines) {
22917
- const stripped = line.trim();
22918
- if (!stripped) {
22919
- blank++;
22920
- continue;
22921
- }
22922
- if (inBlockComment) {
22923
- comment++;
22924
- if (stripped.includes("*/")) {
22925
- inBlockComment = false;
22926
- }
22927
- continue;
22928
- }
22929
- if (stripped.startsWith("/*")) {
22930
- comment++;
22931
- if (!stripped.includes("*/") || stripped.endsWith("/*")) {
22932
- inBlockComment = true;
22933
- }
22934
- continue;
22935
- }
22936
- if (stripped.startsWith("//")) {
22937
- comment++;
22938
- continue;
22939
- }
22940
- loc++;
22941
- }
22942
- return { loc, blank, comment };
22943
- }
22944
- function stripLiterals(source) {
22945
- const out = [];
22946
- const n = source.length;
22947
- let i = 0;
22948
- let prevSignificant = "";
22949
- let prevWord = "";
22950
- const regexKeywords = /* @__PURE__ */ new Set([
22951
- "return",
22952
- "typeof",
22953
- "instanceof",
22954
- "in",
22955
- "of",
22956
- "new",
22957
- "delete",
22958
- "void",
22959
- "throw",
22960
- "case",
22961
- "do",
22962
- "else",
22963
- "yield",
22964
- "await"
22965
- ]);
22966
- function prevEndsExpression() {
22967
- if (prevSignificant === "") return false;
22968
- if (/[A-Za-z0-9_$]/.test(prevSignificant)) {
22969
- return !regexKeywords.has(prevWord);
22970
- }
22971
- if (prevSignificant === ")" || prevSignificant === "]") return true;
22972
- if (prevSignificant === ".") return true;
22973
- return false;
22974
- }
22975
- while (i < n) {
22976
- const ch = source[i];
22977
- const next = i + 1 < n ? source[i + 1] : "";
22978
- if (ch === "/" && next === "/") {
22979
- out.push("//");
22980
- i += 2;
22981
- while (i < n && source[i] !== "\n") {
22982
- out.push(" ");
22983
- i++;
22984
- }
22985
- continue;
22986
- }
22987
- if (ch === "/" && next === "*") {
22988
- out.push("/*");
22989
- i += 2;
22990
- while (i < n && !(source[i] === "*" && source[i + 1] === "/")) {
22991
- out.push(source[i] === "\n" ? "\n" : " ");
22992
- i++;
22993
- }
22994
- if (i < n) {
22995
- out.push("*/");
22996
- i += 2;
22997
- }
22998
- continue;
22999
- }
23000
- if (ch === '"' || ch === "'") {
23001
- const quote = ch;
23002
- out.push(quote);
23003
- i++;
23004
- while (i < n && source[i] !== quote) {
23005
- if (source[i] === "\\" && i + 1 < n) {
23006
- out.push(" ");
23007
- i += 2;
23008
- continue;
23009
- }
23010
- if (source[i] === "\n") {
23011
- out.push("\n");
23012
- i++;
23013
- break;
23014
- }
23015
- out.push(" ");
23016
- i++;
23017
- }
23018
- if (i < n && source[i] === quote) {
23019
- out.push(quote);
23020
- i++;
23021
- }
23022
- prevSignificant = quote;
23023
- prevWord = "";
23024
- continue;
23025
- }
23026
- if (ch === "`") {
23027
- out.push("`");
23028
- i++;
23029
- while (i < n && source[i] !== "`") {
23030
- if (source[i] === "\\" && i + 1 < n) {
23031
- out.push(source[i + 1] === "\n" ? " \n" : " ");
23032
- i += 2;
23033
- continue;
23034
- }
23035
- if (source[i] === "$" && source[i + 1] === "{") {
23036
- out.push("${");
23037
- i += 2;
23038
- let depth = 1;
23039
- const exprStart = i;
23040
- while (i < n && depth > 0) {
23041
- if (source[i] === "{") depth++;
23042
- else if (source[i] === "}") depth--;
23043
- if (depth === 0) break;
23044
- i++;
23045
- }
23046
- out.push(stripLiterals(source.slice(exprStart, i)));
23047
- if (i < n && source[i] === "}") {
23048
- out.push("}");
23049
- i++;
23050
- }
23051
- continue;
23052
- }
23053
- out.push(source[i] === "\n" ? "\n" : " ");
23054
- i++;
23055
- }
23056
- if (i < n && source[i] === "`") {
23057
- out.push("`");
23058
- i++;
23059
- }
23060
- prevSignificant = "`";
23061
- prevWord = "";
23062
- continue;
23063
- }
23064
- if (ch === "/" && !prevEndsExpression()) {
23065
- let j = i + 1;
23066
- let ok = false;
23067
- let inClass = false;
23068
- while (j < n) {
23069
- const c = source[j];
23070
- if (c === "\\") {
23071
- j += 2;
23072
- continue;
23073
- }
23074
- if (c === "\n") break;
23075
- if (c === "[") inClass = true;
23076
- else if (c === "]") inClass = false;
23077
- else if (c === "/" && !inClass) {
23078
- ok = true;
23079
- break;
23080
- }
23081
- j++;
23082
- }
23083
- if (ok) {
23084
- out.push("/");
23085
- for (let k = i + 1; k < j; k++) out.push(" ");
23086
- out.push("/");
23087
- i = j + 1;
23088
- while (i < n && /[a-z]/i.test(source[i])) {
23089
- out.push(source[i]);
23090
- i++;
23091
- }
23092
- prevSignificant = "/";
23093
- prevWord = "";
23094
- continue;
23095
- }
23096
- }
23097
- out.push(ch);
23098
- if (!/\s/.test(ch)) {
23099
- prevSignificant = ch;
23100
- if (/[A-Za-z0-9_$]/.test(ch)) {
23101
- prevWord = /[A-Za-z0-9_$]/.test(source[i - 1] ?? "") ? prevWord + ch : ch;
23102
- } else {
23103
- prevWord = "";
23104
- }
23105
- }
23106
- i++;
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;
23107
22887
  }
23108
- return out.join("");
23109
- }
23110
- function countClassesQuick(source) {
23111
- const matches = source.match(
23112
- /(?:^|\n)\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/g
23113
- );
23114
- return matches ? matches.length : 0;
23115
- }
23116
- function countFunctionsQuick(source) {
23117
- const clean = stripLiterals(source);
23118
- let count = 0;
23119
- const funcDecls = clean.match(
23120
- /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(/g
23121
- );
23122
- if (funcDecls) count += funcDecls.length;
23123
- const methods = clean.match(
23124
- /(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*\([^)]*\)\s*(?::\s*\S+)?\s*\{/g
23125
- );
23126
- if (methods) count += methods.length;
23127
- const arrows = clean.match(
23128
- /(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\(/g
23129
- );
23130
- if (arrows) count += arrows.length;
23131
- return count;
23132
- }
23133
- function resolveRoot(root = "src") {
23134
- const rootPath = path2.resolve(root);
23135
- if (fs3.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
23136
- _lastScanRoot = rootPath;
23137
- return root;
23138
- }
23139
- const fwDir = path2.resolve(path2.dirname(new URL(import.meta.url).pathname));
23140
- _lastScanRoot = fwDir;
23141
- return fwDir;
23142
- }
23143
- function quickMetrics(root = "src") {
23144
- root = resolveRoot(root);
23145
- const rootPath = path2.resolve(root);
23146
- if (!fs3.existsSync(rootPath)) {
23147
- return { error: `Directory not found: ${root}` };
23148
- }
23149
- const tsFiles = walkFiles(rootPath, [".ts", ".js"]);
23150
- const twigFiles = walkFiles(rootPath, [".twig", ".html"]);
23151
- const migrationsDir = path2.resolve("migrations");
23152
- const migrationFiles = [
23153
- ...walkFiles(migrationsDir, [".sql"]),
23154
- ...walkFiles(migrationsDir, [".ts"])
23155
- ];
23156
- const scssFiles = walkFiles(rootPath, [".scss", ".css"]);
23157
- let totalLoc = 0;
23158
- let totalBlank = 0;
23159
- let totalComment = 0;
23160
- let totalClasses = 0;
23161
- let totalFunctions = 0;
23162
- const fileDetails = [];
23163
- for (const f of tsFiles) {
23164
- const source = readFileSafe(f);
23165
- if (source === null) continue;
23166
- const counts = countLines(source);
23167
- const classes = countClassesQuick(source);
23168
- const functions = countFunctionsQuick(source);
23169
- totalLoc += counts.loc;
23170
- totalBlank += counts.blank;
23171
- totalComment += counts.comment;
23172
- totalClasses += classes;
23173
- totalFunctions += functions;
23174
- fileDetails.push({
23175
- path: relativePath(f, rootPath),
23176
- loc: counts.loc,
23177
- blank: counts.blank,
23178
- comment: counts.comment,
23179
- classes,
23180
- functions
23181
- });
23182
- }
23183
- fileDetails.sort((a, b) => b.loc - a.loc);
23184
- let routeCount = 0;
23185
- let ormCount = 0;
23186
- for (const f of tsFiles) {
23187
- const source = readFileSafe(f);
23188
- if (source === null) continue;
23189
- const routes = source.match(
23190
- /(?:router\s*\.\s*(?:get|post|put|delete|patch|any)\s*\(|@(?:get|post|put|delete|patch)\s*\()/g
23191
- );
23192
- if (routes) routeCount += routes.length;
23193
- const orms = source.match(
23194
- /class\s+\w+\s+extends\s+(?:ORM|Model)\b/g
23195
- );
23196
- if (orms) ormCount += orms.length;
23197
- }
23198
- const breakdown = {
23199
- typescript: tsFiles.filter((f) => f.endsWith(".ts")).length,
23200
- javascript: tsFiles.filter((f) => f.endsWith(".js")).length,
23201
- templates: twigFiles.length,
23202
- migrations: migrationFiles.length,
23203
- stylesheets: scssFiles.length
23204
- };
23205
- return {
23206
- file_count: tsFiles.length,
23207
- total_loc: totalLoc,
23208
- total_blank: totalBlank,
23209
- total_comment: totalComment,
23210
- lloc: totalLoc,
23211
- classes: totalClasses,
23212
- functions: totalFunctions,
23213
- route_count: routeCount,
23214
- orm_count: ormCount,
23215
- template_count: twigFiles.length,
23216
- migration_count: migrationFiles.length,
23217
- avg_file_size: tsFiles.length > 0 ? Math.round(totalLoc / tsFiles.length * 10) / 10 : 0,
23218
- largest_files: fileDetails.slice(0, 10),
23219
- breakdown
23220
- };
22888
+ return false;
23221
22889
  }
23222
- function resolveScanTarget(root = "src") {
23223
- const resolved = resolveRoot(root);
23224
- const frameworkDir = path2.dirname(fileURLToPath2(import.meta.url));
23225
- const real = path2.resolve(resolved);
23226
- const scanningFramework = real === frameworkDir || real.startsWith(frameworkDir);
23227
- 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];
23228
22895
  }
23229
22896
  function enginePath() {
23230
- const names = process.platform === "win32" ? ["tina4.exe", "tina4.cmd", "tina4"] : ["tina4"];
23231
- for (const dir of (process.env.PATH || "").split(path2.delimiter)) {
23232
- if (!dir) continue;
22897
+ const names = process.platform === "win32" ? ["tina4.exe", "tina4"] : ["tina4"];
22898
+ for (const directory of (process.env.PATH || "").split(path2.delimiter)) {
23233
22899
  for (const name of names) {
23234
- const candidate = path2.join(dir, name);
22900
+ const candidate = path2.join(directory, name);
23235
22901
  try {
23236
- if (!fs3.statSync(candidate).isFile()) continue;
23237
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;
23238
22909
  } catch {
23239
22910
  continue;
23240
22911
  }
23241
- try {
23242
- const fd = fs3.openSync(candidate, "r");
23243
- const buf = Buffer.alloc(2);
23244
- fs3.readSync(fd, buf, 0, 2, 0);
23245
- fs3.closeSync(fd);
23246
- if (buf.toString("latin1") === "#!") continue;
23247
- } catch {
23248
- }
23249
- return candidate;
23250
22912
  }
23251
22913
  }
23252
22914
  return null;
23253
22915
  }
23254
22916
  function runEngine(target) {
23255
22917
  const binary = enginePath();
23256
- if (binary === null) {
23257
- throw new MetricsEngineError(`tina4 not found on PATH - ${INSTALL_HINT}`);
23258
- }
23259
- 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"], {
23260
22920
  encoding: "utf8",
23261
- timeout: TIMEOUT_MS,
22921
+ timeout: 6e4,
23262
22922
  maxBuffer: 64 * 1024 * 1024
23263
22923
  });
23264
- if (proc.error) {
23265
- const err = proc.error;
23266
- if (err.code === "ETIMEDOUT") {
23267
- throw new MetricsEngineError(`tina4 metrics timed out after ${TIMEOUT_MS / 1e3}s on ${target}`);
23268
- }
23269
- throw new MetricsEngineError(`could not run ${binary}: ${err.message}`);
23270
- }
23271
- if (proc.status !== 0) {
23272
- const detail = (proc.stderr || proc.stdout || "").trim().split("\n")[0];
23273
- throw new MetricsEngineError(
23274
- `tina4 metrics failed on ${target}: ${detail || `exit code ${proc.status}`}`
23275
- );
22924
+ if (processResult.error) {
22925
+ throw new MetricsEngineError(`could not run ${binary}: ${processResult.error.message}`);
23276
22926
  }
23277
- if (!proc.stdout || !proc.stdout.trim()) {
23278
- 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}`);
23279
22930
  }
23280
- let payload;
23281
22931
  try {
23282
- payload = JSON.parse(proc.stdout);
23283
- } catch (e) {
23284
- throw new MetricsEngineError(`tina4 metrics returned unreadable JSON: ${e.message}`);
23285
- }
23286
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
23287
- 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}`);
23288
22939
  }
23289
- return payload;
23290
22940
  }
23291
- function requireKey(payload, key, isArray) {
23292
- const value = payload[key];
23293
- const ok = isArray ? Array.isArray(value) : value !== null && typeof value === "object" && !Array.isArray(value);
23294
- if (!ok) {
23295
- throw new MetricsEngineError(
23296
- `engine payload has no usable '${key}' - the installed tina4 CLI predates a field the dashboard renders. Update it: ${INSTALL_HINT}`
23297
- );
22941
+ function requireArray(payload, key) {
22942
+ if (!Array.isArray(payload[key])) {
22943
+ throw new MetricsEngineError(`engine payload has no usable '${key}' - ${INSTALL_HINT}`);
23298
22944
  }
23299
- return value;
22945
+ return payload[key];
23300
22946
  }
23301
22947
  function fullAnalysis(root = "src") {
23302
- const [resolved, scanMode] = resolveScanTarget(root);
22948
+ const [resolved, scanMode] = resolveTarget(root);
23303
22949
  const payload = runEngine(resolved);
23304
- const summary = requireKey(payload, "summary", false);
23305
- const fileMetrics = requireKey(payload, "file_metrics", true);
23306
- const functions = requireKey(payload, "most_complex_functions", true);
23307
- const missing = SUMMARY_KEYS.filter((k) => !(k in summary));
23308
- if (missing.length) {
23309
- throw new MetricsEngineError(
23310
- `engine summary is missing ${missing.join(", ")} - update the CLI: ${INSTALL_HINT}`
23311
- );
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(", ")}`);
23312
22963
  }
23313
- if (fileMetrics.length) {
23314
- const absent = FILE_KEYS.filter((k) => !(k in fileMetrics[0]));
23315
- if (absent.length) throw new MetricsEngineError(`engine file_metrics is missing ${absent.join(", ")}`);
23316
- }
23317
- if (functions.length) {
23318
- const absent = FUNCTION_KEYS.filter((k) => !(k in functions[0]));
23319
- if (absent.length) throw new MetricsEngineError(`engine function metrics are missing ${absent.join(", ")}`);
23320
- }
23321
- const result = {};
23322
- for (const key of SUMMARY_KEYS) result[key] = summary[key];
23323
- result.file_metrics = fileMetrics;
23324
- result.most_complex_functions = functions.slice(0, 15);
23325
- result.dependency_graph = payload.dependency_graph || {};
23326
- result.scan_mode = scanMode;
23327
- result.scan_root = path2.resolve(resolved);
23328
- result.engine = "tina4-cli";
23329
- return result;
23330
- }
23331
- function offenders(root = "src", top = 20) {
23332
- const [resolved, scanMode] = resolveScanTarget(root);
23333
- const payload = runEngine(resolved);
23334
- const found = requireKey(payload, "offenders", true);
23335
- const summary = { ...requireKey(payload, "summary", false) };
23336
- summary.scan_mode = scanMode;
23337
- summary.scan_root = path2.resolve(resolved);
23338
- summary.engine = "tina4-cli";
23339
- if (summary.total_offenders === void 0) summary.total_offenders = found.length;
23340
- 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
+ };
23341
22973
  }
23342
22974
  function fileDetail(filePath) {
23343
22975
  if (!filePath) throw new MetricsEngineError("fileDetail needs a path");
23344
22976
  let target = filePath;
23345
- if (!fs3.existsSync(target) && _lastScanRoot) {
23346
- const candidate = path2.join(_lastScanRoot, filePath);
23347
- if (fs3.existsSync(candidate)) target = candidate;
23348
- }
22977
+ if (!fs3.existsSync(target) && lastScanRoot) target = path2.join(lastScanRoot, filePath);
23349
22978
  if (!fs3.existsSync(target)) throw new MetricsEngineError(`no such file: ${filePath}`);
23350
22979
  if (fs3.statSync(target).isDirectory()) throw new MetricsEngineError(`not a file: ${filePath}`);
23351
22980
  const payload = runEngine(target);
23352
- const fileMetrics = requireKey(payload, "file_metrics", true);
23353
- if (!fileMetrics.length) {
23354
- throw new MetricsEngineError(`engine reported no metrics for ${filePath}`);
23355
- }
23356
- 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
+ };
23357
22989
  }
23358
- 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;
23359
22991
  var init_metrics = __esm({
23360
22992
  "../core/src/metrics.ts"() {
23361
22993
  "use strict";
23362
- _lastScanRoot = "";
22994
+ lastScanRoot = "";
23363
22995
  MetricsEngineError = class extends Error {
23364
22996
  constructor(message) {
23365
22997
  super(message);
23366
22998
  this.name = "MetricsEngineError";
23367
22999
  }
23368
23000
  };
23369
- TIMEOUT_MS = 6e4;
23370
- INSTALL_HINT = [
23371
- "the tina4 CLI provides the metrics engine (ADR-0002). Install it with",
23372
- " curl -fsSL https://tina4.com/install.sh | sh",
23373
- "or see https://tina4.com/cli"
23374
- ].join("\n");
23001
+ INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
23375
23002
  SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
23376
23003
  FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_tests"];
23377
23004
  FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
@@ -23379,7 +23006,7 @@ var init_metrics = __esm({
23379
23006
  });
23380
23007
 
23381
23008
  // ../core/src/feedback.ts
23382
- import { readFileSync as readFileSync13, existsSync as existsSync14 } from "node:fs";
23009
+ import { readFileSync as readFileSync12, existsSync as existsSync14 } from "node:fs";
23383
23010
  import { dirname as dirname8, join as join19, resolve as resolve10 } from "node:path";
23384
23011
  import { fileURLToPath as fileURLToPath3 } from "node:url";
23385
23012
  function feedbackEnabled() {
@@ -23520,7 +23147,7 @@ var init_feedback = __esm({
23520
23147
  handleFeedbackWidgetJs = (_req, res) => {
23521
23148
  let body;
23522
23149
  if (existsSync14(WIDGET_BUNDLE_PATH)) {
23523
- body = readFileSync13(WIDGET_BUNDLE_PATH);
23150
+ body = readFileSync12(WIDGET_BUNDLE_PATH);
23524
23151
  } else {
23525
23152
  body = "console.warn('tina4-feedback-widget bundle not built yet');";
23526
23153
  }
@@ -23535,7 +23162,7 @@ var init_feedback = __esm({
23535
23162
  });
23536
23163
 
23537
23164
  // ../core/src/version.ts
23538
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "node:fs";
23165
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
23539
23166
  import { dirname as dirname9, join as join20 } from "node:path";
23540
23167
  import { fileURLToPath as fileURLToPath4 } from "node:url";
23541
23168
  function resolveFrameworkVersion() {
@@ -23544,7 +23171,7 @@ function resolveFrameworkVersion() {
23544
23171
  const pkgPath = join20(dir, "package.json");
23545
23172
  if (existsSync15(pkgPath)) {
23546
23173
  try {
23547
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
23174
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
23548
23175
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
23549
23176
  } catch {
23550
23177
  }
@@ -26332,8 +25959,8 @@ __export(context_exports, {
26332
25959
  fts5Supported: () => fts5Supported
26333
25960
  });
26334
25961
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
26335
- import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync16, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
26336
- 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";
26337
25964
  function fts5Supported() {
26338
25965
  try {
26339
25966
  const conn = new DatabaseSync4(":memory:");
@@ -26468,7 +26095,7 @@ var init_context = __esm({
26468
26095
  }
26469
26096
  // ── indexing ───────────────────────────────────────────────
26470
26097
  static chunksFor(label, text) {
26471
- const ext = extname6(label).toLowerCase();
26098
+ const ext = extname5(label).toLowerCase();
26472
26099
  const special = SPECIAL_FILES.has(basename5(label).toLowerCase());
26473
26100
  if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
26474
26101
  return chunkCode(text, label);
@@ -26486,7 +26113,7 @@ var init_context = __esm({
26486
26113
  const stored = label != null ? String(label) : String(file);
26487
26114
  let text;
26488
26115
  try {
26489
- text = readFileSync16(file, "utf-8");
26116
+ text = readFileSync15(file, "utf-8");
26490
26117
  } catch {
26491
26118
  return 0;
26492
26119
  }
@@ -26507,7 +26134,7 @@ var init_context = __esm({
26507
26134
  static eligible(filename) {
26508
26135
  const fn = filename.toLowerCase();
26509
26136
  if (fn.endsWith(".min.js")) return false;
26510
- const ext = extname6(fn);
26137
+ const ext = extname5(fn);
26511
26138
  return CODE_EXTS.has(ext) || DOC_EXTS.has(ext) || CONFIG_EXTS.has(ext) || SPECIAL_FILES.has(fn);
26512
26139
  }
26513
26140
  /**
@@ -26536,7 +26163,7 @@ var init_context = __esm({
26536
26163
  for (const fn of files) {
26537
26164
  if (!_Context.eligible(fn)) continue;
26538
26165
  const full = join22(dir, fn);
26539
- const rel = relative4(rootAbs, full);
26166
+ const rel = relative3(rootAbs, full);
26540
26167
  total += this.indexPath(full, rel);
26541
26168
  }
26542
26169
  for (const d of subdirs) walk2(join22(dir, d));
@@ -26557,7 +26184,7 @@ var init_context = __esm({
26557
26184
  const raw = String(changedPath);
26558
26185
  const abs = isAbsolute6(raw) ? raw : join22(process.cwd(), raw);
26559
26186
  const resolved = realResolve(resolve12(abs));
26560
- const rel = relative4(this.root, resolved);
26187
+ const rel = relative3(this.root, resolved);
26561
26188
  if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) {
26562
26189
  return -1;
26563
26190
  }
@@ -28479,7 +28106,7 @@ var init_job = __esm({
28479
28106
  });
28480
28107
 
28481
28108
  // ../core/src/queueBackends/liteBackend.ts
28482
- 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";
28483
28110
  import { join as join23 } from "node:path";
28484
28111
  import { randomUUID as randomUUID6 } from "node:crypto";
28485
28112
  var LiteBackend;
@@ -28579,7 +28206,7 @@ var init_liteBackend = __esm({
28579
28206
  const filePath = join23(dir, filename);
28580
28207
  let job;
28581
28208
  try {
28582
- job = JSON.parse(readFileSync17(filePath, "utf-8"));
28209
+ job = JSON.parse(readFileSync16(filePath, "utf-8"));
28583
28210
  } catch {
28584
28211
  continue;
28585
28212
  }
@@ -28643,7 +28270,7 @@ var init_liteBackend = __esm({
28643
28270
  const filePath = join23(reservedDir, filename);
28644
28271
  let record;
28645
28272
  try {
28646
- record = JSON.parse(readFileSync17(filePath, "utf-8"));
28273
+ record = JSON.parse(readFileSync16(filePath, "utf-8"));
28647
28274
  } catch {
28648
28275
  continue;
28649
28276
  }
@@ -28756,7 +28383,7 @@ var init_liteBackend = __esm({
28756
28383
  let count = 0;
28757
28384
  for (const file of files) {
28758
28385
  try {
28759
- const job = JSON.parse(readFileSync17(join23(scanDir, file), "utf-8"));
28386
+ const job = JSON.parse(readFileSync16(join23(scanDir, file), "utf-8"));
28760
28387
  if (job.status === status2) count++;
28761
28388
  } catch {
28762
28389
  }
@@ -28813,7 +28440,7 @@ var init_liteBackend = __esm({
28813
28440
  const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data")).sort();
28814
28441
  for (const file of files) {
28815
28442
  try {
28816
- const job = JSON.parse(readFileSync17(join23(dir, file), "utf-8"));
28443
+ const job = JSON.parse(readFileSync16(join23(dir, file), "utf-8"));
28817
28444
  const attempts = job.attempts || 0;
28818
28445
  if (attempts > 0 && attempts < maxRetries) {
28819
28446
  results.push(job);
@@ -28839,7 +28466,7 @@ var init_liteBackend = __esm({
28839
28466
  const failedDir = join23(this.basePath, q, "failed");
28840
28467
  const filePath = join23(failedDir, `${jobId}.queue-data`);
28841
28468
  if (existsSync18(filePath)) {
28842
- const job = JSON.parse(readFileSync17(filePath, "utf-8"));
28469
+ const job = JSON.parse(readFileSync16(filePath, "utf-8"));
28843
28470
  job.status = "pending";
28844
28471
  job.attempts = (job.attempts || 0) + 1;
28845
28472
  job.error = void 0;
@@ -28863,7 +28490,7 @@ var init_liteBackend = __esm({
28863
28490
  const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
28864
28491
  for (const file of files) {
28865
28492
  try {
28866
- const job = JSON.parse(readFileSync17(join23(failedDir, file), "utf-8"));
28493
+ const job = JSON.parse(readFileSync16(join23(failedDir, file), "utf-8"));
28867
28494
  if ((job.attempts || 0) >= maxRetries) {
28868
28495
  job.status = "dead";
28869
28496
  results.push(job);
@@ -28897,7 +28524,7 @@ var init_liteBackend = __esm({
28897
28524
  const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
28898
28525
  for (const file of files) {
28899
28526
  try {
28900
- const job = JSON.parse(readFileSync17(join23(dir, file), "utf-8"));
28527
+ const job = JSON.parse(readFileSync16(join23(dir, file), "utf-8"));
28901
28528
  if (job.status === status2) {
28902
28529
  unlinkSync7(join23(dir, file));
28903
28530
  count++;
@@ -28924,7 +28551,7 @@ var init_liteBackend = __esm({
28924
28551
  for (const file of files) {
28925
28552
  try {
28926
28553
  const filePath = join23(failedDir, file);
28927
- const job = JSON.parse(readFileSync17(filePath, "utf-8"));
28554
+ const job = JSON.parse(readFileSync16(filePath, "utf-8"));
28928
28555
  if ((job.attempts || 0) >= maxRetries) {
28929
28556
  continue;
28930
28557
  }
@@ -28956,7 +28583,7 @@ var init_liteBackend = __esm({
28956
28583
  const filePath = join23(dir, file);
28957
28584
  let job;
28958
28585
  try {
28959
- job = JSON.parse(readFileSync17(filePath, "utf-8"));
28586
+ job = JSON.parse(readFileSync16(filePath, "utf-8"));
28960
28587
  } catch {
28961
28588
  continue;
28962
28589
  }
@@ -30419,7 +30046,7 @@ function detectVersion(projectRoot3) {
30419
30046
  }
30420
30047
  return "0.0.0";
30421
30048
  }
30422
- function relativePath2(absPath, projectRoot3, frameworkRoots) {
30049
+ function relativePath(absPath, projectRoot3, frameworkRoots) {
30423
30050
  const norm = path6.resolve(absPath);
30424
30051
  for (const fw of frameworkRoots) {
30425
30052
  const parent = path6.dirname(fw);
@@ -30884,7 +30511,7 @@ function buildEntriesForFile(absPath, source, fwRoots, projectRoot3, version, ou
30884
30511
  } catch {
30885
30512
  return;
30886
30513
  }
30887
- const rel = relativePath2(absPath, projectRoot3, fwRoots);
30514
+ const rel = relativePath(absPath, projectRoot3, fwRoots);
30888
30515
  for (const cls of parsed.classes) {
30889
30516
  if (!cls.exported && source === "framework") {
30890
30517
  continue;
@@ -31520,8 +31147,8 @@ ${end}
31520
31147
 
31521
31148
  // ../core/src/devAdmin.ts
31522
31149
  import { cpus as osCpus } from "node:os";
31523
- 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";
31524
- 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";
31525
31152
  import { fileURLToPath as fileURLToPath6 } from "node:url";
31526
31153
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
31527
31154
  function escapeHtml(value) {
@@ -31640,7 +31267,7 @@ function readQueueDir(dir, topic, status2) {
31640
31267
  for (const filename of readdirSync15(dir).sort()) {
31641
31268
  if (!filename.endsWith(".queue-data")) continue;
31642
31269
  try {
31643
- 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));
31644
31271
  } catch {
31645
31272
  }
31646
31273
  }
@@ -31759,7 +31386,7 @@ function resolveDevEnvVar(key) {
31759
31386
  if (live !== void 0 && live !== "") return live;
31760
31387
  const envPath = join27(process.cwd(), ".env");
31761
31388
  if (!existsSync22(envPath)) return "";
31762
- for (const line of readFileSync21(envPath, "utf-8").split("\n")) {
31389
+ for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
31763
31390
  const t = line.trim();
31764
31391
  if (!t || t.startsWith("#") || !t.includes("=")) continue;
31765
31392
  const eq = t.indexOf("=");
@@ -31769,7 +31396,7 @@ function resolveDevEnvVar(key) {
31769
31396
  }
31770
31397
  function upsertDevEnvVar(key, value) {
31771
31398
  const envPath = join27(process.cwd(), ".env");
31772
- const lines = existsSync22(envPath) ? readFileSync21(envPath, "utf-8").split("\n") : [];
31399
+ const lines = existsSync22(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
31773
31400
  let found = false;
31774
31401
  const out = [];
31775
31402
  for (const line of lines) {
@@ -31802,7 +31429,7 @@ function parseEnvFile() {
31802
31429
  const envPath = join27(process.cwd(), ".env");
31803
31430
  const result = {};
31804
31431
  if (!existsSync22(envPath)) return result;
31805
- const lines = readFileSync21(envPath, "utf-8").split("\n");
31432
+ const lines = readFileSync20(envPath, "utf-8").split("\n");
31806
31433
  for (const line of lines) {
31807
31434
  const trimmed = line.trim();
31808
31435
  if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
@@ -31842,7 +31469,7 @@ function handleGalleryDeploy(router) {
31842
31469
  const copied = [];
31843
31470
  const allFiles = walkDirRecursive(gallerySrc);
31844
31471
  for (const srcFile of allFiles) {
31845
- const rel = relative8(gallerySrc, srcFile);
31472
+ const rel = relative7(gallerySrc, srcFile);
31846
31473
  const dest = join27(projectSrc, rel);
31847
31474
  mkdirSync18(dirname13(dest), { recursive: true });
31848
31475
  copyFileSync2(srcFile, dest);
@@ -32504,9 +32131,6 @@ var init_devAdmin = __esm({
32504
32131
  { method: "GET", pattern: "/__dev/api/gallery", handler: handleGalleryList },
32505
32132
  { method: "POST", pattern: "/__dev/api/gallery/deploy", handler: handleGalleryDeploy(router) },
32506
32133
  // Metrics
32507
- { method: "GET", pattern: "/__dev/api/metrics", handler: (_req, res) => {
32508
- res.json(quickMetrics());
32509
- } },
32510
32134
  // No fallback (ADR-0002): a missing or stale CLI is a 503 naming the
32511
32135
  // install command, never zeros that read as a healthy codebase.
32512
32136
  { method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req, res) => {
@@ -33315,7 +32939,7 @@ var init_devAdmin = __esm({
33315
32939
  }
33316
32940
  try {
33317
32941
  const envPath = join27(process.cwd(), ".env");
33318
- const lines = existsSync22(envPath) ? readFileSync21(envPath, "utf-8").split("\n") : [];
32942
+ const lines = existsSync22(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
33319
32943
  const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
33320
32944
  const newLines = [];
33321
32945
  for (const line of lines) {
@@ -33361,12 +32985,12 @@ var init_devAdmin = __esm({
33361
32985
  const metaFile = join27(entryPath, "meta.json");
33362
32986
  if (statSync16(entryPath).isDirectory() && existsSync22(metaFile)) {
33363
32987
  try {
33364
- const meta = JSON.parse(readFileSync21(metaFile, "utf-8"));
32988
+ const meta = JSON.parse(readFileSync20(metaFile, "utf-8"));
33365
32989
  meta.id = entry;
33366
32990
  const srcDir = join27(entryPath, "src");
33367
32991
  if (existsSync22(srcDir)) {
33368
32992
  const allFiles = walkDirRecursive(srcDir);
33369
- meta.files = allFiles.map((f) => relative8(srcDir, f));
32993
+ meta.files = allFiles.map((f) => relative7(srcDir, f));
33370
32994
  }
33371
32995
  const projectSrc = resolve16(process.cwd(), "src");
33372
32996
  if (existsSync22(srcDir) && meta.files) {
@@ -33484,7 +33108,7 @@ var init_devAdmin = __esm({
33484
33108
  for (const name of readdirSync15(target).sort()) {
33485
33109
  if (devFilesHidden(name)) continue;
33486
33110
  const full = join27(target, name);
33487
- const entryRel = relative8(root, full).replace(/\\/g, "/");
33111
+ const entryRel = relative7(root, full).replace(/\\/g, "/");
33488
33112
  if (isSecretPath(entryRel)) continue;
33489
33113
  let isDir = false;
33490
33114
  let size = null;
@@ -33529,7 +33153,7 @@ var init_devAdmin = __esm({
33529
33153
  size
33530
33154
  });
33531
33155
  }
33532
- res.json({ path: relative8(root, target).replace(/\\/g, "/") || ".", branch, entries });
33156
+ res.json({ path: relative7(root, target).replace(/\\/g, "/") || ".", branch, entries });
33533
33157
  };
33534
33158
  DEV_ADMIN_LANG_MAP = {
33535
33159
  ".py": "python",
@@ -33581,8 +33205,8 @@ var init_devAdmin = __esm({
33581
33205
  return;
33582
33206
  }
33583
33207
  try {
33584
- const content = readFileSync21(target, "utf-8");
33585
- const path8 = relative8(root, target);
33208
+ const content = readFileSync20(target, "utf-8");
33209
+ const path8 = relative7(root, target);
33586
33210
  res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
33587
33211
  } catch (e) {
33588
33212
  res.json({ error: e.message }, 500);
@@ -33604,10 +33228,10 @@ var init_devAdmin = __esm({
33604
33228
  writeFileSync15(target, content, "utf-8");
33605
33229
  try {
33606
33230
  const { Plan: Plan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
33607
- Plan2.recordAction(existed ? "patched" : "created", relative8(root, target));
33231
+ Plan2.recordAction(existed ? "patched" : "created", relative7(root, target));
33608
33232
  } catch {
33609
33233
  }
33610
- 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") });
33611
33235
  } catch (e) {
33612
33236
  res.json({ error: e.message }, 500);
33613
33237
  }
@@ -33627,7 +33251,7 @@ var init_devAdmin = __esm({
33627
33251
  return;
33628
33252
  }
33629
33253
  try {
33630
- const buf = readFileSync21(target);
33254
+ const buf = readFileSync20(target);
33631
33255
  const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
33632
33256
  const mime = {
33633
33257
  js: "application/javascript",
@@ -33669,7 +33293,7 @@ var init_devAdmin = __esm({
33669
33293
  const { renameSync: renameSync3 } = await import("node:fs");
33670
33294
  mkdirSync18(dirname13(dst), { recursive: true });
33671
33295
  renameSync3(src, dst);
33672
- 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) });
33673
33297
  } catch (e) {
33674
33298
  res.json({ error: e.message }, 500);
33675
33299
  }
@@ -33690,7 +33314,7 @@ var init_devAdmin = __esm({
33690
33314
  try {
33691
33315
  const { rmSync } = await import("node:fs");
33692
33316
  rmSync(target, { recursive: true, force: true });
33693
- res.json({ ok: true, deleted: relative8(root, target) });
33317
+ res.json({ ok: true, deleted: relative7(root, target) });
33694
33318
  } catch (e) {
33695
33319
  res.json({ error: e.message }, 500);
33696
33320
  }
@@ -34024,7 +33648,7 @@ var init_devAdmin = __esm({
34024
33648
  });
34025
33649
  };
34026
33650
  handleDevAdminJs = async (_req, res) => {
34027
- const { readFileSync: readFileSync30, existsSync: existsSync37 } = await import("node:fs");
33651
+ const { readFileSync: readFileSync29, existsSync: existsSync37 } = await import("node:fs");
34028
33652
  const { dirname: dirname17, join: join40, resolve: resolve30 } = await import("node:path");
34029
33653
  const { fileURLToPath: fileURLToPath10 } = await import("node:url");
34030
33654
  const dir = dirname17(fileURLToPath10(import.meta.url));
@@ -34040,7 +33664,7 @@ var init_devAdmin = __esm({
34040
33664
  for (const jsPath of candidates) {
34041
33665
  if (existsSync37(jsPath)) {
34042
33666
  try {
34043
- const content = readFileSync30(jsPath, "utf-8");
33667
+ const content = readFileSync29(jsPath, "utf-8");
34044
33668
  res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
34045
33669
  res.raw.end(content);
34046
33670
  return;
@@ -34055,7 +33679,7 @@ var init_devAdmin = __esm({
34055
33679
  });
34056
33680
 
34057
33681
  // ../core/src/i18n.ts
34058
- 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";
34059
33683
  import { join as join28, resolve as resolve17 } from "node:path";
34060
33684
  var I18n;
34061
33685
  var init_i18n = __esm({
@@ -34152,7 +33776,7 @@ var init_i18n = __esm({
34152
33776
  const filePath = join28(this._localeDir, `${locale}.json`);
34153
33777
  if (existsSync23(filePath)) {
34154
33778
  try {
34155
- const raw = readFileSync22(filePath, "utf-8");
33779
+ const raw = readFileSync21(filePath, "utf-8");
34156
33780
  const data = JSON.parse(raw);
34157
33781
  this._translations.set(locale, _I18n._flatten(data));
34158
33782
  return;
@@ -34165,7 +33789,7 @@ var init_i18n = __esm({
34165
33789
  const yamlPath = join28(this._localeDir, `${locale}${ext}`);
34166
33790
  if (existsSync23(yamlPath)) {
34167
33791
  try {
34168
- const raw = readFileSync22(yamlPath, "utf-8");
33792
+ const raw = readFileSync21(yamlPath, "utf-8");
34169
33793
  const data = _I18n._parseSimpleYaml(raw);
34170
33794
  this._translations.set(locale, _I18n._flatten(data));
34171
33795
  return;
@@ -35028,8 +34652,8 @@ var init_docsAutoDiscovery = __esm({
35028
34652
  // ../core/src/server.ts
35029
34653
  import { createServer as createServer2 } from "node:http";
35030
34654
  import { randomBytes as randomBytes7 } from "node:crypto";
35031
- import { resolve as resolve19, dirname as dirname14, join as join30, relative as relative9 } from "node:path";
35032
- 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";
35033
34657
  import { isatty } from "node:tty";
35034
34658
  import { fileURLToPath as fileURLToPath7 } from "node:url";
35035
34659
  import { execFileSync as execFileSync3, exec } from "node:child_process";
@@ -35253,7 +34877,7 @@ function getGalleryDeployedState() {
35253
34877
  if (existsSync25(srcDir)) {
35254
34878
  const files = walkGalleryFiles(srcDir);
35255
34879
  const projectSrc = resolve19(process.cwd(), "src");
35256
- state[entry] = files.every((f) => existsSync25(join30(projectSrc, relative9(srcDir, f))));
34880
+ state[entry] = files.every((f) => existsSync25(join30(projectSrc, relative8(srcDir, f))));
35257
34881
  } else {
35258
34882
  state[entry] = false;
35259
34883
  }
@@ -35690,7 +35314,7 @@ function serveTemplateFallback(ctx) {
35690
35314
  if ((ctx.req.method ?? "GET") !== "GET") return false;
35691
35315
  const tplFile = resolveTemplate(ctx.pathname, ctx.templatesDir);
35692
35316
  if (!tplFile) return false;
35693
- 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");
35694
35318
  ctx.res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
35695
35319
  ctx.res.raw.end(html);
35696
35320
  return true;
@@ -36517,7 +36141,7 @@ var init_mqttMessage = __esm({
36517
36141
  import net2 from "node:net";
36518
36142
  import tls from "node:tls";
36519
36143
  import { randomBytes as randomBytes8 } from "node:crypto";
36520
- import { existsSync as existsSync26, readFileSync as readFileSync25 } from "node:fs";
36144
+ import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
36521
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;
36522
36146
  var init_mqtt = __esm({
36523
36147
  "../core/src/mqtt.ts"() {
@@ -36982,7 +36606,7 @@ var init_mqtt = __esm({
36982
36606
  servername: this.host,
36983
36607
  rejectUnauthorized: this.tlsVerify
36984
36608
  };
36985
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync25(this.caFile);
36609
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
36986
36610
  sock = tls.connect(opts, () => settle(() => resolve30(sock)));
36987
36611
  } else {
36988
36612
  sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve30(sock)));
@@ -37203,7 +36827,7 @@ var init_mqtt = __esm({
37203
36827
 
37204
36828
  // ../core/src/service.ts
37205
36829
  import { readdirSync as readdirSync18, statSync as statSync18, watchFile, unwatchFile } from "node:fs";
37206
- import { join as join31, extname as extname8 } from "node:path";
36830
+ import { join as join31, extname as extname7 } from "node:path";
37207
36831
  import { pathToFileURL } from "node:url";
37208
36832
  function matchCronField(field, value) {
37209
36833
  if (field === "*") return true;
@@ -37374,7 +36998,7 @@ var init_service = __esm({
37374
36998
  return discovered;
37375
36999
  }
37376
37000
  for (const entry of entries) {
37377
- const ext = extname8(entry);
37001
+ const ext = extname7(entry);
37378
37002
  if (ext !== ".ts" && ext !== ".js") continue;
37379
37003
  const fullPath = join31(dir, entry);
37380
37004
  const stat = statSync18(fullPath);
@@ -37490,7 +37114,7 @@ var init_service = __esm({
37490
37114
  return;
37491
37115
  }
37492
37116
  for (const entry of entries) {
37493
- const ext = extname8(entry);
37117
+ const ext = extname7(entry);
37494
37118
  if (ext !== ".ts" && ext !== ".js") continue;
37495
37119
  const fullPath = join31(dir, entry);
37496
37120
  if (watchedFiles.has(fullPath)) continue;
@@ -38193,7 +37817,7 @@ var init_api = __esm({
38193
37817
  // ../core/src/messenger.ts
38194
37818
  import net3 from "node:net";
38195
37819
  import tls2 from "node:tls";
38196
- import { readFileSync as readFileSync26 } from "node:fs";
37820
+ import { readFileSync as readFileSync25 } from "node:fs";
38197
37821
  import { basename as basename7 } from "node:path";
38198
37822
  import { randomUUID as randomUUID7 } from "node:crypto";
38199
37823
  function tlsRejectUnauthorized() {
@@ -38290,7 +37914,7 @@ function buildMimeMessage(options) {
38290
37914
  }
38291
37915
  for (const filePath of options.attachments) {
38292
37916
  const fileName = basename7(filePath);
38293
- const fileData = readFileSync26(filePath);
37917
+ const fileData = readFileSync25(filePath);
38294
37918
  const base64Data = fileData.toString("base64");
38295
37919
  lines.push("");
38296
37920
  lines.push(`--${boundary}`);
@@ -39771,9 +39395,9 @@ __export(ai_exports, {
39771
39395
  skillBlock: () => skillBlock,
39772
39396
  writeOrMerge: () => writeOrMerge
39773
39397
  });
39774
- 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";
39775
39399
  import { homedir } from "node:os";
39776
- 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";
39777
39401
  import { fileURLToPath as fileURLToPath8 } from "node:url";
39778
39402
  import { execSync as execSync2, execFileSync as execFileSync4 } from "node:child_process";
39779
39403
  import { createInterface } from "node:readline";
@@ -39781,7 +39405,7 @@ function readVersion() {
39781
39405
  try {
39782
39406
  const thisDir = dirname15(fileURLToPath8(import.meta.url));
39783
39407
  const rootPkg = resolve20(thisDir, "..", "..", "..", "package.json");
39784
- const pkg = JSON.parse(readFileSync27(rootPkg, "utf-8"));
39408
+ const pkg = JSON.parse(readFileSync26(rootPkg, "utf-8"));
39785
39409
  return pkg.version ?? "0.0.0";
39786
39410
  } catch {
39787
39411
  return "0.0.0";
@@ -40004,7 +39628,7 @@ function writeOrMerge(contextPath, contextFile, frameworkGuide) {
40004
39628
  writeFileSync18(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
40005
39629
  return "Installed";
40006
39630
  }
40007
- const existing = readFileSync27(contextPath, "utf-8");
39631
+ const existing = readFileSync26(contextPath, "utf-8");
40008
39632
  if (hasMarkers(existing, start2, end)) {
40009
39633
  writeFileSync18(contextPath, replaceMarkerBlock(existing, block, start2, end), "utf-8");
40010
39634
  return "Refreshed skill block in";
@@ -40028,7 +39652,7 @@ function installForTool(root, tool, context) {
40028
39652
  const parentDir = dirname15(contextPath);
40029
39653
  mkdirSync21(parentDir, { recursive: true });
40030
39654
  const action = writeOrMerge(contextPath, tool.contextFile, context);
40031
- const rel = relative10(root, contextPath);
39655
+ const rel = relative9(root, contextPath);
40032
39656
  created.push(rel);
40033
39657
  console.log(` ${GREEN2}\u2713${RESET2} ${action} ${rel}`);
40034
39658
  if (tool.name === "claude-code") {
@@ -40406,7 +40030,7 @@ function generateClaudeCodeContext() {
40406
40030
  const repoRoot = resolve20(thisDir, "..", "..", "..");
40407
40031
  const claudeMdPath = join32(repoRoot, "CLAUDE.md");
40408
40032
  if (existsSync27(claudeMdPath)) {
40409
- return readFileSync27(claudeMdPath, "utf-8");
40033
+ return readFileSync26(claudeMdPath, "utf-8");
40410
40034
  }
40411
40035
  } catch {
40412
40036
  }
@@ -40600,6 +40224,292 @@ export default class User {
40600
40224
  }
40601
40225
  });
40602
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
+
40603
40513
  // ../core/src/queueBackends/rabbitmqBackend.ts
40604
40514
  import { execFileSync as execFileSync5 } from "node:child_process";
40605
40515
  import { randomUUID as randomUUID8 } from "node:crypto";
@@ -42303,6 +42213,12 @@ __export(src_exports3, {
42303
42213
  APPLICATION_JSON: () => APPLICATION_JSON,
42304
42214
  APPLICATION_OCTET: () => APPLICATION_OCTET,
42305
42215
  APPLICATION_XML: () => APPLICATION_XML,
42216
+ Ai: () => Ai,
42217
+ AiConfigError: () => AiConfigError,
42218
+ AiError: () => AiError,
42219
+ AiHTTPError: () => AiHTTPError,
42220
+ AiParseError: () => AiParseError,
42221
+ AiTimeoutError: () => AiTimeoutError,
42306
42222
  Api: () => Api,
42307
42223
  Auth: () => Auth,
42308
42224
  CANONICAL_SESSION_BACKENDS: () => CANONICAL_SESSION_BACKENDS,
@@ -42630,6 +42546,7 @@ var init_src3 = __esm({
42630
42546
  init_htmlElement();
42631
42547
  init_errorOverlay();
42632
42548
  init_ai();
42549
+ init_aiClient();
42633
42550
  init_liteBackend();
42634
42551
  init_rabbitmqBackend();
42635
42552
  init_kafkaBackend();
@@ -43143,7 +43060,7 @@ async function listRoutes() {
43143
43060
  }
43144
43061
 
43145
43062
  // src/commands/test.ts
43146
- 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";
43147
43064
  import { resolve as resolve27, join as join34 } from "node:path";
43148
43065
  import { pathToFileURL as pathToFileURL2 } from "node:url";
43149
43066
  import { execSync as execSync3 } from "node:child_process";
@@ -43180,7 +43097,7 @@ async function runInlineTests(cwd) {
43180
43097
  for (const file of walkSource(srcDir)) {
43181
43098
  let text;
43182
43099
  try {
43183
- text = readFileSync28(file, "utf-8");
43100
+ text = readFileSync27(file, "utf-8");
43184
43101
  } catch {
43185
43102
  continue;
43186
43103
  }
@@ -43244,8 +43161,8 @@ async function runTests(testPath) {
43244
43161
  console.log(` Found ${testFiles.length} test file(s)
43245
43162
  `);
43246
43163
  for (const file of testFiles) {
43247
- const relative11 = file.replace(cwd + "/", "");
43248
- console.log(` Running: ${relative11}`);
43164
+ const relative10 = file.replace(cwd + "/", "");
43165
+ console.log(` Running: ${relative10}`);
43249
43166
  try {
43250
43167
  execSync3(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
43251
43168
  } catch {
@@ -44894,8 +44811,8 @@ async function runSeeds(seedPath) {
44894
44811
  `);
44895
44812
  let failed = false;
44896
44813
  for (const file of seedFiles) {
44897
- const relative11 = file.replace(cwd + "/", "");
44898
- console.log(` Seeding: ${relative11}`);
44814
+ const relative10 = file.replace(cwd + "/", "");
44815
+ console.log(` Seeding: ${relative10}`);
44899
44816
  try {
44900
44817
  execSync4(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
44901
44818
  } catch {
@@ -44909,121 +44826,14 @@ async function runSeeds(seedPath) {
44909
44826
  console.log("\n All seeds completed.");
44910
44827
  }
44911
44828
 
44912
- // src/commands/metrics.ts
44913
- init_metrics();
44914
- function parseFlags(args) {
44915
- const flags = { top: 20, json: false, path: "src", failOn: null };
44916
- for (let i = 0; i < args.length; i++) {
44917
- const a = args[i];
44918
- switch (a) {
44919
- case "--json":
44920
- flags.json = true;
44921
- break;
44922
- case "--top": {
44923
- const v = args[++i];
44924
- if (v === void 0 || !/^\d+$/.test(v)) {
44925
- return { error: `--top expects a number (got '${v ?? ""}')` };
44926
- }
44927
- flags.top = parseInt(v, 10);
44928
- break;
44929
- }
44930
- case "--path": {
44931
- const v = args[++i];
44932
- if (v === void 0) return { error: "--path expects a directory" };
44933
- flags.path = v;
44934
- break;
44935
- }
44936
- case "--fail-on": {
44937
- const v = args[++i];
44938
- if (v !== "warn" && v !== "error") {
44939
- return { error: `invalid --fail-on '${v ?? ""}' (use warn or error)` };
44940
- }
44941
- flags.failOn = v;
44942
- break;
44943
- }
44944
- default:
44945
- return { error: `unknown option '${a}'` };
44946
- }
44947
- }
44948
- return flags;
44949
- }
44950
- function runMetrics(args = []) {
44951
- const parsed = parseFlags(args);
44952
- if ("error" in parsed) {
44953
- console.log(` ${parsed.error}`);
44954
- return 2;
44955
- }
44956
- const { top, json, path: path8, failOn } = parsed;
44957
- let result;
44958
- try {
44959
- result = offenders(path8, Number.MAX_SAFE_INTEGER);
44960
- } catch (e) {
44961
- if (e instanceof MetricsEngineError) {
44962
- console.error(` metrics error: ${e.message}`);
44963
- return 2;
44964
- }
44965
- throw e;
44966
- }
44967
- const summary = result.summary;
44968
- const allOffenders = result.offenders;
44969
- const found = allOffenders.slice(0, top);
44970
- const severities = new Set(allOffenders.map((o) => o.severity));
44971
- let exitCode = 0;
44972
- if (failOn === "warn" && (severities.has("warn") || severities.has("error"))) {
44973
- exitCode = 1;
44974
- } else if (failOn === "error" && severities.has("error")) {
44975
- exitCode = 1;
44976
- }
44977
- if (json) {
44978
- console.log(JSON.stringify({ summary, offenders: found }, null, 2));
44979
- return exitCode;
44980
- }
44981
- const useColor = Boolean(process.stdout.isTTY);
44982
- const c = (text, code) => useColor ? `\x1B[${code}m${text}\x1B[0m` : text;
44983
- const sevColor = { error: "31", warn: "33", info: "2" };
44984
- console.log("");
44985
- console.log(` Tina4 Metrics \u2014 ${summary.scan_mode} scan (${summary.scan_root})`);
44986
- console.log(
44987
- ` files: ${summary.files_analyzed} functions: ${summary.total_functions} avg complexity: ${summary.avg_complexity} avg maintainability: ${summary.avg_maintainability}`
44988
- );
44989
- console.log(
44990
- ` offenders: ${summary.total_offenders} total` + (found.length ? ` (showing top ${found.length})` : "")
44991
- );
44992
- console.log("");
44993
- if (found.length === 0) {
44994
- console.log(" " + c("\u2713 no offenders \u2014 clean", "32"));
44995
- console.log("");
44996
- return exitCode;
44997
- }
44998
- const locs = found.map((o) => `${o.file}:${o.line}`);
44999
- const locW = Math.max("FILE:LINE".length, ...locs.map((s) => s.length));
45000
- const kindW = Math.max("KIND".length, ...found.map((o) => o.kind.length));
45001
- const pad = (s, w) => s.padEnd(w);
45002
- const header = ` ${pad("#", 3)} ${pad("SEVERITY", 8)} ${pad("KIND", kindW)} ${pad(
45003
- "FILE:LINE",
45004
- locW
45005
- )} DETAIL`;
45006
- console.log(c(header, "1"));
45007
- console.log(" " + "-".repeat(header.length - 2));
45008
- found.forEach((o, idx) => {
45009
- const i = idx + 1;
45010
- const sevCell = c(pad(o.severity, 8), sevColor[o.severity]);
45011
- console.log(
45012
- ` ${String(i).padStart(3)} ${sevCell} ${pad(o.kind, kindW)} ${pad(locs[idx], locW)} ${o.detail}`
45013
- );
45014
- });
45015
- console.log("");
45016
- return exitCode;
45017
- }
45018
-
45019
44829
  // src/commands/queue.ts
45020
44830
  init_dotenv();
45021
44831
  init_queue();
45022
44832
  import { readdirSync as readdirSync22, statSync as statSync20 } from "node:fs";
45023
- import { extname as extname9, join as join37 } from "node:path";
44833
+ import { extname as extname8, join as join37 } from "node:path";
45024
44834
  import { pathToFileURL as pathToFileURL3 } from "node:url";
45025
44835
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["once", "json"]);
45026
- function parseFlags2(args) {
44836
+ function parseFlags(args) {
45027
44837
  const flags = {};
45028
44838
  const positional = [];
45029
44839
  let i = 0;
@@ -45058,7 +44868,7 @@ async function resolveQueueHandler(servicesDir, topic) {
45058
44868
  }
45059
44869
  for (const entry of entries.sort()) {
45060
44870
  if (entry.startsWith("_")) continue;
45061
- const ext = extname9(entry);
44871
+ const ext = extname8(entry);
45062
44872
  if (ext !== ".ts" && ext !== ".js") continue;
45063
44873
  const fullPath = join37(servicesDir, entry);
45064
44874
  try {
@@ -45079,7 +44889,7 @@ function firstJob(yielded) {
45079
44889
  }
45080
44890
  async function queueWork(args) {
45081
44891
  loadEnv();
45082
- const { flags, positional } = parseFlags2(args);
44892
+ const { flags, positional } = parseFlags(args);
45083
44893
  const topic = positional[0] ?? "default";
45084
44894
  const once2 = Boolean(flags.once);
45085
44895
  let pollSeconds;
@@ -45131,7 +44941,7 @@ async function queueWork(args) {
45131
44941
  }
45132
44942
  async function queueStats(args) {
45133
44943
  loadEnv();
45134
- const { flags, positional } = parseFlags2(args);
44944
+ const { flags, positional } = parseFlags(args);
45135
44945
  const topic = positional[0] ?? "default";
45136
44946
  const queue = new Queue({ topic });
45137
44947
  const stats = {
@@ -45162,7 +44972,7 @@ async function queueStats(args) {
45162
44972
  }
45163
44973
  async function queueRetry(args) {
45164
44974
  loadEnv();
45165
- const { positional } = parseFlags2(args);
44975
+ const { positional } = parseFlags(args);
45166
44976
  const topic = positional[0] ?? "default";
45167
44977
  const queue = new Queue({ topic });
45168
44978
  const dead = queue.deadLetters(0);
@@ -45178,7 +44988,7 @@ async function queueRetry(args) {
45178
44988
  }
45179
44989
  async function queueClear(args) {
45180
44990
  loadEnv();
45181
- const { positional } = parseFlags2(args);
44991
+ const { positional } = parseFlags(args);
45182
44992
  const status2 = positional[0] ?? "completed";
45183
44993
  const topic = positional[1] ?? "default";
45184
44994
  const queue = new Queue({ topic });
@@ -45215,7 +45025,7 @@ async function queueCommand(args = []) {
45215
45025
  import { accessSync as accessSync2, constants as constants2, existsSync as existsSync35, statSync as statSync21 } from "node:fs";
45216
45026
  import { basename as basename8, delimiter as delimiter2, join as join38 } from "node:path";
45217
45027
  import { spawnSync as spawnSync3 } from "node:child_process";
45218
- function parseFlags3(args) {
45028
+ function parseFlags2(args) {
45219
45029
  const flags = {};
45220
45030
  let i = 0;
45221
45031
  while (i < args.length) {
@@ -45253,7 +45063,7 @@ function whichDocker() {
45253
45063
  return null;
45254
45064
  }
45255
45065
  function buildImage(args) {
45256
- const flags = parseFlags3(args);
45066
+ const flags = parseFlags2(args);
45257
45067
  let tag = typeof flags.tag === "string" ? flags.tag : "";
45258
45068
  if (!tag) {
45259
45069
  const dirName = basename8(process.cwd()).toLowerCase();
@@ -45288,7 +45098,7 @@ function buildImage(args) {
45288
45098
 
45289
45099
  // src/bin.ts
45290
45100
  import { spawnSync as spawnSync4 } from "node:child_process";
45291
- 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";
45292
45102
  import { delimiter as delimiter3, dirname as dirname16, join as join39 } from "node:path";
45293
45103
  import { fileURLToPath as fileURLToPath9, pathToFileURL as pathToFileURL4 } from "node:url";
45294
45104
  function readCliVersion() {
@@ -45297,7 +45107,7 @@ function readCliVersion() {
45297
45107
  const pkgPath = join39(dir, "package.json");
45298
45108
  if (existsSync36(pkgPath)) {
45299
45109
  try {
45300
- const pkg = JSON.parse(readFileSync29(pkgPath, "utf-8"));
45110
+ const pkg = JSON.parse(readFileSync28(pkgPath, "utf-8"));
45301
45111
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
45302
45112
  } catch {
45303
45113
  }
@@ -45533,13 +45343,6 @@ var COMMANDS = {
45533
45343
  usage: "[file]",
45534
45344
  summary: "Run database seed files from src/seeds/"
45535
45345
  },
45536
- metrics: {
45537
- handler: (a) => {
45538
- process.exit(runMetrics(a));
45539
- },
45540
- usage: "[--top N] [--json] [--fail-on warn|error] [--path DIR]",
45541
- summary: "Rank top code-quality offenders"
45542
- },
45543
45346
  console: {
45544
45347
  handler: async () => {
45545
45348
  await openConsole();