@elsikora/setup-wizard
Version:
Setup Wizard - CLI scaffolding utility
396 lines (393 loc) • 22.2 kB
JavaScript
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