Initial
This commit is contained in:
76
server/services/installerIntelService.js
Normal file
76
server/services/installerIntelService.js
Normal file
@@ -0,0 +1,76 @@
|
||||
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.'
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user