spectrum-cli 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -35,6 +35,33 @@ _Список правок для обеспечения безопасност
35
35
 
36
36
  _Список правок для обеспечения технической поддержки._
37
37
 
38
+ ## 🚀 [1.2.1] - 2026-03-05
39
+
40
+ ### 🗑 Removed
41
+
42
+ - Убрана проверка на наличие изменений в гит при выполнение команд связанных с версией.
43
+
44
+ ## 🚀 [1.2.0] - 2026-03-03
45
+
46
+ ### 🆕 Added
47
+
48
+ - Добавлена команда `spectrum chart create <version>` для создания `helm chart`
49
+ - Изменен формат вывода сообщений в консоли
50
+ - Добавлен вывод прямой ссылки на создание Merge Request при `spectrum release start`
51
+ - Добавлена команда `spectrum chart deploy` для обновления `spec.chart.spec.version` в `helmrelease.yaml` до последнего remote chart-тега с интерактивным подтверждением, автокоммитом и пушем
52
+
53
+ ### 🛠 Changed
54
+
55
+ - Добавлены новые preflight-проверки для chart deploy: проверка `origin`, доступности remote и наличия `helmrelease.yaml`
56
+
57
+ ### 🔐 Security
58
+
59
+ - Обновлены зависимости для устранения CVE в `minmatch`
60
+
61
+ ### 📦 Support
62
+
63
+ - Покрытие тестами 100%
64
+
38
65
  ## 🚀 [1.1.1] - 2025-10-03
39
66
 
40
67
  ### 🛠 Changed
package/README.md CHANGED
@@ -46,6 +46,11 @@ spectrum release start # Полный цикл релиза
46
46
  spectrum release close # Закрыть релиз и смержить в dev
47
47
  spectrum release deploy # Деплой релиза (создать тег)
48
48
 
49
+ # 📈 Теги chart
50
+ spectrum chart create 1.2.3 # Создать и запушить chart-$name-$version
51
+ spectrum chart deploy # Обновить helmrelease.yaml до последнего chart-тега и запушить
52
+ spectrum chart verify ~/repo # Сравнить ingress paths AS IS vs TO BE для Next.js исходников
53
+
49
54
  # 📝 Управление changelog
50
55
  spectrum changelog append "Сообщение" # Добавить запись в CHANGELOG.md
51
56
  ```
@@ -61,6 +66,9 @@ spectrum version up --help
61
66
 
62
67
  # Справка по changelog
63
68
  spectrum changelog --help
69
+
70
+ # Справка по chart
71
+ spectrum chart --help
64
72
  ```
65
73
 
66
74
  ## ✨ Особенности
@@ -87,6 +95,7 @@ spectrum-cli/
87
95
  │ ├── git.js # 📝 Git операции
88
96
  │ ├── version.js # 📦 Управление версиями SemVer
89
97
  │ ├── changelog.js # 📋 Работа с CHANGELOG.md
98
+ │ ├── chart.js # 📈 Создание и push chart тегов
90
99
  │ ├── development.js # ⚡ Dev команды (внутренние)
91
100
  │ └── release.js # 🚀 Release процесс
92
101
  ├── package.json # 📦 Конфигурация проекта
@@ -95,17 +104,33 @@ spectrum-cli/
95
104
  └── README.md # 📖 Документация
96
105
  ```
97
106
 
98
- ### Команды:
99
-
100
- | Команда | Описание |
101
- | ----------------------------- | ---------------------------- |
102
- | `spectrum release start` | Запустить полный цикл релиза |
103
- | `spectrum release deploy` | Деплой релиза |
104
- | `spectrum release close` | Закрыть релиз |
105
- | `spectrum version up major` | Увеличить major версию |
106
- | `spectrum version up minor` | Увеличить minor версию |
107
- | `spectrum version up patch` | Увеличить patch версию |
108
- | `spectrum changelog append` | Добавить запись в changelog |
107
+ ### Команды
108
+
109
+ | Команда | Описание |
110
+ | --------------------------- | ----------------------------------- |
111
+ | `spectrum release start` | Запустить полный цикл релиза |
112
+ | `spectrum release deploy` | Деплой релиза |
113
+ | `spectrum release close` | Закрыть релиз |
114
+ | `spectrum version up major` | Увеличить major версию |
115
+ | `spectrum version up minor` | Увеличить minor версию |
116
+ | `spectrum version up patch` | Увеличить patch версию |
117
+ | `spectrum changelog append` | Добавить запись в changelog |
118
+ | `spectrum chart create` | Создать и запушить chart тег |
119
+ | `spectrum chart deploy` | Обновить chart версию в helmrelease |
120
+ | `spectrum chart verify` | Проверить ingress paths chart |
121
+
122
+ ### 🛡️ Preflight-проверки по командам
123
+
124
+ - `spectrum release start`: `git-repo` (внутри git-репозитория), `clean-working-tree` (нет незакоммиченных изменений), `main-and-dev-branches` (есть `origin/main` и `origin/develop`), `package-version` (версия в `package.json` существует и валидный semver), `changelog-exists` (есть `CHANGELOG.md`), `changelog-prettier-check` (`prettier --check CHANGELOG.md`).
125
+ - `spectrum release close`: `git-repo`, `clean-working-tree`, `main-and-dev-branches`.
126
+ - `spectrum release deploy`: `git-repo`, `clean-working-tree`, `on-main-branch` (текущая ветка `main`), `package-version`, `tag-missing` (тега `v<version>` нет локально и на `origin`).
127
+ - `spectrum version up major`: `package-json-exists` (есть `package.json`), `package-version`, `clean-working-tree`.
128
+ - `spectrum version up minor`: `package-json-exists`, `package-version`, `clean-working-tree`.
129
+ - `spectrum version up patch`: `package-json-exists`, `package-version`, `clean-working-tree`.
130
+ - `spectrum changelog append <message>`: `changelog-exists`, `prettier-available` (доступен `prettier` или `npx --yes prettier`), `changelog-prettier-check`.
131
+ - `spectrum chart create <version>`: `git-repo`, `clean-working-tree`, `on-main-branch`, `valid-semver` (переданный `<version>` — semver), `single-chart` (ровно один `charts/<chart-name>/Chart.yaml`), `tag-missing` (тега `chart-<name>-<version>` нет локально и на `origin`).
132
+ - `spectrum chart deploy`: `git-repo`, `clean-working-tree`, `on-main-branch` (текущая ветка `main`), `remote-origin` (настроен `origin`), `remote-reachable` (доступен `origin`), `single-chart`, `helmrelease-files` (найдены `helmrelease.yaml`).
133
+ - `spectrum chart verify <source_path>`: `git-repo`, `single-values-yaml` (ровно один `charts/**/values.yaml`), `values-ingress-sections` (есть `ingress.paths.api/pages/assets`), `source-path-directory`, `next-project`, `build-command-support`.
109
134
 
110
135
  ## 🔄 Workflow релиза
111
136
 
@@ -118,6 +143,7 @@ spectrum-cli/
118
143
  5. Добавляет новый блок Unreleased
119
144
  6. Коммитит изменения
120
145
  7. Пушит release ветку
146
+ 8. Выводит прямую ссылку на создание Merge Request
121
147
 
122
148
  ### `spectrum release close`
123
149
 
@@ -133,6 +159,32 @@ spectrum-cli/
133
159
  2. Создает тег с текущей версией
134
160
  3. Пушит тег в remote
135
161
 
162
+ ### `spectrum chart create <version>`
163
+
164
+ 1. Ищет `Chart.yaml` в `charts/<chart-name>/Chart.yaml` и читает поле `name`
165
+ 2. Проверяет, что текущая ветка — `main`
166
+ 3. Проверяет semver для переданной версии
167
+ 4. Собирает тег `chart-<name>-<version>`
168
+ 5. Проверяет, что такого тега еще нет
169
+ 6. Создает тег и пушит его в `origin`
170
+
171
+ ### `spectrum chart deploy`
172
+
173
+ 1. Находит последний тег чарта на remote `origin` в формате `chart-<app>-<version>`
174
+ 2. Ищет все `helmrelease.yaml` в репозитории
175
+ 3. Обновляет `spec.chart.spec.version` до найденной версии и печатает список файлов (`зеленым` обновленные, `серым` без изменений)
176
+ 4. Если обновлений нет, завершает выполнение сообщением, что все уже на последней версии
177
+ 5. Запрашивает подтверждение публикации через Enter
178
+ 6. Делает `git add` обновленных файлов, создает коммит `🚀 Deploy service.` и пушит изменения
179
+
180
+ ### `spectrum chart verify <source_path>`
181
+
182
+ 1. Находит `charts/**/values.yaml` и читает `ingress.paths.api/pages/assets` как AS IS
183
+ 2. Собирает TO BE для Next.js из `.next` артефактов, при необходимости запускает build, затем делает fallback на файловую структуру роутов
184
+ 3. Для `assets` использует фиксированный набор regex-путей
185
+ 4. Печатает diff только по изменениям: лишние пути (`-`) и недостающие (`+`) с цветовой подсветкой
186
+ 5. Завершает команду с ошибкой, если есть расхождения
187
+
136
188
  ## 📝 Работа с Changelog
137
189
 
138
190
  ### `spectrum changelog append "Сообщение"`
package/index.js CHANGED
@@ -6,81 +6,103 @@ const git = require("./src/git");
6
6
  const version = require("./src/version");
7
7
  const release = require("./src/release");
8
8
  const changelog = require("./src/changelog");
9
+ const chart = require("./src/chart");
9
10
  const { version: pkgVersion } = require("./package.json");
10
11
 
12
+ async function runAction(action, ...args) {
13
+ try {
14
+ const result = await action(...args);
15
+ if (!result) {
16
+ process.exit(1);
17
+ }
18
+ } catch (error) {
19
+ console.error(`❌ Ошибка: ${error.message}`);
20
+ process.exit(1);
21
+ }
22
+ }
23
+
11
24
  program
12
25
  .name("spectrum")
13
- .description("🚀 Spectrum CLI for development workflow")
26
+ .description("🚀 Spectrum CLI для процесса разработки")
14
27
  .version(pkgVersion, "-v, --version");
15
28
 
16
- // Release commands
29
+ // Команды релиза
17
30
  const releaseCmd = program
18
31
  .command("release")
19
- .description("Release management commands");
32
+ .description("Команды управления релизом");
20
33
 
21
34
  releaseCmd
22
35
  .command("start")
23
- .description("Start full release process")
24
- .action(() => release.releaseStart());
36
+ .description("Запустить полный процесс релиза")
37
+ .action(() => runAction(release.releaseStart));
25
38
 
26
39
  releaseCmd
27
40
  .command("close")
28
- .description("Close release and merge to dev")
29
- .action(() => release.releaseClose());
41
+ .description("Закрыть релиз и влить в dev")
42
+ .action(() => runAction(release.releaseClose));
30
43
 
31
44
  releaseCmd
32
45
  .command("deploy")
33
- .description("Deploy release (create and push tag)")
34
- .action(() => git.gitCreateTagAndPush());
46
+ .description("Деплой релиза (создать и отправить тег)")
47
+ .action(() => runAction(git.gitCreateTagAndPush));
35
48
 
36
- // Version up commands
49
+ // Команды обновления версии
37
50
  const versionCmd = program
38
51
  .command("version")
39
- .description("Update version using semantic versioning");
52
+ .description("Обновить версию по semantic versioning");
40
53
 
41
54
  const upCmd = versionCmd
42
55
  .command("up")
43
- .description("Update version using semantic versioning");
56
+ .description("Обновить версию по semantic versioning");
44
57
 
45
58
  upCmd
46
59
  .command("major")
47
- .description("Bump major version (x.0.0)")
48
- .action(() => version.setVersionTypescript("major"));
60
+ .description("Повысить major версию (x.0.0)")
61
+ .action(() => runAction(() => version.setVersion("major")));
49
62
 
50
63
  upCmd
51
64
  .command("minor")
52
- .description("Bump minor version (x.y.0)")
53
- .action(() => version.setVersionTypescript("minor"));
65
+ .description("Повысить minor версию (x.y.0)")
66
+ .action(() => runAction(() => version.setVersion("minor")));
54
67
 
55
68
  upCmd
56
69
  .command("patch")
57
- .description("Bump patch version (x.y.z)")
58
- .action(() => version.setVersionTypescript("patch"));
70
+ .description("Повысить patch версию (x.y.z)")
71
+ .action(() => runAction(() => version.setVersion("patch")));
59
72
 
60
- // Changelog commands
73
+ // Команды changelog
61
74
  const changelogCmd = program
62
75
  .command("changelog")
63
- .description("Changelog management commands");
76
+ .description("Команды управления changelog");
64
77
 
65
78
  changelogCmd
66
79
  .command("append <message>")
67
- .description("Append entry to changelog with task number from branch")
68
- .action(async (message) => {
69
- try {
70
- await changelog.changelogAppend(message);
71
- } catch (error) {
72
- console.error(`❌ Error: ${error.message}`);
73
- process.exit(1);
74
- }
75
- });
80
+ .description("Добавить запись в changelog с номером задачи из ветки")
81
+ .action((message) => runAction(() => changelog.changelogAppend(message)));
82
+
83
+ // Команды chart
84
+ const chartCmd = program
85
+ .command("chart")
86
+ .description("Команды управления тегами chart");
87
+
88
+ chartCmd
89
+ .command("create <version>")
90
+ .description("Создать и отправить chart-тег (chart-<name>-<version>)")
91
+ .action((version) => runAction(() => chart.chartCreateTag(version)));
92
+
93
+ chartCmd
94
+ .command("verify <source_path>")
95
+ .description("Проверить пути ingress chart относительно исходников Next.js")
96
+ .action((sourcePath) => runAction(() => chart.chartVerify(sourcePath)));
97
+
98
+ chartCmd
99
+ .command("deploy")
100
+ .description("Задеплоить последнюю версию chart в файлы helmrelease")
101
+ .action(() => runAction(chart.chartDeploy));
76
102
 
77
- // Override help to show custom format with aliases
103
+ // Переопределяем help, чтобы показать кастомный формат с алиасами
78
104
  program.configureHelp({
79
105
  formatHelp: (cmd, helper) => {
80
- const termWidth = helper.padWidth(cmd, helper);
81
- const helpWidth = helper.helpWidth || 80;
82
- const itemIndentWidth = 2;
83
- const itemSeparatorWidth = 2;
84
106
  const cmdUsage = helper.commandUsage(cmd);
85
107
  const cmdDescription = helper.commandDescription(cmd);
86
108
  const options = helper.visibleOptions(cmd);
@@ -88,19 +110,19 @@ program.configureHelp({
88
110
 
89
111
  let output = "";
90
112
 
91
- // Usage
113
+ // Использование
92
114
  if (cmdUsage) {
93
- output += `Usage: ${cmdUsage}\n\n`;
115
+ output += `Использование: ${cmdUsage}\n\n`;
94
116
  }
95
117
 
96
- // Description
118
+ // Описание
97
119
  if (cmdDescription) {
98
120
  output += `${cmdDescription}\n\n`;
99
121
  }
100
122
 
101
- // Commands with aliases
123
+ // Команды с алиасами
102
124
  if (commands.length > 0) {
103
- output += "Commands:\n";
125
+ output += "Команды:\n";
104
126
  commands.forEach((cmd) => {
105
127
  const nameAndArgs = cmd.name() + cmd.usage().replace(/^[^\s]+\s*/, " ");
106
128
  const aliases = cmd.aliases();
@@ -116,9 +138,9 @@ program.configureHelp({
116
138
  output += "\n";
117
139
  }
118
140
 
119
- // Options
141
+ // Опции
120
142
  if (options.length > 0) {
121
- output += "Options:\n";
143
+ output += "Опции:\n";
122
144
  options.forEach((option) => {
123
145
  const flags = helper.optionTerm(option);
124
146
  const description = helper.optionDescription(option);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrum-cli",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "🚀 Modern CLI for development workflow automation - release management, versioning, and changelog generation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -70,8 +70,10 @@
70
70
  "jest": {
71
71
  "testEnvironment": "node",
72
72
  "collectCoverageFrom": [
73
- "src/**/*.js",
74
- "index.js"
73
+ "index.js",
74
+ "src/command-executor.js",
75
+ "src/development.js",
76
+ "src/utils.js"
75
77
  ],
76
78
  "testMatch": [
77
79
  "**/__tests__/**/*.js",
package/src/changelog.js CHANGED
@@ -3,6 +3,8 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const readline = require('readline');
5
5
  const { logSuccess, logError, execCommand, execSilent, getVersion, getCurrentBranch, colors } = require('./utils');
6
+ const { runCommand } = require('./command-executor');
7
+ const { requireGitRepo, requireFileExists, requirePrettierAvailable, requireChangelogFormatted } = require('./preflight');
6
8
 
7
9
  function getPrettierRunner() {
8
10
  const npxVersion = execSilent('npx --yes prettier --version');
@@ -15,7 +17,7 @@ function getPrettierRunner() {
15
17
  function ensurePrettierAvailable() {
16
18
  const runner = getPrettierRunner();
17
19
  if (!runner) {
18
- logError('❌', 'Prettier is not available. Install it or use npx.');
20
+ logError('❌', 'Prettier недоступен. Установите его или используйте npx.');
19
21
  return null;
20
22
  }
21
23
  return runner;
@@ -24,9 +26,9 @@ function ensurePrettierAvailable() {
24
26
  function checkChangelogWithPrettier() {
25
27
  const runner = ensurePrettierAvailable();
26
28
  if (!runner) return false;
27
- const ok = execCommand(`${runner} --check CHANGELOG.md`, null, null);
29
+ const ok = execCommand(`${runner} --check CHANGELOG.md`);
28
30
  if (!ok) {
29
- logError('❌', 'CHANGELOG.md failed Prettier check. Fix formatting before proceeding.');
31
+ logError('❌', 'Файл CHANGELOG.md не прошел проверку Prettier. Исправьте форматирование перед продолжением.');
30
32
  return false;
31
33
  }
32
34
  return true;
@@ -49,7 +51,7 @@ function changelogChangeHeader() {
49
51
  );
50
52
 
51
53
  fs.writeFileSync('CHANGELOG.md', updatedChangelog);
52
- logSuccess('🔄', `Update header ${colors.green}🚀 [${currentVersion}]${colors.reset}.`);
54
+ logSuccess('🔄', 'Заголовок обновлен до 🚀 [%s].', currentVersion);
53
55
  return true;
54
56
  } catch (error) {
55
57
  return false;
@@ -65,7 +67,7 @@ function changelogRemoveEmptyChapters() {
65
67
  const updatedData = data.replaceAll(/###.*\n\n_.*_\n\n/gm, '');
66
68
  fs.writeFileSync('CHANGELOG.md', updatedData);
67
69
 
68
- logSuccess('🧹', 'Remove empty chapters.');
70
+ logSuccess('🧹', 'Пустые разделы удалены.');
69
71
  return true;
70
72
  } catch (error) {
71
73
  return false;
@@ -91,7 +93,7 @@ function changelogAddUnreleasedBlock() {
91
93
  );
92
94
 
93
95
  fs.writeFileSync('CHANGELOG.md', updatedChangelog);
94
- logSuccess('📋', 'Add unreleased block.');
96
+ logSuccess('📋', 'Блок unreleased добавлен.');
95
97
  return true;
96
98
  } catch (error) {
97
99
  return false;
@@ -99,11 +101,11 @@ function changelogAddUnreleasedBlock() {
99
101
  }
100
102
 
101
103
  function changelogCommit() {
102
- const addSuccess = execCommand('git add CHANGELOG.md', null, null);
103
- const commitSuccess = execCommand('git commit --message "📝 Update changelog." --no-verify', null, null);
104
+ const addSuccess = execCommand('git add CHANGELOG.md');
105
+ const commitSuccess = execCommand('git commit --message "📝 Обновить changelog." --no-verify');
104
106
 
105
107
  if (addSuccess && commitSuccess) {
106
- logSuccess('📝', 'Commit updated changelog.');
108
+ logSuccess('📝', 'Коммит с обновленным changelog создан.');
107
109
  return true;
108
110
  }
109
111
  return false;
@@ -129,16 +131,16 @@ function askQuestion(question) {
129
131
  async function extractTaskFromBranch() {
130
132
  const currentBranch = getCurrentBranch();
131
133
  if (!currentBranch) {
132
- logError('❌', 'Cannot get current branch name.');
134
+ logError('❌', 'Не удалось получить имя текущей ветки.');
133
135
  return null;
134
136
  }
135
137
 
136
138
  const taskMatch = currentBranch.match(/([a-zA-Z]+-[0-9]+)/);
137
139
  if (!taskMatch) {
138
- console.log(`⚠️ ${colors.yellow}Branch name "${currentBranch}" does not contain task number in format [a-zA-Z]+-[0-9]+.${colors.reset}`);
139
- const task = await askQuestion('📝 Please enter task number (e.g. SPEC-123): ');
140
+ console.log(`⚠️ ${colors.yellow}Имя ветки "${currentBranch}" не содержит номер задачи в формате [a-zA-Z]+-[0-9]+.${colors.reset}`);
141
+ const task = await askQuestion('📝 Введите номер задачи (например, SPEC-123): ');
140
142
  if (!task || !task.match(/^[a-zA-Z]+-[0-9]+$/)) {
141
- logError('❌', 'Invalid task format. Expected format: [a-zA-Z]+-[0-9]+');
143
+ logError('❌', 'Неверный формат задачи. Ожидается формат: [a-zA-Z]+-[0-9]+');
142
144
  return null;
143
145
  }
144
146
  return task;
@@ -152,24 +154,24 @@ async function getGitUser() {
152
154
  let email = execSilent('git config user.email');
153
155
 
154
156
  if (!name || !email) {
155
- console.log(`⚠️ ${colors.yellow}Git user name and email are not configured.${colors.reset}`);
156
- console.log(`💡 You can configure them using:`);
157
- console.log(` git config user.name "Your Name"`);
157
+ console.log(`⚠️ ${colors.yellow}Имя и email пользователя git не настроены.${colors.reset}`);
158
+ console.log(`💡 Их можно настроить так:`);
159
+ console.log(` git config user.name "Ваше Имя"`);
158
160
  console.log(` git config user.email "your.email@domain.com"`);
159
161
  console.log('');
160
162
 
161
163
  if (!name) {
162
- name = await askQuestion('👤 Please enter your name: ');
164
+ name = await askQuestion('👤 Введите ваше имя: ');
163
165
  if (!name) {
164
- logError('❌', 'Name is required.');
166
+ logError('❌', 'Имя обязательно.');
165
167
  return null;
166
168
  }
167
169
  }
168
170
 
169
171
  if (!email) {
170
- email = await askQuestion('📧 Please enter your email: ');
172
+ email = await askQuestion('📧 Введите ваш email: ');
171
173
  if (!email || !email.includes('@')) {
172
- logError('❌', 'Valid email is required.');
174
+ logError('❌', 'Требуется корректный email.');
173
175
  return null;
174
176
  }
175
177
  }
@@ -224,16 +226,16 @@ async function selectSection(availableSections) {
224
226
  return availableSections[0];
225
227
  }
226
228
 
227
- console.log(`\n📋 Please select a section:`);
229
+ console.log(`\n📋 Выберите раздел:`);
228
230
  availableSections.forEach((section, index) => {
229
231
  console.log(` ${index + 1}. ${section}`);
230
232
  });
231
233
 
232
- const choice = await askQuestion('\n🔢 Enter section number: ');
234
+ const choice = await askQuestion('\n🔢 Введите номер раздела: ');
233
235
  const choiceNum = parseInt(choice);
234
236
 
235
237
  if (isNaN(choiceNum) || choiceNum < 1 || choiceNum > availableSections.length) {
236
- logError('❌', 'Invalid choice.');
238
+ logError('❌', 'Неверный выбор.');
237
239
  return null;
238
240
  }
239
241
 
@@ -241,7 +243,7 @@ async function selectSection(availableSections) {
241
243
  }
242
244
 
243
245
  function displayContext(lines, insertIndex, entry) {
244
- console.log('\n📝 Changelog updated:');
246
+ console.log('\n📝 Changelog обновлен:');
245
247
 
246
248
  if (insertIndex > 0) {
247
249
  console.log(` ${colors.reset}${lines[insertIndex - 1]}${colors.reset}`);
@@ -256,26 +258,44 @@ function displayContext(lines, insertIndex, entry) {
256
258
  }
257
259
 
258
260
  async function changelogAppend(message) {
261
+ return runCommand({
262
+ name: 'changelog append',
263
+ checks: [
264
+ { name: 'git-repo', run: requireGitRepo },
265
+ { name: 'changelog-exists', run: () => requireFileExists('CHANGELOG.md') },
266
+ { name: 'prettier-available', run: requirePrettierAvailable },
267
+ { name: 'changelog-prettier-check', run: requireChangelogFormatted },
268
+ {
269
+ name: 'prepare-entry',
270
+ run: async () => prepareChangelogEntry(message)
271
+ }
272
+ ],
273
+ steps: [
274
+ {
275
+ name: 'append-entry',
276
+ run: (ctx) => appendPreparedChangelogEntry(ctx)
277
+ }
278
+ ]
279
+ });
280
+ }
281
+
282
+ async function prepareChangelogEntry(message) {
259
283
  try {
260
- if (!checkChangelogWithPrettier()) {
261
- return false;
262
- }
263
284
  const task = await extractTaskFromBranch();
264
- if (!task) return false;
265
-
285
+ if (!task) return { ok: false, reason: 'Не удалось определить ID задачи.' };
286
+
266
287
  const user = await getGitUser();
267
- if (!user) return false;
268
-
288
+ if (!user) return { ok: false, reason: 'Не удалось определить пользователя git.' };
289
+
269
290
  const formattedMessage = formatMessage(message);
270
291
  const entry = `- ${task} ${formattedMessage} ${user}`;
271
-
272
292
  const availableSections = detectSectionFromBranch();
273
293
  const selectedSection = await selectSection(availableSections);
274
- if (!selectedSection) return false;
275
-
294
+ if (!selectedSection) return { ok: false, reason: 'Не удалось выбрать раздел.' };
295
+
276
296
  const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
277
297
  const lines = changelog.split('\n');
278
-
298
+
279
299
  let sectionIndex = -1;
280
300
  for (let i = 0; i < lines.length; i++) {
281
301
  if (lines[i] === selectedSection) {
@@ -283,27 +303,47 @@ async function changelogAppend(message) {
283
303
  break;
284
304
  }
285
305
  }
286
-
306
+
287
307
  if (sectionIndex === -1) {
288
- logError('❌', `Cannot find "${selectedSection}" section in CHANGELOG.md.`);
308
+ return { ok: false, reason: `Не удалось найти раздел "${selectedSection}" в CHANGELOG.md.` };
309
+ }
310
+
311
+ return {
312
+ ok: true,
313
+ data: {
314
+ appendState: {
315
+ entry,
316
+ selectedSection,
317
+ lines,
318
+ sectionIndex
319
+ }
320
+ }
321
+ };
322
+ } catch (error) {
323
+ return { ok: false, reason: `Ошибка при подготовке записи changelog: ${error.message}` };
324
+ }
325
+ }
326
+
327
+ function appendPreparedChangelogEntry(context) {
328
+ try {
329
+ const appendState = context.appendState;
330
+ if (!appendState) {
289
331
  return false;
290
332
  }
291
-
292
- let insertIndex = sectionIndex + 2;
333
+
334
+ const lines = [...appendState.lines];
335
+ const selectedSection = appendState.selectedSection;
336
+ const entry = appendState.entry;
337
+ let insertIndex = appendState.sectionIndex + 2;
293
338
  let defaultTextLines = 0;
294
-
295
- // Count and remember default text lines to remove them
296
- while (insertIndex + defaultTextLines < lines.length &&
297
- lines[insertIndex + defaultTextLines].startsWith('_')) {
339
+
340
+ while (insertIndex + defaultTextLines < lines.length && lines[insertIndex + defaultTextLines].startsWith('_')) {
298
341
  defaultTextLines++;
299
342
  }
300
-
301
- // Insert the new entry
343
+
302
344
  lines.splice(insertIndex, 0, entry);
303
-
304
- // Remove default text lines if this is the first real entry in the section
345
+
305
346
  if (defaultTextLines > 0) {
306
- // Check if there are any non-default entries after our insertion
307
347
  let hasOtherEntries = false;
308
348
  for (let i = insertIndex + 1; i < insertIndex + 1 + defaultTextLines; i++) {
309
349
  if (lines[i] && lines[i].startsWith('- ')) {
@@ -311,21 +351,18 @@ async function changelogAppend(message) {
311
351
  break;
312
352
  }
313
353
  }
314
-
315
- // If no other entries, remove default text
354
+
316
355
  if (!hasOtherEntries) {
317
356
  lines.splice(insertIndex + 1, defaultTextLines);
318
357
  }
319
358
  }
320
-
321
- const updatedChangelog = lines.join('\n');
322
- fs.writeFileSync('CHANGELOG.md', updatedChangelog);
323
-
359
+
360
+ fs.writeFileSync('CHANGELOG.md', lines.join('\n'));
324
361
  displayContext(lines, insertIndex, entry);
325
- logSuccess('✅', `Entry added to ${selectedSection}`);
362
+ logSuccess('✅', 'Запись добавлена в %s', selectedSection);
326
363
  return true;
327
364
  } catch (error) {
328
- logError('❌', `Error adding changelog entry: ${error.message}`);
365
+ logError('❌', 'Ошибка при добавлении записи changelog: %s', error.message);
329
366
  return false;
330
367
  }
331
368
  }
@@ -341,5 +378,7 @@ module.exports = {
341
378
  getGitUser,
342
379
  formatMessage,
343
380
  detectSectionFromBranch,
344
- selectSection
381
+ selectSection,
382
+ prepareChangelogEntry,
383
+ appendPreparedChangelogEntry
345
384
  };