UNPKG

hana-cli

Version:
369 lines (327 loc) 10.9 kB
// @ts-check /** * @module base-lite - Lightweight utilities for CLI initialization (no heavy dependencies) */ import { fileURLToPath } from 'url' import { URL } from 'url' const __dirname = fileURLToPath(new URL('.', import.meta.url)) import { createRequire } from 'module' export const require = createRequire(import.meta.url) import * as path from 'path' import fs from 'fs' /** @type {typeof import("chalk")} */ import chalk from 'chalk' export const colors = chalk // Global configuration storage let _config = {} /** * Set global configuration (called from cli.js at startup) * @param {Object} config Configuration object */ export function setConfig(config) { _config = config || {} } /** * Get global configuration * @returns {Object} Configuration object */ export function getConfig() { return _config } /** * Get a specific configuration value with dot notation support * @param {string} key Configuration key (supports dot notation for nested access) * @param {*} defaultValue Default value if key not found * @returns {*} Configuration value or default */ export function getConfigValue(key, defaultValue = undefined) { const keys = key.split('.') let value = _config for (const k of keys) { if (value && typeof value === 'object' && k in value) { value = value[k] } else { return defaultValue } } return value } /** @type {typeof import("debug") } */ // Lazy-load debug module only if DEBUG env var is set (saves ~8ms on startup) let _debug = null let _debugEnabled = false export const debug = (...args) => { // Check if DEBUG was just enabled (e.g., by --debug flag at runtime) if (process.env.DEBUG && !_debugEnabled) { _debug = null _debugEnabled = true } if (!_debug) { // Only load debug module if DEBUG is enabled if (process.env.DEBUG) { const debugModule = require('debug') _debug = debugModule('hana-cli') _debugEnabled = true } else { // No-op function when debug is disabled _debug = () => {} _debugEnabled = false } } return _debug(...args) } import * as locale from "./locale.js" const TextBundle = require('@sap/textbundle').TextBundle /** * Parse .properties file content into a key-value map. * @param {string} content * @returns {Record<string, string>} */ function parseProperties(content) { /** @type {Record<string, string>} */ const entries = {} const lines = content.split(/\r?\n/) for (const line of lines) { const trimmed = line.trim() if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('!')) { continue } const separatorIndex = trimmed.search(/[:=]/) if (separatorIndex === -1) { continue } const key = trimmed.slice(0, separatorIndex).trim() const value = trimmed.slice(separatorIndex + 1).trim() if (key) { entries[key] = value } } return entries } /** * Load .properties file content if present. * @param {string} filePath * @returns {Record<string, string>} */ function loadPropertiesFile(filePath) { try { if (!fs.existsSync(filePath)) { return {} } const content = fs.readFileSync(filePath, 'utf-8') return parseProperties(content) } catch { return {} } } /** * Load additional text resources for a given base name and locale. * @param {string} baseName * @param {string} localeTag * @returns {Record<string, string>} */ function loadAdditionalTexts(baseName, localeTag) { const basePath = path.join(__dirname, '..', '/_i18n', `${baseName}.properties`) let texts = loadPropertiesFile(basePath) const candidates = [] if (localeTag) { candidates.push(localeTag) const languageOnly = localeTag.split(/[_-]/)[0] if (languageOnly && languageOnly !== localeTag) { candidates.push(languageOnly) } } for (const candidate of candidates) { const localizedPath = path.join(__dirname, '..', '/_i18n', `${baseName}_${candidate}.properties`) texts = { ...texts, ...loadPropertiesFile(localizedPath) } } return texts } /** * Format text with {n} placeholders. * @param {string} value * @param {Array<any>} args * @returns {string} */ function formatText(value, args) { if (!args || args.length === 0) { return value } return value.replace(/\{(\d+)\}/g, (match, index) => { const replacement = args[Number(index)] return replacement !== undefined ? String(replacement) : match }) } const normalizedLocale = locale.normalizeLocale(locale.getLocale()) const baseBundle = new TextBundle(path.join(__dirname, '..', '/_i18n/messages'), normalizedLocale) const additionalBundles = [ 'duplicateDetection', 'compareData', 'dataDiff', 'dataLineage', 'dataProfile', 'dataValidator', 'export', 'referentialCheck', 'interactive' ] const additionalTexts = additionalBundles.reduce((acc, bundleName) => { return { ...acc, ...loadAdditionalTexts(bundleName, normalizedLocale) } }, {}) /** @typeof TextBundle - instance of sap/textbundle */ export const bundle = new Proxy(baseBundle, { get(target, prop) { if (prop === 'getText') { return (key, args) => { if (Object.prototype.hasOwnProperty.call(additionalTexts, key)) { return formatText(additionalTexts[key], args) } return baseBundle.getText(key, args) } } return target[prop] } }) /** * Output an error to the console * @param {Error} err - Error object or error message * @returns {Promise<void>} */ export async function error(err) { console.error(colors.red(`${bundle.getText("error")} ${err.toString()}`)) if (err.stack) { console.error(colors.red(err.stack)) } } /** * Build yargs options with common connection and debug parameters * Applies configuration defaults from .hana-cli-config or hana-cli.config.js * @param {object} input - Command-specific options * @param {boolean} [iConn=true] - Include connection parameters * @param {boolean} [iDebug=true] - Include debug parameters * @returns {object} Combined options object */ export function getBuilder(input, iConn = true, iDebug = true) { let grpConn = {} let grpDebug = {} if (iConn) { grpConn = { admin: { alias: ['a', 'Admin'], type: 'boolean', default: getConfigValue('admin', false), group: bundle.getText("grpConn"), desc: bundle.getText("admin") }, conn: { default: getConfigValue('conn'), group: bundle.getText("grpConn"), desc: bundle.getText("connFile") }, } } if (iDebug) { grpDebug = { disableVerbose: { alias: ['quiet'], group: bundle.getText("grpDebug"), type: 'boolean', default: getConfigValue('disableVerbose', false), desc: bundle.getText("disableVerbose") }, debug: { alias: ['d', 'Debug'], group: bundle.getText("grpDebug"), type: 'boolean', default: getConfigValue('debug', false), desc: bundle.getText("debug") } } } // Merge strategy: defaults (grpConn, grpDebug) are overridden by command-specific input // Only add default profile if not already defined in input let grpConnFinal = { ...grpConn } if (!input.profile && iConn) { grpConnFinal.profile = { alias: ['p'], type: 'string', default: getConfigValue('profile'), group: bundle.getText("grpConn"), desc: bundle.getText("profile") } } let builder = { ...grpConnFinal, ...grpDebug, ...input // Input options override defaults } const ensureAlias = (option, aliasValue) => { if (!option || !aliasValue) { return } if (!option.alias) { option.alias = [] } if (typeof option.alias === 'string') { option.alias = [option.alias] } if (!option.alias.includes(aliasValue)) { option.alias.push(aliasValue) } } ensureAlias(builder.schema, 'Schema') ensureAlias(builder.table, 'Table') ensureAlias(builder.debug, 'Debug') // Apply config defaults to input options - only if not already set in input for (const [key, option] of Object.entries(builder)) { if (option && typeof option === 'object' && !('default' in option)) { const configValue = getConfigValue(key) if (configValue !== undefined) { option.default = configValue } } } // Apply global config defaults if (getConfigValue('defaultSchema')) { if (builder.schema && !builder.schema.default) { builder.schema.default = getConfigValue('defaultSchema') } } if (getConfigValue('outputFormat')) { if (builder.outputFormat && !builder.outputFormat.default) { builder.outputFormat.default = getConfigValue('outputFormat') } } if (getConfigValue('language')) { // Language is typically handled globally, not per-command } return builder } /** * Extension of getBuilder for UI commands that require port and host options * @param {Object} input - Command-specific parameters * @param {boolean} [iConn=true] - Include connection parameters * @param {boolean} [iDebug=true] - Include debug parameters * @returns {Object} Builder configuration with UI-specific options (port, host) */ export function getUIBuilder(input = {}, iConn = true, iDebug = true) { // Check if input has profile parameter (which uses 'p' alias) const hasProfile = input.profile !== undefined const uiOptions = { port: { // Only use 'p' alias if profile is not in input or if iConn is false alias: (!hasProfile && !iConn) ? ['p'] : [], type: 'number', default: 3010, desc: bundle.getText("port") || 'Server port (default: 3010)' }, host: { type: 'string', default: 'localhost', desc: bundle.getText("host") || 'Server host (default: localhost)' } } // Merge input with UI options, input takes precedence const mergedInput = { ...uiOptions, ...input } return getBuilder(mergedInput, iConn, iDebug) }