UNPKG

@elsikora/setup-wizard

Version:

Setup Wizard - CLI scaffolding utility

396 lines (393 loc) 22.2 kB
#!/usr/bin/env node import { EModule } from '../../domain/enum/module.enum.js'; import { EPackageJsonDependencyType } from '../../domain/enum/package-json-dependency-type.enum.js'; import { EPrlintGenerationProvider } from '../../domain/enum/prlint-generation-provider.enum.js'; import { EPrlintTicketMissingBranchLintBehavior } from '../../domain/enum/prlint-ticket-missing-branch-lint-behavior.enum.js'; import { EPrlintTicketNormalization } from '../../domain/enum/prlint-ticket-normalization.enum.js'; import { EPrlintTicketSource } from '../../domain/enum/prlint-ticket-source.enum.js'; import { NodeCommandService } from '../../infrastructure/service/node-command.service.js'; import { PRLINT_CONFIG, PRLINT_CONFIG_DEFAULTS } from '../constant/prlint/config.constant.js'; import { PRLINT_CONFIG_CORE_DEPENDENCIES } from '../constant/prlint/core-dependencies.constant.js'; import { PRLINT_CONFIG_FILE_NAMES } from '../constant/prlint/file-names.constant.js'; import { PRLINT_CONFIG_FILE_PATHS } from '../constant/prlint/file-paths.constant.js'; import { PRLINT_CONFIG_MESSAGES } from '../constant/prlint/messages.constant.js'; import { PRLINT_CONFIG_PACKAGE_JSON_SCRIPT_NAMES } from '../constant/prlint/package-json-script-names.constant.js'; import { PRLINT_CONFIG_SCRIPTS } from '../constant/prlint/scripts.constant.js'; import { PRLINT_CONFIG_SUMMARY } from '../constant/prlint/summary.constant.js'; import { PackageJsonService } from './package-json.service.js'; const MAX_RETRY_COUNT = 10; const MIN_RETRY_COUNT = 1; class PrlintModuleService { CLI_INTERFACE_SERVICE; COMMAND_SERVICE; CONFIG_SERVICE; FILE_SYSTEM_SERVICE; PACKAGE_JSON_SERVICE; config = null; constructor(cliInterfaceService, fileSystemService, configService) { this.CLI_INTERFACE_SERVICE = cliInterfaceService; this.FILE_SYSTEM_SERVICE = fileSystemService; this.COMMAND_SERVICE = new NodeCommandService(cliInterfaceService); this.PACKAGE_JSON_SERVICE = new PackageJsonService(fileSystemService, this.COMMAND_SERVICE); this.CONFIG_SERVICE = configService; } async handleExistingSetup() { const existingFiles = await this.findExistingConfigFiles(); const packageJson = await this.PACKAGE_JSON_SERVICE.get(); const hasPrlintScripts = PRLINT_CONFIG_PACKAGE_JSON_SCRIPT_NAMES.some((scriptName) => packageJson.scripts?.[scriptName]); if (hasPrlintScripts) { existingFiles.push("package.json scripts"); } if (existingFiles.length > 0) { const messageLines = [PRLINT_CONFIG_MESSAGES.existingFilesDetected]; messageLines.push(""); for (const file of existingFiles) { messageLines.push(`- ${file}`); } messageLines.push("", PRLINT_CONFIG_MESSAGES.deleteFilesQuestion); const shouldDelete = await this.CLI_INTERFACE_SERVICE.confirm(messageLines.join("\n"), true); if (shouldDelete) { await Promise.all(existingFiles.filter((file) => file !== "package.json scripts").map((file) => this.FILE_SYSTEM_SERVICE.deleteFile(file))); if (hasPrlintScripts && packageJson.scripts) { const scripts = { ...packageJson.scripts }; for (const scriptName of PRLINT_CONFIG_PACKAGE_JSON_SCRIPT_NAMES) { if (scriptName in scripts) { // eslint-disable-next-line @elsikora/typescript/no-dynamic-delete delete scripts[scriptName]; } } packageJson.scripts = scripts; await this.PACKAGE_JSON_SERVICE.set(packageJson); } } else { this.CLI_INTERFACE_SERVICE.warn(PRLINT_CONFIG_MESSAGES.existingFilesAborted); return false; } } return true; } async install() { try { this.config = await this.CONFIG_SERVICE.getModuleConfig(EModule.PRLINT); if (!(await this.shouldInstall())) { return { wasInstalled: false }; } if (!(await this.handleExistingSetup())) { return { wasInstalled: false }; } const isScriptsEnabled = await this.shouldEnableScripts(); const config = await this.resolvePrlintConfig(); await this.setupPrlint(config, isScriptsEnabled); return { customProperties: { ...config, isScriptsEnabled, }, wasInstalled: true, }; } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(PRLINT_CONFIG_MESSAGES.failedSetupError, error); throw error; } } async readCommaSeparatedList(prompt, errorPrompt, defaultValue) { try { const value = await this.CLI_INTERFACE_SERVICE.text(prompt, "", defaultValue.join(", ")); return this.parseCommaSeparatedList(value, defaultValue); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(errorPrompt, error); return defaultValue; } } async readGenerationModel(defaultValue) { try { const value = await this.CLI_INTERFACE_SERVICE.text(PRLINT_CONFIG_MESSAGES.modelPrompt, "", defaultValue, (model) => (model.trim().length === 0 ? "Model is required" : undefined)); return value.trim().length > 0 ? value.trim() : defaultValue; } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(PRLINT_CONFIG_MESSAGES.modelPromptError, error); return defaultValue; } } async readRetryCount(prompt, errorPrompt, defaultValue) { try { const value = await this.CLI_INTERFACE_SERVICE.text(prompt, "", String(defaultValue), (retryCount) => { const parsedValue = Number.parseInt(retryCount, 10); return Number.isNaN(parsedValue) || parsedValue < MIN_RETRY_COUNT || parsedValue > MAX_RETRY_COUNT ? `Please enter a number between ${MIN_RETRY_COUNT} and ${MAX_RETRY_COUNT}` : undefined; }); const parsedValue = Number.parseInt(value, 10); return Number.isNaN(parsedValue) ? defaultValue : parsedValue; } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(errorPrompt, error); return defaultValue; } } async readText(prompt, errorPrompt, defaultValue) { try { const value = await this.CLI_INTERFACE_SERVICE.text(prompt, "", defaultValue); return value.trim().length > 0 ? value.trim() : defaultValue; } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(errorPrompt, error); return defaultValue; } } async resolvePrlintConfig() { const defaultConfig = this.getDefaultConfig(); const provider = await this.selectGenerationProvider(defaultConfig.generation.provider); const providerDefaultModel = this.getDefaultModelByProvider(provider); const generationModel = await this.readGenerationModel(this.config?.generation?.model ?? providerDefaultModel); const retries = await this.readRetryCount(PRLINT_CONFIG_MESSAGES.retriesPrompt, PRLINT_CONFIG_MESSAGES.retriesPromptError, defaultConfig.generation.retries); const validationRetries = await this.readRetryCount(PRLINT_CONFIG_MESSAGES.validationRetriesPrompt, PRLINT_CONFIG_MESSAGES.validationRetriesPromptError, defaultConfig.generation.validationRetries); const baseBranch = await this.readText(PRLINT_CONFIG_MESSAGES.githubBaseBranchPrompt, PRLINT_CONFIG_MESSAGES.githubBaseBranchPromptError, defaultConfig.github.base); const isDraft = await this.confirm(PRLINT_CONFIG_MESSAGES.githubDraftPrompt, PRLINT_CONFIG_MESSAGES.githubDraftPromptError, defaultConfig.github.isDraft); const prohibitedBranches = await this.readCommaSeparatedList(PRLINT_CONFIG_MESSAGES.githubProhibitedBranchesPrompt, PRLINT_CONFIG_MESSAGES.githubProhibitedBranchesPromptError, defaultConfig.github.prohibitedBranches); const titlePattern = await this.readText(PRLINT_CONFIG_MESSAGES.lintTitlePatternPrompt, PRLINT_CONFIG_MESSAGES.lintTitlePatternPromptError, defaultConfig.lint.titlePattern); const requiredSections = await this.readCommaSeparatedList(PRLINT_CONFIG_MESSAGES.lintRequiredSectionsPrompt, PRLINT_CONFIG_MESSAGES.lintRequiredSectionsPromptError, defaultConfig.lint.requiredSections); const forbiddenPlaceholders = await this.readCommaSeparatedList(PRLINT_CONFIG_MESSAGES.forbiddenPlaceholdersPrompt, PRLINT_CONFIG_MESSAGES.forbiddenPlaceholdersPromptError, defaultConfig.lint.forbiddenPlaceholders); const isTicketEnabled = await this.shouldEnableTicketIntegration(defaultConfig.ticket.source); const ticketSourceDefault = defaultConfig.ticket.source === EPrlintTicketSource.NONE ? EPrlintTicketSource.BRANCH_LINT : defaultConfig.ticket.source; const source = isTicketEnabled ? await this.selectTicketSource(ticketSourceDefault) : EPrlintTicketSource.NONE; const normalization = isTicketEnabled ? await this.selectTicketNormalization(defaultConfig.ticket.normalization) : defaultConfig.ticket.normalization; let pattern = defaultConfig.ticket.pattern; let patternFlags = defaultConfig.ticket.patternFlags; let missingBranchLintBehavior = defaultConfig.ticket.missingBranchLintBehavior; if (source === EPrlintTicketSource.AUTO || source === EPrlintTicketSource.PATTERN) { pattern = await this.readText(PRLINT_CONFIG_MESSAGES.ticketPatternPrompt, PRLINT_CONFIG_MESSAGES.ticketPatternPromptError, defaultConfig.ticket.pattern); patternFlags = await this.readText(PRLINT_CONFIG_MESSAGES.ticketPatternFlagsPrompt, PRLINT_CONFIG_MESSAGES.ticketPatternFlagsPromptError, defaultConfig.ticket.patternFlags); } if (source === EPrlintTicketSource.BRANCH_LINT) { missingBranchLintBehavior = await this.selectMissingBranchLintBehavior(defaultConfig.ticket.missingBranchLintBehavior); } return { generation: { model: generationModel, provider, retries, validationRetries, }, github: { base: baseBranch, isDraft, prohibitedBranches, }, lint: { forbiddenPlaceholders, requiredSections, titlePattern, }, ticket: { missingBranchLintBehavior, normalization, pattern, patternFlags, source, }, }; } async selectGenerationProvider(defaultValue) { const options = [ { label: "anthropic", value: EPrlintGenerationProvider.ANTHROPIC }, { label: "google", value: EPrlintGenerationProvider.GOOGLE }, { label: "ollama", value: EPrlintGenerationProvider.OLLAMA }, { label: "openai", value: EPrlintGenerationProvider.OPENAI }, ]; try { return await this.CLI_INTERFACE_SERVICE.select(PRLINT_CONFIG_MESSAGES.providerPrompt, options, defaultValue); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(PRLINT_CONFIG_MESSAGES.providerPromptError, error); return defaultValue; } } async selectMissingBranchLintBehavior(defaultValue) { const options = [ { label: "error - stop setup when branch-lint config is unavailable", value: EPrlintTicketMissingBranchLintBehavior.ERROR }, { label: "fallback - use local regex pattern as fallback", value: EPrlintTicketMissingBranchLintBehavior.FALLBACK }, ]; try { return await this.CLI_INTERFACE_SERVICE.select(PRLINT_CONFIG_MESSAGES.branchLintMissingBehaviorPrompt, options, defaultValue); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(PRLINT_CONFIG_MESSAGES.branchLintMissingBehaviorPromptError, error); return defaultValue; } } async selectTicketNormalization(defaultValue) { const options = [ { label: "upper - convert ticket to upper case", value: EPrlintTicketNormalization.UPPER }, { label: "preserve - keep ticket case as detected", value: EPrlintTicketNormalization.PRESERVE }, { label: "lower - convert ticket to lower case", value: EPrlintTicketNormalization.LOWER }, ]; try { return await this.CLI_INTERFACE_SERVICE.select(PRLINT_CONFIG_MESSAGES.ticketNormalizationPrompt, options, defaultValue); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(PRLINT_CONFIG_MESSAGES.ticketNormalizationPromptError, error); return defaultValue; } } async selectTicketSource(defaultValue) { const options = [ { label: "branch-lint - parse ticket by git-branch-lint pattern", value: EPrlintTicketSource.BRANCH_LINT }, { label: "auto - branch-lint first, then local regex fallback", value: EPrlintTicketSource.AUTO }, { label: "pattern - parse ticket only by local regex pattern", value: EPrlintTicketSource.PATTERN }, ]; try { return await this.CLI_INTERFACE_SERVICE.select(PRLINT_CONFIG_MESSAGES.ticketSourcePrompt, options, defaultValue); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(PRLINT_CONFIG_MESSAGES.ticketSourcePromptError, error); return defaultValue; } } async shouldEnableScripts() { const isScriptsEnabledByDefault = this.config?.isScriptsEnabled ?? true; return await this.confirm(PRLINT_CONFIG_MESSAGES.addScriptsPrompt, PRLINT_CONFIG_MESSAGES.addScriptsPromptError, isScriptsEnabledByDefault); } async shouldEnableTicketIntegration(defaultSource) { const isTicketEnabledByDefault = defaultSource !== EPrlintTicketSource.NONE; return await this.confirm(PRLINT_CONFIG_MESSAGES.ticketEnabledPrompt, PRLINT_CONFIG_MESSAGES.ticketEnabledPromptError, isTicketEnabledByDefault); } async shouldInstall() { try { return await this.CLI_INTERFACE_SERVICE.confirm(PRLINT_CONFIG_MESSAGES.confirmSetup, await this.CONFIG_SERVICE.isModuleEnabled(EModule.PRLINT)); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(PRLINT_CONFIG_MESSAGES.failedConfirmation, error); return false; } } async confirm(prompt, errorPrompt, isEnabledByDefault) { try { return await this.CLI_INTERFACE_SERVICE.confirm(prompt, isEnabledByDefault); } catch (error) { this.CLI_INTERFACE_SERVICE.handleError(errorPrompt, error); return isEnabledByDefault; } } async createConfigs(config) { await this.FILE_SYSTEM_SERVICE.writeFile(PRLINT_CONFIG_FILE_PATHS.configFile, PRLINT_CONFIG.template(config), "utf8"); } displaySetupSummary(config, isScriptsEnabled) { const generationConfig = config.generation ?? PRLINT_CONFIG_DEFAULTS.generation; const githubConfig = config.github ?? PRLINT_CONFIG_DEFAULTS.github; const lintConfig = config.lint ?? PRLINT_CONFIG_DEFAULTS.lint; const ticketConfig = config.ticket ?? PRLINT_CONFIG_DEFAULTS.ticket; const generationDescription = PRLINT_CONFIG_SUMMARY.generationConfigurationDescription .replace("{provider}", generationConfig.provider ?? PRLINT_CONFIG_DEFAULTS.generation.provider) .replace("{model}", generationConfig.model ?? PRLINT_CONFIG_DEFAULTS.generation.model) .replace("{retries}", String(generationConfig.retries ?? PRLINT_CONFIG_DEFAULTS.generation.retries)) .replace("{validationRetries}", String(generationConfig.validationRetries ?? PRLINT_CONFIG_DEFAULTS.generation.validationRetries)); const githubDescription = PRLINT_CONFIG_SUMMARY.githubConfigurationDescription .replace("{base}", githubConfig.base ?? PRLINT_CONFIG_DEFAULTS.github.base) .replace("{draft}", String(githubConfig.isDraft ?? PRLINT_CONFIG_DEFAULTS.github.isDraft)) .replace("{prohibitedBranches}", (githubConfig.prohibitedBranches ?? PRLINT_CONFIG_DEFAULTS.github.prohibitedBranches).join(", ")); const lintDescription = PRLINT_CONFIG_SUMMARY.lintConfigurationDescription.replace("{titlePattern}", lintConfig.titlePattern ?? PRLINT_CONFIG_DEFAULTS.lint.titlePattern); const ticketDescription = PRLINT_CONFIG_SUMMARY.ticketConfigurationDescription .replace("{source}", ticketConfig.source ?? PRLINT_CONFIG_DEFAULTS.ticket.source) .replace("{normalization}", ticketConfig.normalization ?? PRLINT_CONFIG_DEFAULTS.ticket.normalization) .replace("{missingBranchLintBehavior}", ticketConfig.missingBranchLintBehavior ?? PRLINT_CONFIG_DEFAULTS.ticket.missingBranchLintBehavior); const summary = [PRLINT_CONFIG_MESSAGES.configurationCreated, "", generationDescription, githubDescription, lintDescription, ticketDescription, "", PRLINT_CONFIG_MESSAGES.configurationFilesLabel, PRLINT_CONFIG_SUMMARY.configFileDescription]; if (isScriptsEnabled) { summary.push("", PRLINT_CONFIG_MESSAGES.generatedScriptsLabel); for (const script of this.getPrlintScripts()) { summary.push(`- npm run ${script.name}`); } } this.CLI_INTERFACE_SERVICE.note(PRLINT_CONFIG_MESSAGES.setupCompleteTitle, summary.join("\n")); } async findExistingConfigFiles() { const existingFiles = []; for (const file of PRLINT_CONFIG_FILE_NAMES) { if (await this.FILE_SYSTEM_SERVICE.isPathExists(file)) { existingFiles.push(file); } } return existingFiles; } getDefaultConfig() { return { generation: { model: this.config?.generation?.model ?? PRLINT_CONFIG_DEFAULTS.generation.model, provider: this.config?.generation?.provider ?? PRLINT_CONFIG_DEFAULTS.generation.provider, retries: this.config?.generation?.retries ?? PRLINT_CONFIG_DEFAULTS.generation.retries, validationRetries: this.config?.generation?.validationRetries ?? PRLINT_CONFIG_DEFAULTS.generation.validationRetries, }, github: { base: this.config?.github?.base ?? PRLINT_CONFIG_DEFAULTS.github.base, isDraft: this.config?.github?.isDraft ?? PRLINT_CONFIG_DEFAULTS.github.isDraft, prohibitedBranches: this.config?.github?.prohibitedBranches ?? PRLINT_CONFIG_DEFAULTS.github.prohibitedBranches, }, lint: { forbiddenPlaceholders: this.config?.lint?.forbiddenPlaceholders ?? PRLINT_CONFIG_DEFAULTS.lint.forbiddenPlaceholders, requiredSections: this.config?.lint?.requiredSections ?? PRLINT_CONFIG_DEFAULTS.lint.requiredSections, titlePattern: this.config?.lint?.titlePattern ?? PRLINT_CONFIG_DEFAULTS.lint.titlePattern, }, ticket: { missingBranchLintBehavior: this.config?.ticket?.missingBranchLintBehavior ?? PRLINT_CONFIG_DEFAULTS.ticket.missingBranchLintBehavior, normalization: this.config?.ticket?.normalization ?? PRLINT_CONFIG_DEFAULTS.ticket.normalization, pattern: this.config?.ticket?.pattern ?? PRLINT_CONFIG_DEFAULTS.ticket.pattern, patternFlags: this.config?.ticket?.patternFlags ?? PRLINT_CONFIG_DEFAULTS.ticket.patternFlags, source: this.config?.ticket?.source ?? PRLINT_CONFIG_DEFAULTS.ticket.source, }, }; } getDefaultModelByProvider(provider) { switch (provider) { case EPrlintGenerationProvider.ANTHROPIC: { return "claude-opus-4-5"; } case EPrlintGenerationProvider.GOOGLE: { return "gemini-2.5-pro-preview-05-06"; } case EPrlintGenerationProvider.OLLAMA: { return "llama3"; } case EPrlintGenerationProvider.OPENAI: { return "gpt-4o-mini"; } default: { return PRLINT_CONFIG_DEFAULTS.generation.model; } } } getPrlintScripts() { return [PRLINT_CONFIG_SCRIPTS.context, PRLINT_CONFIG_SCRIPTS.create, PRLINT_CONFIG_SCRIPTS.fix, PRLINT_CONFIG_SCRIPTS.generate, PRLINT_CONFIG_SCRIPTS.lint]; } parseCommaSeparatedList(value, fallback) { const parsedValue = value .split(",") .map((item) => item.trim()) .filter((item) => item.length > 0); return parsedValue.length > 0 ? parsedValue : fallback; } async setupPrlint(config, isScriptsEnabled) { this.CLI_INTERFACE_SERVICE.startSpinner(PRLINT_CONFIG_MESSAGES.settingUpSpinner); try { await this.PACKAGE_JSON_SERVICE.installPackages(PRLINT_CONFIG_CORE_DEPENDENCIES, "latest", EPackageJsonDependencyType.DEV); await this.createConfigs(config); if (isScriptsEnabled) { await this.setupScripts(); } this.CLI_INTERFACE_SERVICE.stopSpinner(PRLINT_CONFIG_MESSAGES.configurationCompleted); this.displaySetupSummary(config, isScriptsEnabled); } catch (error) { this.CLI_INTERFACE_SERVICE.stopSpinner(PRLINT_CONFIG_MESSAGES.failedSetupConfiguration); throw error; } } async setupScripts() { for (const script of this.getPrlintScripts()) { await this.PACKAGE_JSON_SERVICE.addScript(script.name, script.command); } } } export { PrlintModuleService }; //# sourceMappingURL=prlint-module.service.js.map