Implements Phases 1-8 of the TFTSR implementation plan. Rust backend (Tauri 2.x, src-tauri/): - Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama - PII detection engine: 11 regex patterns with overlap resolution - SQLCipher AES-256 encrypted database with 10 versioned migrations - 28 Tauri IPC commands for triage, analysis, document, and system ops - Ollama: hardware probe, model recommendations, pull/delete with events - RCA and blameless post-mortem Markdown document generators - PDF export via printpdf - Audit log: SHA-256 hash of every external data send - Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2) Frontend (React 18 + TypeScript + Vite, src/): - 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings - 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives - 3 Zustand stores: session, settings (persisted), history - Type-safe tauriCommands.ts matching Rust backend types exactly - 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs) DevOps: - .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push - .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload Verified: - cargo check: zero errors - tsc --noEmit: zero errors - vitest run: 13/13 unit tests passing Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
171 lines
5.1 KiB
JavaScript
171 lines
5.1 KiB
JavaScript
import { Console } from 'node:console';
|
|
import { relative } from 'node:path';
|
|
import { Writable } from 'node:stream';
|
|
import { getSafeTimers } from '@vitest/utils';
|
|
import c from 'tinyrainbow';
|
|
import { R as RealDate } from './date.W2xKR2qe.js';
|
|
import { g as getWorkerState } from './utils.C8RiOc4B.js';
|
|
|
|
const UNKNOWN_TEST_ID = "__vitest__unknown_test__";
|
|
function getTaskIdByStack(root) {
|
|
const stack = new Error("STACK_TRACE_ERROR").stack?.split("\n");
|
|
if (!stack) {
|
|
return UNKNOWN_TEST_ID;
|
|
}
|
|
const index = stack.findIndex((line2) => line2.includes("at Console.value"));
|
|
const line = index === -1 ? null : stack[index + 2];
|
|
if (!line) {
|
|
return UNKNOWN_TEST_ID;
|
|
}
|
|
const filepath = line.match(/at\s(.*)\s?/)?.[1];
|
|
if (filepath) {
|
|
return relative(root, filepath);
|
|
}
|
|
return UNKNOWN_TEST_ID;
|
|
}
|
|
function createCustomConsole(defaultState) {
|
|
const stdoutBuffer = /* @__PURE__ */ new Map();
|
|
const stderrBuffer = /* @__PURE__ */ new Map();
|
|
const timers = /* @__PURE__ */ new Map();
|
|
const { setTimeout, clearTimeout } = getSafeTimers();
|
|
const state = () => defaultState || getWorkerState();
|
|
function schedule(taskId) {
|
|
const timer = timers.get(taskId);
|
|
const { stdoutTime, stderrTime } = timer;
|
|
clearTimeout(timer.timer);
|
|
timer.timer = setTimeout(() => {
|
|
if (stderrTime < stdoutTime) {
|
|
sendStderr(taskId);
|
|
sendStdout(taskId);
|
|
} else {
|
|
sendStdout(taskId);
|
|
sendStderr(taskId);
|
|
}
|
|
});
|
|
}
|
|
function sendStdout(taskId) {
|
|
sendBuffer("stdout", taskId);
|
|
}
|
|
function sendStderr(taskId) {
|
|
sendBuffer("stderr", taskId);
|
|
}
|
|
function sendBuffer(type, taskId) {
|
|
const buffers = type === "stdout" ? stdoutBuffer : stderrBuffer;
|
|
const buffer = buffers.get(taskId);
|
|
if (!buffer) {
|
|
return;
|
|
}
|
|
if (state().config.printConsoleTrace) {
|
|
buffer.forEach(([buffer2, origin]) => {
|
|
sendLog(type, taskId, String(buffer2), buffer2.length, origin);
|
|
});
|
|
} else {
|
|
const content = buffer.map((i) => String(i[0])).join("");
|
|
sendLog(type, taskId, content, buffer.length);
|
|
}
|
|
const timer = timers.get(taskId);
|
|
buffers.delete(taskId);
|
|
if (type === "stderr") {
|
|
timer.stderrTime = 0;
|
|
} else {
|
|
timer.stdoutTime = 0;
|
|
}
|
|
}
|
|
function sendLog(type, taskId, content, size, origin) {
|
|
const timer = timers.get(taskId);
|
|
const time = type === "stderr" ? timer.stderrTime : timer.stdoutTime;
|
|
state().rpc.onUserConsoleLog({
|
|
type,
|
|
content: content || "<empty line>",
|
|
taskId,
|
|
time: time || RealDate.now(),
|
|
size,
|
|
origin
|
|
});
|
|
}
|
|
const stdout = new Writable({
|
|
write(data, encoding, callback) {
|
|
const s = state();
|
|
const id = s?.current?.id || s?.current?.suite?.id || s.current?.file.id || getTaskIdByStack(s.config.root);
|
|
let timer = timers.get(id);
|
|
if (timer) {
|
|
timer.stdoutTime = timer.stdoutTime || RealDate.now();
|
|
} else {
|
|
timer = {
|
|
stdoutTime: RealDate.now(),
|
|
stderrTime: RealDate.now(),
|
|
timer: 0
|
|
};
|
|
timers.set(id, timer);
|
|
}
|
|
let buffer = stdoutBuffer.get(id);
|
|
if (!buffer) {
|
|
buffer = [];
|
|
stdoutBuffer.set(id, buffer);
|
|
}
|
|
if (state().config.printConsoleTrace) {
|
|
const limit = Error.stackTraceLimit;
|
|
Error.stackTraceLimit = limit + 6;
|
|
const stack = new Error("STACK_TRACE").stack;
|
|
const trace = stack?.split("\n").slice(7).join("\n");
|
|
Error.stackTraceLimit = limit;
|
|
buffer.push([data, trace]);
|
|
} else {
|
|
buffer.push([data, void 0]);
|
|
}
|
|
schedule(id);
|
|
callback();
|
|
}
|
|
});
|
|
const stderr = new Writable({
|
|
write(data, encoding, callback) {
|
|
const s = state();
|
|
const id = s?.current?.id || s?.current?.suite?.id || s.current?.file.id || getTaskIdByStack(s.config.root);
|
|
let timer = timers.get(id);
|
|
if (timer) {
|
|
timer.stderrTime = timer.stderrTime || RealDate.now();
|
|
} else {
|
|
timer = {
|
|
stderrTime: RealDate.now(),
|
|
stdoutTime: RealDate.now(),
|
|
timer: 0
|
|
};
|
|
timers.set(id, timer);
|
|
}
|
|
let buffer = stderrBuffer.get(id);
|
|
if (!buffer) {
|
|
buffer = [];
|
|
stderrBuffer.set(id, buffer);
|
|
}
|
|
if (state().config.printConsoleTrace) {
|
|
const limit = Error.stackTraceLimit;
|
|
Error.stackTraceLimit = limit + 6;
|
|
const stack = new Error("STACK_TRACE").stack?.split("\n");
|
|
Error.stackTraceLimit = limit;
|
|
const isTrace = stack?.some(
|
|
(line) => line.includes("at Console.trace")
|
|
);
|
|
if (isTrace) {
|
|
buffer.push([data, void 0]);
|
|
} else {
|
|
const trace = stack?.slice(7).join("\n");
|
|
Error.stackTraceLimit = limit;
|
|
buffer.push([data, trace]);
|
|
}
|
|
} else {
|
|
buffer.push([data, void 0]);
|
|
}
|
|
schedule(id);
|
|
callback();
|
|
}
|
|
});
|
|
return new Console({
|
|
stdout,
|
|
stderr,
|
|
colorMode: c.isColorSupported,
|
|
groupIndentation: 2
|
|
});
|
|
}
|
|
|
|
export { UNKNOWN_TEST_ID, createCustomConsole };
|