Lumi/plugins/lumi_transcription/backend/companion/package_service.js
2026-07-22 20:57:50 +02:00

78 lines
4.2 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip");
const { ArtifactManager } = require("../models/artifact_manager");
class CompanionPackageService {
constructor(root, manifest, options = {}) {
this.root = root;
this.manifestPath = typeof manifest === "string" ? manifest : null;
this.manifest = typeof manifest === "string" ? null : manifest;
this.artifacts = new ArtifactManager(root, { fetch: options.fetch });
fs.mkdirSync(root, { recursive: true });
}
currentManifest() { return this.manifestPath ? JSON.parse(fs.readFileSync(this.manifestPath, "utf8")) : this.manifest; }
entry(manifest = this.currentManifest()) { return manifest?.artifacts?.find((entry) => entry.platform === "win32" && entry.architecture === "x64") || null; }
installerEntry(manifest = this.currentManifest()) { return manifest?.installer || null; }
status() {
const manifest = this.currentManifest();
const entry = this.entry(manifest);
if (!entry) return { available: false, installed: false, valid: false, reason: "No Windows Companion artifact is configured." };
const artifact = this.artifacts.status(entry);
return { available: true, version: manifest.version, artifact: entry.id, ...artifact };
}
async build(pairing) {
const manifest = this.currentManifest();
const entry = this.entry(manifest);
if (!entry) throw new Error("No Windows Companion artifact is configured.");
const output = new AdmZip();
const installer = this.installerEntry(manifest);
if (installer) {
let installerStatus = this.artifacts.status(installer);
if (!installerStatus.valid) installerStatus = await this.artifacts.download(installer, { confirmed: true });
const installerName = safeArchivePath(installer.filename || "Lumi.Companion-Setup.exe");
if (!installerName.toLowerCase().endsWith(".exe")) throw new Error("The Companion installer entrypoint is invalid.");
output.addFile(installerName, fs.readFileSync(installerStatus.path));
} else {
let status = this.artifacts.status(entry);
if (!status.valid) status = await this.artifacts.download(entry, { confirmed: true });
const source = new AdmZip(status.path);
let expandedBytes = 0;
for (const item of source.getEntries()) {
const name = safeArchivePath(item.entryName);
if (!name || item.isDirectory) continue;
const body = item.getData();
expandedBytes += body.length;
if (expandedBytes > 300 * 1024 * 1024) throw new Error("The Companion artifact exceeds its expanded size limit.");
output.addFile(name, body);
}
if (!output.getEntry(entry.entrypoint)) throw new Error("The Companion artifact is missing its expected application entrypoint.");
}
const pairingName = `lumi-companion-${pairing.pairing_id}.lumi-pairing.json`;
output.addFile(pairingName, Buffer.from(`${JSON.stringify(pairing.bootstrap, null, 2)}\n`, "utf8"));
output.addFile("START-HERE.txt", Buffer.from([
"Lumi Companion — experimental transcription MVP", "",
"1. Extract both files in this ZIP to a temporary folder on the Windows streaming computer.",
`2. Start ${installer?.filename || entry.entrypoint} within 15 minutes.`,
"3. Setup installs Companion in your Windows account, imports the adjacent one-time pairing package, and launches the durable installed copy.",
"4. After setup completes, this extracted folder can be deleted. Use the Start menu to open Lumi Companion.", "",
"Do not share this ZIP. Its pairing package works once and expires after 15 minutes.",
"Windows may warn because this experimental build is not code-signed yet.", ""
].join("\r\n"), "utf8"));
return { buffer: output.toBuffer(), filename: `Lumi-Companion-${manifest.version}-paired.zip`, pairingName };
}
}
function safeArchivePath(value) {
const portable = String(value || "").replace(/\\/g, "/");
const normalized = path.posix.normalize(portable).replace(/^\/+/, "");
if (!normalized || normalized === ".." || normalized.startsWith("../") || /^[A-Za-z]:/.test(normalized)) throw new Error("The Companion artifact contains an unsafe path.");
return normalized;
}
module.exports = { CompanionPackageService, safeArchivePath };