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>
136 lines
3.8 KiB
JavaScript
136 lines
3.8 KiB
JavaScript
import {writeFileSync, appendFileSync} from 'node:fs';
|
|
import {shouldLogOutput, logLinesSync} from '../verbose/output.js';
|
|
import {runGeneratorsSync} from '../transform/generator.js';
|
|
import {splitLinesSync} from '../transform/split.js';
|
|
import {joinToString, joinToUint8Array, bufferToUint8Array} from '../utils/uint-array.js';
|
|
import {FILE_TYPES} from '../stdio/type.js';
|
|
import {truncateMaxBufferSync} from './max-buffer.js';
|
|
|
|
// Apply `stdout`/`stderr` options, after spawning, in sync mode
|
|
export const transformOutputSync = ({fileDescriptors, syncResult: {output}, options, isMaxBuffer, verboseInfo}) => {
|
|
if (output === null) {
|
|
return {output: Array.from({length: 3})};
|
|
}
|
|
|
|
const state = {};
|
|
const outputFiles = new Set([]);
|
|
const transformedOutput = output.map((result, fdNumber) =>
|
|
transformOutputResultSync({
|
|
result,
|
|
fileDescriptors,
|
|
fdNumber,
|
|
state,
|
|
outputFiles,
|
|
isMaxBuffer,
|
|
verboseInfo,
|
|
}, options));
|
|
return {output: transformedOutput, ...state};
|
|
};
|
|
|
|
const transformOutputResultSync = (
|
|
{result, fileDescriptors, fdNumber, state, outputFiles, isMaxBuffer, verboseInfo},
|
|
{buffer, encoding, lines, stripFinalNewline, maxBuffer},
|
|
) => {
|
|
if (result === null) {
|
|
return;
|
|
}
|
|
|
|
const truncatedResult = truncateMaxBufferSync(result, isMaxBuffer, maxBuffer);
|
|
const uint8ArrayResult = bufferToUint8Array(truncatedResult);
|
|
const {stdioItems, objectMode} = fileDescriptors[fdNumber];
|
|
const chunks = runOutputGeneratorsSync([uint8ArrayResult], stdioItems, encoding, state);
|
|
const {serializedResult, finalResult = serializedResult} = serializeChunks({
|
|
chunks,
|
|
objectMode,
|
|
encoding,
|
|
lines,
|
|
stripFinalNewline,
|
|
fdNumber,
|
|
});
|
|
|
|
logOutputSync({
|
|
serializedResult,
|
|
fdNumber,
|
|
state,
|
|
verboseInfo,
|
|
encoding,
|
|
stdioItems,
|
|
objectMode,
|
|
});
|
|
|
|
const returnedResult = buffer[fdNumber] ? finalResult : undefined;
|
|
|
|
try {
|
|
if (state.error === undefined) {
|
|
writeToFiles(serializedResult, stdioItems, outputFiles);
|
|
}
|
|
|
|
return returnedResult;
|
|
} catch (error) {
|
|
state.error = error;
|
|
return returnedResult;
|
|
}
|
|
};
|
|
|
|
// Applies transform generators to `stdout`/`stderr`
|
|
const runOutputGeneratorsSync = (chunks, stdioItems, encoding, state) => {
|
|
try {
|
|
return runGeneratorsSync(chunks, stdioItems, encoding, false);
|
|
} catch (error) {
|
|
state.error = error;
|
|
return chunks;
|
|
}
|
|
};
|
|
|
|
// The contents is converted to three stages:
|
|
// - serializedResult: used when the target is a file path/URL or a file descriptor (including 'inherit')
|
|
// - finalResult/returnedResult: returned as `result.std*`
|
|
const serializeChunks = ({chunks, objectMode, encoding, lines, stripFinalNewline, fdNumber}) => {
|
|
if (objectMode) {
|
|
return {serializedResult: chunks};
|
|
}
|
|
|
|
if (encoding === 'buffer') {
|
|
return {serializedResult: joinToUint8Array(chunks)};
|
|
}
|
|
|
|
const serializedResult = joinToString(chunks, encoding);
|
|
if (lines[fdNumber]) {
|
|
return {serializedResult, finalResult: splitLinesSync(serializedResult, !stripFinalNewline[fdNumber], objectMode)};
|
|
}
|
|
|
|
return {serializedResult};
|
|
};
|
|
|
|
const logOutputSync = ({serializedResult, fdNumber, state, verboseInfo, encoding, stdioItems, objectMode}) => {
|
|
if (!shouldLogOutput({
|
|
stdioItems,
|
|
encoding,
|
|
verboseInfo,
|
|
fdNumber,
|
|
})) {
|
|
return;
|
|
}
|
|
|
|
const linesArray = splitLinesSync(serializedResult, false, objectMode);
|
|
|
|
try {
|
|
logLinesSync(linesArray, fdNumber, verboseInfo);
|
|
} catch (error) {
|
|
state.error ??= error;
|
|
}
|
|
};
|
|
|
|
// When the `std*` target is a file path/URL or a file descriptor
|
|
const writeToFiles = (serializedResult, stdioItems, outputFiles) => {
|
|
for (const {path, append} of stdioItems.filter(({type}) => FILE_TYPES.has(type))) {
|
|
const pathString = typeof path === 'string' ? path : path.toString();
|
|
if (append || outputFiles.has(pathString)) {
|
|
appendFileSync(path, serializedResult);
|
|
} else {
|
|
outputFiles.add(pathString);
|
|
writeFileSync(path, serializedResult);
|
|
}
|
|
}
|
|
};
|