UNPKG

@elsikora/setup-wizard

Version:

Setup Wizard - CLI scaffolding utility

335 lines (332 loc) 16.4 kB
#!/usr/bin/env node import { CI_CONFIG } from '../../domain/constant/ci-config.constant.js'; import { ECiModuleType } from '../../domain/enum/ci-module-type.enum.js'; import { ECiModule } from '../../domain/enum/ci-module.enum.js'; import { ECiProvider } from '../../domain/enum/ci-provider.enum.js'; import { EModule } from '../../domain/enum/module.enum.js'; /** * Service for setting up and managing Continuous Integration (CI) modules. * Handles the selection, configuration, and setup of CI workflows for different providers. */ class CiModuleService { /** CLI interface service for user interaction */ CLI_INTERFACE_SERVICE; /** Configuration service for managing app configuration */ CONFIG_SERVICE; /** File system service for file operations */ FILE_SYSTEM_SERVICE; /** Cached CI configuration */ config = null; /** Selected CI modules to install */ selectedModules = []; /** Selected CI provider (e.g., GitHub) */ selectedProvider; /** * Initializes a new instance of the CiModuleService. * @param cliInterfaceService - Service for CLI user interactions * @param fileSystemService - Service for file system operations * @param configService - Service for managing app configuration */ constructor(cliInterfaceService, fileSystemService, configService) { this.CLI_INTERFACE_SERVICE = cliInterfaceService; this.FILE_SYSTEM_SERVICE = fileSystemService; this.CONFIG_SERVICE = configService; } /** * Handles existing CI setup files. * Checks for existing CI configuration files and asks for user confirmation if found. * @returns Promise resolving to true if setup should proceed, false otherwise */ async handleExistingSetup() { try { const existingFiles = await this.findExistingCiFiles(); if (existingFiles.length === 0) { return true; } this.CLI_INTERFACE_SERVICE.warn("Found existing CI configuration files that might be modified:\n" + existingFiles.map((file) => `- ${file}`).join("\n")); return await this.CLI_INTERFACE_SERVICE.confirm("Do you want to continue? This might overwrite existing files.", false); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError("Failed to check existing CI setup", error); return false; } } /** * Installs and configures selected CI modules. * Guides the user through selecting and configuring CI modules. * @returns Promise resolving to the module setup result */ async install() { try { this.config = await this.CONFIG_SERVICE.getModuleConfig(EModule.CI); if (!(await this.shouldInstall())) { return { wasInstalled: false }; } const moduleType = await this.determineModuleType(this.config?.moduleProperties?.[ECiModule.RELEASE_NPM] === true); this.selectedProvider = await this.selectProvider(this.config?.provider); this.selectedModules = await this.selectCompatibleModules(moduleType, this.config?.modules ?? []); if (this.selectedModules.length === 0) { this.CLI_INTERFACE_SERVICE.warn("No CI modules selected."); return { wasInstalled: false }; } if (!(await this.handleExistingSetup())) { this.CLI_INTERFACE_SERVICE.warn("Setup cancelled by user."); return { wasInstalled: false }; } const moduleProperties = await this.setupSelectedModules(this.config?.moduleProperties ?? {}); const customProperties = { isNpmPackage: moduleType === ECiModuleType.NPM_ONLY, moduleProperties, modules: this.selectedModules, provider: this.selectedProvider, }; return { customProperties, wasInstalled: true, }; } catch (error) { this.CLI_INTERFACE_SERVICE.handleError("Failed to complete CI setup", error); throw error; } } /** * Determines if the CI module should be installed. * Asks the user if they want to set up CI workflows. * Uses the saved config value as default if it exists. * @returns Promise resolving to true if the module should be installed, false otherwise */ async shouldInstall() { try { return await this.CLI_INTERFACE_SERVICE.confirm("Would you like to set up CI workflows?", await this.CONFIG_SERVICE.isModuleEnabled(EModule.CI)); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError("Failed to get user confirmation", error); return false; } } /** * Collects module-specific properties from the user. * @param module - The CI module to collect properties for * @param savedProperties - Previously saved properties for this module * @returns Promise resolving to a record of collected properties */ async collectModuleProperties(module, savedProperties = {}) { const properties = {}; if (module === ECiModule.DEPENDABOT) { const defaultBranch = savedProperties.devBranchName || "dev"; properties.devBranchName = await this.CLI_INTERFACE_SERVICE.text("Enter the target branch for Dependabot updates:", "dev", defaultBranch); } if (module === ECiModule.RELEASE || module === ECiModule.RELEASE_NPM) { let mainBranchDefault = savedProperties.mainBranch || "main"; let isPrereleaseDefault = savedProperties.isPrerelease ?? false; let preReleaseBranchDefault = savedProperties.preReleaseBranch || "dev"; if (!savedProperties.mainBranch || !Object.prototype.hasOwnProperty.call(savedProperties, "isPrerelease")) { // Semantic release config should already be loaded const semanticReleaseConfig = await this.CONFIG_SERVICE.getModuleConfig(EModule.SEMANTIC_RELEASE); if (semanticReleaseConfig) { let usedSemanticReleaseConfig = false; if (!savedProperties.mainBranch && semanticReleaseConfig.mainBranch) { mainBranchDefault = semanticReleaseConfig.mainBranch; usedSemanticReleaseConfig = true; } if (!Object.prototype.hasOwnProperty.call(savedProperties, "isPrerelease") && Object.prototype.hasOwnProperty.call(semanticReleaseConfig, "isPrereleaseEnabled")) { isPrereleaseDefault = semanticReleaseConfig.isPrereleaseEnabled ?? false; usedSemanticReleaseConfig = true; } if (!savedProperties.preReleaseBranch && semanticReleaseConfig.preReleaseBranch) { preReleaseBranchDefault = semanticReleaseConfig.preReleaseBranch; usedSemanticReleaseConfig = true; } if (usedSemanticReleaseConfig) { this.CLI_INTERFACE_SERVICE.info("Found semantic-release configuration. Using its values as defaults for release CI setup."); } } } properties.mainBranch = await this.CLI_INTERFACE_SERVICE.text("Enter the name of your main release branch:", "main", mainBranchDefault); const shouldEnablePrerelease = await this.CLI_INTERFACE_SERVICE.confirm("Do you want to configure CI for a pre-release branch?", isPrereleaseDefault); if (shouldEnablePrerelease) { properties.isPrerelease = true; properties.preReleaseBranch = await this.CLI_INTERFACE_SERVICE.text("Enter the name of your pre-release branch:", "dev", preReleaseBranchDefault); } else { properties.isPrerelease = false; } } return properties; } /** * Determines the type of CI module based on whether it's an NPM package. * @param isSavedNpmPackage - Whether the package was previously saved as an NPM package * @returns Promise resolving to the determined module type */ async determineModuleType(isSavedNpmPackage = false) { const isConfirmedByDefault = isSavedNpmPackage ?? false; const isNpmPackage = await this.CLI_INTERFACE_SERVICE.confirm("Is this package going to be published to NPM?", isConfirmedByDefault); return isNpmPackage ? ECiModuleType.NPM_ONLY : ECiModuleType.NON_NPM; } /** * Displays a summary of successful and failed CI module setups. * @param successful - Array of successfully set up modules * @param failed - Array of modules that failed to set up */ displaySetupSummary(successful, failed) { const summary = ["Successfully created configurations:", ...successful.map(({ module }) => `✓ ${CI_CONFIG[module].name}`)]; if (failed.length > 0) { summary.push("Failed configurations:", ...failed.map(({ error, module }) => `✗ ${CI_CONFIG[module].name} - ${error?.message ?? "Unknown error"}`)); } summary.push("", "The workflows will be activated when you push to the repository.", "", "Note: Make sure to set up required secrets in your CI provider."); this.CLI_INTERFACE_SERVICE.note("CI Setup Summary", summary.join("\n")); } /** * Extracts module-specific properties from a module configuration. * @param moduleConfig - The module configuration object or boolean * @returns Record of module properties, or empty object if none found */ extractModuleProperties(moduleConfig) { if (!moduleConfig) { return {}; } if (typeof moduleConfig === "boolean") { return {}; } if (typeof moduleConfig === "object" && "isEnabled" in moduleConfig) { const { isEnabled, ...properties } = moduleConfig; return properties; } return moduleConfig; } /** * Finds existing CI configuration files that might be overwritten. * @returns Promise resolving to an array of file paths for existing CI configurations */ async findExistingCiFiles() { if (!this.selectedProvider || !this.selectedModules || this.selectedModules.length === 0) { return []; } const existingFiles = []; for (const module of this.selectedModules) { const config = CI_CONFIG[module]; const providerConfig = config.content[this.selectedProvider]; if (providerConfig && (await this.FILE_SYSTEM_SERVICE.isPathExists(providerConfig.filePath))) { existingFiles.push(providerConfig.filePath); } } return existingFiles; } /** * Gets a human-readable description for a CI provider. * @param provider - The CI provider to get a description for * @returns Description string for the provider */ getProviderDescription(provider) { const descriptions = { [ECiProvider.GITHUB]: "GitHub Actions - Cloud-based CI/CD", }; return descriptions[provider] || provider; } /** * Selects compatible CI modules based on the module type and saved configuration. * @param moduleType - The type of CI module (NPM or non-NPM) * @param savedModules - Previously saved modules * @returns Promise resolving to an array of selected CI module enum values */ async selectCompatibleModules(moduleType, savedModules) { const compatibleModules = Object.entries(CI_CONFIG) .filter(([, config]) => { return config.type === ECiModuleType.UNIVERSAL || config.type === moduleType; }) .map(([key, config]) => ({ description: config.description, label: config.name, value: key, })); const compatibleValues = new Set(compatibleModules.map((module) => module.value)); const validSavedModules = savedModules.filter((module) => compatibleValues.has(module)); return await this.CLI_INTERFACE_SERVICE.multiselect("Select the CI modules you want to set up:", compatibleModules, false, validSavedModules); } /** * Prompts the user to select a CI provider. * @param savedProvider - Previously saved provider * @returns Promise resolving to the selected CI provider */ async selectProvider(savedProvider) { const providers = Object.values(ECiProvider).map((provider) => ({ description: this.getProviderDescription(provider), label: provider, value: provider, })); const initialProvider = savedProvider ?? undefined; return await this.CLI_INTERFACE_SERVICE.select("Select CI provider:", providers, initialProvider); } /** * Sets up a specific CI module. * Creates necessary directories and configuration files. * @param module - The CI module to set up * @param properties - Module-specific properties to use in configuration * @returns Promise resolving to an object indicating success or failure */ async setupModule(module, properties) { try { const config = CI_CONFIG[module]; // eslint-disable-next-line @elsikora/typescript/no-non-null-assertion const providerConfig = config.content[this.selectedProvider]; if (!providerConfig) { // eslint-disable-next-line @elsikora/typescript/restrict-template-expressions throw new Error(`Provider ${this.selectedProvider} is not supported for ${config.name}`); } const directionPath = providerConfig.filePath.split("/").slice(0, -1).join("/"); if (directionPath) { await this.FILE_SYSTEM_SERVICE.createDirectory(directionPath, { isRecursive: true, }); } const content = providerConfig.template(properties); await this.FILE_SYSTEM_SERVICE.writeFile(providerConfig.filePath, content); return { isSuccess: true, module }; } catch (error) { const formattedError = error; return { error: formattedError, isSuccess: false, module }; } } /** * Sets up all selected CI modules. * Collects module properties and creates configuration files. * @param savedProperties - Previously saved module properties * @returns Promise resolving to a record of module properties */ async setupSelectedModules(savedProperties = {}) { if (!this.selectedProvider) { throw new Error("Provider not selected"); } try { const moduleProperties = {}; for (const module of this.selectedModules) { // eslint-disable-next-line @elsikora/typescript/no-unsafe-argument const savedModuleProperties = this.extractModuleProperties(savedProperties[module]); const properties = await this.collectModuleProperties(module, savedModuleProperties); if (Object.keys(properties).length > 0) { moduleProperties[module] = properties; } } this.CLI_INTERFACE_SERVICE.startSpinner("Setting up CI configuration..."); const results = await Promise.all(this.selectedModules.map((module) => { // eslint-disable-next-line @elsikora/typescript/no-unsafe-assignment const setupProperties = moduleProperties[module] ?? {}; return this.setupModule(module, setupProperties); })); this.CLI_INTERFACE_SERVICE.stopSpinner("CI configuration completed successfully!"); const successfulSetups = results.filter((r) => r.isSuccess); const failedSetups = results.filter((r) => !r.isSuccess); this.displaySetupSummary(successfulSetups, failedSetups); return moduleProperties; } catch (error) { this.CLI_INTERFACE_SERVICE.stopSpinner(); throw error; } } } export { CiModuleService }; //# sourceMappingURL=ci-module.service.js.map