/** * Profile Manager * Handles multiple WMS profiles (AD, LIMAGRAIN, ...) defined in .env. * * Shared settings (same for all profiles): * - WMS_API_AUTH OAuth client Basic auth header * - WMS_APPLICATION Application name (e.g. "EasyWMS") * - WMS_API_PATH URL path to the ApplicationService API * - WMS_TOKEN_PATH URL path to the OAuth token endpoint * - WORKFLOW_API_PATH URL path to the AD API * * Per-profile settings (prefixed by profile name): * - _HOST Hostname or IP (e.g. "10.255.255.2", "p4swms.mss.mecalux.com") * - _USERNAME * - _PASSWORD * - _TENANT * - _SAAS "true" | "false" (default false). When true the WMS is * cloud-hosted — local filesystem logs are not reachable, * so log tools return an error instead of probing paths. * * Profile registry: * - WMS_PROFILES Comma-separated list of profile names (e.g. "AD,LIMAGRAIN") * - DEFAULT_WMS_PROFILE Profile activated at startup (optional) */ const DEFAULTS = { apiPath: '/ApplicationService/api', tokenPath: '/EasySTS/OAuth/Token', workflowApiPath: '/AD/api', application: 'EasyWMS', protocol: 'https', }; const listeners = []; let profiles = {}; let currentProfileName = null; /** * Parse .env into an in-memory profile registry. * Called once at startup; safe to call again after env changes (tests). */ function loadProfiles() { profiles = {}; currentProfileName = null; const names = (process.env.WMS_PROFILES || '') .split(',') .map(s => s.trim()) .filter(Boolean); if (names.length === 0) { console.error('[Profile] WARNING: WMS_PROFILES is empty — no profiles loaded'); return; } const shared = { auth: process.env.WMS_API_AUTH, application: process.env.WMS_APPLICATION || DEFAULTS.application, apiPath: process.env.WMS_API_PATH || DEFAULTS.apiPath, tokenPath: process.env.WMS_TOKEN_PATH || DEFAULTS.tokenPath, workflowApiPath: process.env.WORKFLOW_API_PATH || DEFAULTS.workflowApiPath, }; for (const name of names) { const host = process.env[`${name}_HOST`]; const username = process.env[`${name}_USERNAME`]; const password = process.env[`${name}_PASSWORD`]; const tenant = process.env[`${name}_TENANT`]; const saasRaw = process.env[`${name}_SAAS`]; const saas = saasRaw != null && /^(true|1|yes)$/i.test(saasRaw.trim()); if (!host || !username || !password || !tenant) { console.error(`[Profile] WARNING: profile "${name}" is incomplete (missing HOST/USERNAME/PASSWORD/TENANT) — skipped`); continue; } const base = `${DEFAULTS.protocol}://${host}`; profiles[name] = { name, host, username, password, tenant, saas, auth: shared.auth, application: shared.application, apiBaseUrl: `${base}${shared.apiPath}`, tokenUrl: `${base}${shared.tokenPath}`, workflowApiBase: `${base}${shared.workflowApiPath}`, }; } const defaultName = process.env.DEFAULT_WMS_PROFILE; if (defaultName && profiles[defaultName]) { currentProfileName = defaultName; console.error(`[Profile] Loaded ${Object.keys(profiles).length} profile(s). Active: ${currentProfileName}`); } else if (defaultName) { console.error(`[Profile] DEFAULT_WMS_PROFILE="${defaultName}" is not a valid profile — no profile active`); } else { console.error(`[Profile] Loaded ${Object.keys(profiles).length} profile(s). No default selected — Claude must call switch_wms_profile before any API call`); } } function listProfiles() { return Object.keys(profiles).sort(); } function getCurrentName() { return currentProfileName; } /** * Return the active profile config. Throws a helpful error if none is selected, * so tool responses tell Claude exactly what to do next. */ function getCurrent() { if (!currentProfileName) { const available = listProfiles(); const hint = available.length > 0 ? `Available profiles: ${available.join(', ')}. Call switch_wms_profile with one of them.` : 'No profiles configured in .env (WMS_PROFILES is empty).'; throw new Error(`No WMS profile selected. ${hint}`); } return profiles[currentProfileName]; } /** * Switch to a different profile. Notifies listeners (services) so they can * reset token + invalidate caches tied to the previous profile. */ function switchTo(name) { if (!profiles[name]) { const available = listProfiles(); throw new Error(`Unknown profile "${name}". Available: ${available.join(', ') || '(none)'}`); } if (name === currentProfileName) { return profiles[name]; } const previous = currentProfileName; currentProfileName = name; console.error(`[Profile] Switched: ${previous || '(none)'} → ${name}`); for (const listener of listeners) { try { listener({ previous, current: name }); } catch (err) { console.error(`[Profile] Listener error: ${err.message}`); } } return profiles[name]; } /** * Register a callback fired whenever the active profile changes. * Used by services to invalidate caches / reset tokens. */ function onSwitch(listener) { listeners.push(listener); } module.exports = { loadProfiles, listProfiles, getCurrent, getCurrentName, switchTo, onSwitch, };