78 lines
4.2 KiB
JavaScript
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 };
|