Files
poshmanager/server/services/installerIntelService.js
2026-06-25 12:05:53 -05:00

77 lines
3.0 KiB
JavaScript

import { INSTALLER_TECHNOLOGIES } from '../data/installerCatalog.js';
// Installer intelligence: infer the installer technology from a file name and
// return recommended silent install/uninstall commands plus a detection
// suggestion. Pure + table-driven, so it is fully unit-tested. Binary signature
// sniffing (reading the file) is Phase 5; this works from the name/extension.
function extensionOf(fileName = '') {
const match = String(fileName).toLowerCase().match(/(\.[a-z0-9]+)$/);
return match ? match[1] : '';
}
function render(template = '', { file = '', productCode = '' } = {}) {
return String(template)
.split('{file}').join(file)
.split('{productCode}').join(productCode || '{productCode}');
}
function shape(tech, ctx) {
return {
id: tech.id,
label: tech.label,
install: render(tech.install, ctx),
uninstall: render(tech.uninstall, ctx),
psadtInstall: render(tech.psadtInstall, ctx),
psadtUninstall: tech.psadtUninstall ? render(tech.psadtUninstall, ctx) : '',
detection: tech.detection,
notes: tech.notes
};
}
export function listInstallerTechnologies() {
return INSTALLER_TECHNOLOGIES.map(({ id, label, extensions, detection, notes }) => ({ id, label, extensions, detection, notes }));
}
// Analyze a file name. Returns the best-guess technology, a confidence level,
// and the full set of candidate technologies for that extension so the operator
// can pick when the name alone is ambiguous (common for .exe).
export function analyzeInstaller({ fileName = '', productCode = '' } = {}) {
const ext = extensionOf(fileName);
const lower = String(fileName).toLowerCase();
const ctx = { file: fileName, productCode };
const byExt = INSTALLER_TECHNOLOGIES.filter((tech) => tech.extensions.includes(ext));
if (!byExt.length) {
return {
fileName,
extension: ext,
confidence: 'unknown',
primary: null,
candidates: [],
message: ext ? `No installer technology is known for "${ext}".` : 'Provide an installer file name with an extension.'
};
}
// Unique extension (.msi/.msp/.msix...) → high confidence, single technology.
if (byExt.length === 1) {
return { fileName, extension: ext, confidence: 'high', primary: shape(byExt[0], ctx), candidates: [shape(byExt[0], ctx)] };
}
// Ambiguous (.exe): try a name hint, otherwise fall back to generic EXE.
const hinted = byExt.find((tech) => (tech.hints || []).some((hint) => lower.includes(hint)) && tech.id !== 'exe');
const generic = byExt.find((tech) => tech.id === 'exe') || byExt[0];
const primary = hinted || generic;
return {
fileName,
extension: ext,
confidence: hinted ? 'medium' : 'low',
primary: shape(primary, ctx),
// Offer all candidates (generic last) so the UI can present a picker.
candidates: [...byExt.filter((t) => t.id !== 'exe'), ...byExt.filter((t) => t.id === 'exe')].map((t) => shape(t, ctx)),
message: hinted
? `Matched "${hinted.label}" from the file name.`
: 'Ambiguous EXE — pick the matching technology, or confirm the silent switch with the vendor.'
};
}