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>
230 lines
9.8 KiB
JavaScript
230 lines
9.8 KiB
JavaScript
import path from 'node:path';
|
||
import fsp, { writeFile } from 'node:fs/promises';
|
||
import os from 'node:os';
|
||
import cp from 'node:child_process';
|
||
import { format } from 'node:util';
|
||
import { XMLParser } from 'fast-xml-parser';
|
||
import { BlobReader, BlobWriter, ZipReader } from '@zip.js/zip.js';
|
||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||
import { HttpProxyAgent } from 'http-proxy-agent';
|
||
import findEdgePath from './finder.js';
|
||
import { TAGGED_VERSIONS, EDGE_PRODUCTS_API, EDGEDRIVER_BUCKET, TAGGED_VERSION_URL, LATEST_RELEASE_URL, DOWNLOAD_URL, BINARY_FILE, log } from './constants.js';
|
||
import { hasAccess, getNameByArchitecture, sleep, extractBasicAuthFromUrl } from './utils.js';
|
||
const fetchOpts = {};
|
||
if (process.env.HTTPS_PROXY) {
|
||
fetchOpts.agent = new HttpsProxyAgent(process.env.HTTPS_PROXY);
|
||
}
|
||
else if (process.env.HTTP_PROXY) {
|
||
fetchOpts.agent = new HttpProxyAgent(process.env.HTTP_PROXY);
|
||
}
|
||
export async function download(edgeVersion = process.env.EDGEDRIVER_VERSION, cacheDir = process.env.EDGEDRIVER_CACHE_DIR || os.tmpdir()) {
|
||
const binaryFilePath = path.resolve(cacheDir, BINARY_FILE);
|
||
if (await hasAccess(binaryFilePath)) {
|
||
return binaryFilePath;
|
||
}
|
||
if (!edgeVersion) {
|
||
const edgePath = findEdgePath();
|
||
if (!edgePath) {
|
||
throw new Error('Could not find Microsoft Edge binary, please make sure the browser is installed on your system.');
|
||
}
|
||
log.info(`Trying to detect Microsoft Edge version from binary found at ${edgePath}`);
|
||
edgeVersion = os.platform() === 'win32' ? await getEdgeVersionWin(edgePath) : await getEdgeVersionUnix(edgePath);
|
||
log.info(`Detected Microsoft Edge v${edgeVersion}`);
|
||
}
|
||
const version = await fetchVersion(edgeVersion);
|
||
const res = await downloadDriver(version);
|
||
await fsp.mkdir(cacheDir, { recursive: true });
|
||
await downloadZip(res, cacheDir);
|
||
await fsp.chmod(binaryFilePath, '755');
|
||
log.info('Finished downloading Edgedriver');
|
||
await sleep(); // wait for file to be accessible, avoid ETXTBSY errors
|
||
return binaryFilePath;
|
||
}
|
||
async function downloadDriver(version) {
|
||
try {
|
||
const rawDownloadUrl = format(DOWNLOAD_URL, version, getNameByArchitecture());
|
||
const { url: downloadUrl, authHeader } = extractBasicAuthFromUrl(rawDownloadUrl);
|
||
log.info(`Downloading Edgedriver from ${downloadUrl}`);
|
||
const opts = { ...fetchOpts };
|
||
if (authHeader) {
|
||
opts.headers = { ...opts.headers, Authorization: authHeader };
|
||
}
|
||
const res = await fetch(downloadUrl, opts);
|
||
if (!res.body || !res.ok || res.status !== 200) {
|
||
throw new Error(`Failed to download binary from ${downloadUrl} (statusCode ${res.status})`);
|
||
}
|
||
return res;
|
||
}
|
||
catch (err) {
|
||
log.error(`Failed to download Edgedriver: ${err.message}, trying alternative download URL...`);
|
||
}
|
||
try {
|
||
const majorVersion = version.split('.')[0];
|
||
const platform = process.platform === 'darwin'
|
||
? 'macos'
|
||
: process.platform === 'win32'
|
||
? 'windows'
|
||
: 'linux';
|
||
log.info(`Attempt to fetch latest v${majorVersion} for ${platform} from ${EDGEDRIVER_BUCKET}`);
|
||
const versions = await fetch(EDGEDRIVER_BUCKET, {
|
||
...fetchOpts,
|
||
headers: {
|
||
accept: '*/*',
|
||
'accept-language': 'en-US,en;q=0.9',
|
||
'cache-control': 'no-cache',
|
||
'content-type': 'application/json; charset=utf-8',
|
||
pragma: 'no-cache',
|
||
}
|
||
});
|
||
const parser = new XMLParser();
|
||
const { EnumerationResults } = parser.parse(await versions.text());
|
||
const blobName = `LATEST_RELEASE_${majorVersion}_${platform.toUpperCase()}`;
|
||
const alternativeDownloadUrl = EnumerationResults.Blobs.Blob
|
||
.find((blob) => blob.Name === blobName).Url;
|
||
if (!alternativeDownloadUrl) {
|
||
throw new Error(`Couldn't find alternative download URL for ${version}`);
|
||
}
|
||
log.info(`Downloading alternative Edgedriver version from ${alternativeDownloadUrl}`);
|
||
const versionResponse = await fetch(alternativeDownloadUrl, fetchOpts);
|
||
const alternativeVersion = sanitizeVersion(await versionResponse.text());
|
||
const rawDownloadUrl = format(DOWNLOAD_URL, alternativeVersion, getNameByArchitecture());
|
||
const { url: downloadUrl, authHeader } = extractBasicAuthFromUrl(rawDownloadUrl);
|
||
log.info(`Downloading Edgedriver from ${downloadUrl}`);
|
||
const opts = { ...fetchOpts };
|
||
if (authHeader) {
|
||
opts.headers = { ...opts.headers, Authorization: authHeader };
|
||
}
|
||
const res = await fetch(downloadUrl, opts);
|
||
if (!res.body || !res.ok || res.status !== 200) {
|
||
throw new Error(`Failed to download binary from ${downloadUrl} (statusCode ${res.status})`);
|
||
}
|
||
return res;
|
||
}
|
||
catch (err) {
|
||
throw new Error(`Failed to download Edgedriver: ${err.message}`);
|
||
}
|
||
}
|
||
async function getEdgeVersionWin(edgePath) {
|
||
const versionPath = path.dirname(edgePath);
|
||
const contents = await fsp.readdir(versionPath);
|
||
const versions = contents.filter((p) => /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/g.test(p));
|
||
// returning oldest in case there is an updated version and Edge still hasn't relaunched
|
||
const oldest = versions.sort((a, b) => a > b ? 1 : -1)[0];
|
||
return oldest;
|
||
}
|
||
async function getEdgeVersionUnix(edgePath) {
|
||
log.info(`Trying to detect Microsoft Edge version from binary found at ${edgePath}`);
|
||
const versionOutput = await new Promise((resolve, reject) => cp.exec(`"${edgePath}" --version`, (err, stdout, stderr) => {
|
||
if (err) {
|
||
return reject(err);
|
||
}
|
||
if (stderr) {
|
||
return reject(new Error(stderr));
|
||
}
|
||
return resolve(stdout);
|
||
}));
|
||
/**
|
||
* example output: "Microsoft Edge 124.0.2478.105 unknown"
|
||
*/
|
||
return versionOutput
|
||
/**
|
||
* trim the output
|
||
*/
|
||
.trim()
|
||
/**
|
||
* split by space, e.g. `[Microsoft, Edge, 124.0.2478.105, unknown]
|
||
*/
|
||
.split(' ')
|
||
/**
|
||
* filter for entity that matches the version pattern, e.g. `124.0.2478.105`
|
||
*/
|
||
.filter((v) => v.match(/\d+\.\d+\.\d+\.\d+/g))
|
||
/**
|
||
* get the first entity
|
||
*/
|
||
.pop();
|
||
}
|
||
export async function fetchVersion(edgeVersion) {
|
||
const p = os.platform();
|
||
const platform = p === 'win32' ? 'win' : p === 'darwin' ? 'mac' : 'linux';
|
||
/**
|
||
* if version has 4 digits it is a valid version, e.g. 109.0.1467.0
|
||
*/
|
||
if (edgeVersion.split('.').length === 4) {
|
||
return edgeVersion;
|
||
}
|
||
/**
|
||
* if browser version is a tagged version, e.g. stable, beta, dev, canary
|
||
*/
|
||
if (TAGGED_VERSIONS.includes(edgeVersion.toLowerCase())) {
|
||
const apiResponse = await fetch(EDGE_PRODUCTS_API, fetchOpts).catch((err) => {
|
||
log.error(`Couldn't fetch version from ${EDGE_PRODUCTS_API}: ${err.stack}`);
|
||
return { json: async () => [] };
|
||
});
|
||
const products = await apiResponse.json();
|
||
const product = products.find((p) => p.Product.toLowerCase() === edgeVersion.toLowerCase());
|
||
const productVersion = product?.Releases.find((r) => (
|
||
/**
|
||
* On Mac we all product versions are universal to its architecture
|
||
*/
|
||
(platform === 'mac' && r.Platform === 'MacOS') ||
|
||
/**
|
||
* On Windows we need to check for the architecture
|
||
*/
|
||
(platform === 'win' && r.Platform === 'Windows' && os.arch() === r.Architecture) ||
|
||
/**
|
||
* On Linux we only have one architecture
|
||
*/
|
||
(platform === 'linux' && r.Platform === 'Linux')))?.ProductVersion;
|
||
if (productVersion) {
|
||
return productVersion;
|
||
}
|
||
const res = await fetch(format(TAGGED_VERSION_URL, edgeVersion.toUpperCase()), fetchOpts);
|
||
return sanitizeVersion(await res.text());
|
||
}
|
||
/**
|
||
* check for a number in the version and check for that
|
||
*/
|
||
const MATCH_VERSION = /\d+/g;
|
||
if (edgeVersion.match(MATCH_VERSION)) {
|
||
const [major] = edgeVersion.match(MATCH_VERSION);
|
||
const url = format(LATEST_RELEASE_URL, major.toString().toUpperCase(), platform.toUpperCase());
|
||
log.info(`Fetching latest version from ${url}`);
|
||
const res = await fetch(url, fetchOpts);
|
||
if (!res.ok || res.status !== 200) {
|
||
throw new Error(`Couldn't detect version for ${edgeVersion}`);
|
||
}
|
||
return sanitizeVersion(await res.text());
|
||
}
|
||
throw new Error(`Couldn't detect version for ${edgeVersion}`);
|
||
}
|
||
async function downloadZip(res, cacheDir) {
|
||
const zipBlob = await res.blob();
|
||
const zip = new ZipReader(new BlobReader(zipBlob));
|
||
for (const entry of await zip.getEntries()) {
|
||
const unzippedFilePath = path.join(cacheDir, entry.filename);
|
||
if (entry.directory) {
|
||
continue;
|
||
}
|
||
const fileEntry = entry;
|
||
if (!await hasAccess(path.dirname(unzippedFilePath))) {
|
||
await fsp.mkdir(path.dirname(unzippedFilePath), { recursive: true });
|
||
}
|
||
const content = await fileEntry.getData(new BlobWriter());
|
||
await writeFile(unzippedFilePath, content.stream());
|
||
}
|
||
}
|
||
/**
|
||
* Fetching the latest version from the CDN contains extra characters that need to be removed,
|
||
* e.g. "<22><>127.0.2651.87\n\n"
|
||
*/
|
||
function sanitizeVersion(version) {
|
||
return version.replace(/\0/g, '').slice(2).trim();
|
||
}
|
||
/**
|
||
* download on install
|
||
*/
|
||
if (process.argv[1] && process.argv[1].endsWith('/dist/install.js') && Boolean(process.env.EDGEDRIVER_AUTO_INSTALL)) {
|
||
await download().then(() => log.info('Success!'), (err) => log.error(`Failed to install Edgedriver: ${err.stack}`));
|
||
}
|
||
//# sourceMappingURL=install.js.map
|