spectrum-cli 1.1.0 → 1.2.0

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/src/changelog.js CHANGED
@@ -3,9 +3,42 @@ 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');
8
+
9
+ function getPrettierRunner() {
10
+ const npxVersion = execSilent('npx --yes prettier --version');
11
+ if (npxVersion) return 'npx --yes prettier';
12
+ const directVersion = execSilent('prettier --version');
13
+ if (directVersion) return 'prettier';
14
+ return null;
15
+ }
16
+
17
+ function ensurePrettierAvailable() {
18
+ const runner = getPrettierRunner();
19
+ if (!runner) {
20
+ logError('❌', 'Prettier недоступен. Установите его или используйте npx.');
21
+ return null;
22
+ }
23
+ return runner;
24
+ }
25
+
26
+ function checkChangelogWithPrettier() {
27
+ const runner = ensurePrettierAvailable();
28
+ if (!runner) return false;
29
+ const ok = execCommand(`${runner} --check CHANGELOG.md`);
30
+ if (!ok) {
31
+ logError('❌', 'Файл CHANGELOG.md не прошел проверку Prettier. Исправьте форматирование перед продолжением.');
32
+ return false;
33
+ }
34
+ return true;
35
+ }
6
36
 
7
37
  function changelogChangeHeader() {
8
38
  try {
39
+ if (!checkChangelogWithPrettier()) {
40
+ return false;
41
+ }
9
42
  const currentVersion = getVersion();
10
43
  if (!currentVersion) {
11
44
  return false;
@@ -18,7 +51,7 @@ function changelogChangeHeader() {
18
51
  );
19
52
 
20
53
  fs.writeFileSync('CHANGELOG.md', updatedChangelog);
21
- logSuccess('🔄', `Update header ${colors.green}🚀 [${currentVersion}]${colors.reset}.`);
54
+ logSuccess('🔄', 'Заголовок обновлен до 🚀 [%s].', currentVersion);
22
55
  return true;
23
56
  } catch (error) {
24
57
  return false;
@@ -27,11 +60,14 @@ function changelogChangeHeader() {
27
60
 
28
61
  function changelogRemoveEmptyChapters() {
29
62
  try {
63
+ if (!checkChangelogWithPrettier()) {
64
+ return false;
65
+ }
30
66
  const data = fs.readFileSync('CHANGELOG.md', 'utf8');
31
67
  const updatedData = data.replaceAll(/###.*\n\n_.*_\n\n/gm, '');
32
68
  fs.writeFileSync('CHANGELOG.md', updatedData);
33
69
 
34
- logSuccess('🧹', 'Remove empty chapters.');
70
+ logSuccess('🧹', 'Пустые разделы удалены.');
35
71
  return true;
36
72
  } catch (error) {
37
73
  return false;
@@ -40,6 +76,9 @@ function changelogRemoveEmptyChapters() {
40
76
 
41
77
  function changelogAddUnreleasedBlock() {
42
78
  try {
79
+ if (!checkChangelogWithPrettier()) {
80
+ return false;
81
+ }
43
82
  const currentVersion = getVersion();
44
83
  if (!currentVersion) {
45
84
  return false;
@@ -54,7 +93,7 @@ function changelogAddUnreleasedBlock() {
54
93
  );
55
94
 
56
95
  fs.writeFileSync('CHANGELOG.md', updatedChangelog);
57
- logSuccess('📋', 'Add unreleased block.');
96
+ logSuccess('📋', 'Блок unreleased добавлен.');
58
97
  return true;
59
98
  } catch (error) {
60
99
  return false;
@@ -62,11 +101,11 @@ function changelogAddUnreleasedBlock() {
62
101
  }
63
102
 
64
103
  function changelogCommit() {
65
- const addSuccess = execCommand('git add CHANGELOG.md', null, null);
66
- 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');
67
106
 
68
107
  if (addSuccess && commitSuccess) {
69
- logSuccess('📝', 'Commit updated changelog.');
108
+ logSuccess('📝', 'Коммит с обновленным changelog создан.');
70
109
  return true;
71
110
  }
72
111
  return false;
@@ -92,16 +131,16 @@ function askQuestion(question) {
92
131
  async function extractTaskFromBranch() {
93
132
  const currentBranch = getCurrentBranch();
94
133
  if (!currentBranch) {
95
- logError('❌', 'Cannot get current branch name.');
134
+ logError('❌', 'Не удалось получить имя текущей ветки.');
96
135
  return null;
97
136
  }
98
137
 
99
138
  const taskMatch = currentBranch.match(/([a-zA-Z]+-[0-9]+)/);
100
139
  if (!taskMatch) {
101
- console.log(`⚠️ ${colors.yellow}Branch name "${currentBranch}" does not contain task number in format [a-zA-Z]+-[0-9]+.${colors.reset}`);
102
- 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): ');
103
142
  if (!task || !task.match(/^[a-zA-Z]+-[0-9]+$/)) {
104
- logError('❌', 'Invalid task format. Expected format: [a-zA-Z]+-[0-9]+');
143
+ logError('❌', 'Неверный формат задачи. Ожидается формат: [a-zA-Z]+-[0-9]+');
105
144
  return null;
106
145
  }
107
146
  return task;
@@ -115,24 +154,24 @@ async function getGitUser() {
115
154
  let email = execSilent('git config user.email');
116
155
 
117
156
  if (!name || !email) {
118
- console.log(`⚠️ ${colors.yellow}Git user name and email are not configured.${colors.reset}`);
119
- console.log(`💡 You can configure them using:`);
120
- 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 "Ваше Имя"`);
121
160
  console.log(` git config user.email "your.email@domain.com"`);
122
161
  console.log('');
123
162
 
124
163
  if (!name) {
125
- name = await askQuestion('👤 Please enter your name: ');
164
+ name = await askQuestion('👤 Введите ваше имя: ');
126
165
  if (!name) {
127
- logError('❌', 'Name is required.');
166
+ logError('❌', 'Имя обязательно.');
128
167
  return null;
129
168
  }
130
169
  }
131
170
 
132
171
  if (!email) {
133
- email = await askQuestion('📧 Please enter your email: ');
172
+ email = await askQuestion('📧 Введите ваш email: ');
134
173
  if (!email || !email.includes('@')) {
135
- logError('❌', 'Valid email is required.');
174
+ logError('❌', 'Требуется корректный email.');
136
175
  return null;
137
176
  }
138
177
  }
@@ -187,16 +226,16 @@ async function selectSection(availableSections) {
187
226
  return availableSections[0];
188
227
  }
189
228
 
190
- console.log(`\n📋 Please select a section:`);
229
+ console.log(`\n📋 Выберите раздел:`);
191
230
  availableSections.forEach((section, index) => {
192
231
  console.log(` ${index + 1}. ${section}`);
193
232
  });
194
233
 
195
- const choice = await askQuestion('\n🔢 Enter section number: ');
234
+ const choice = await askQuestion('\n🔢 Введите номер раздела: ');
196
235
  const choiceNum = parseInt(choice);
197
236
 
198
237
  if (isNaN(choiceNum) || choiceNum < 1 || choiceNum > availableSections.length) {
199
- logError('❌', 'Invalid choice.');
238
+ logError('❌', 'Неверный выбор.');
200
239
  return null;
201
240
  }
202
241
 
@@ -204,7 +243,7 @@ async function selectSection(availableSections) {
204
243
  }
205
244
 
206
245
  function displayContext(lines, insertIndex, entry) {
207
- console.log('\n📝 Changelog updated:');
246
+ console.log('\n📝 Changelog обновлен:');
208
247
 
209
248
  if (insertIndex > 0) {
210
249
  console.log(` ${colors.reset}${lines[insertIndex - 1]}${colors.reset}`);
@@ -219,23 +258,44 @@ function displayContext(lines, insertIndex, entry) {
219
258
  }
220
259
 
221
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) {
222
283
  try {
223
284
  const task = await extractTaskFromBranch();
224
- if (!task) return false;
225
-
285
+ if (!task) return { ok: false, reason: 'Не удалось определить ID задачи.' };
286
+
226
287
  const user = await getGitUser();
227
- if (!user) return false;
228
-
288
+ if (!user) return { ok: false, reason: 'Не удалось определить пользователя git.' };
289
+
229
290
  const formattedMessage = formatMessage(message);
230
291
  const entry = `- ${task} ${formattedMessage} ${user}`;
231
-
232
292
  const availableSections = detectSectionFromBranch();
233
293
  const selectedSection = await selectSection(availableSections);
234
- if (!selectedSection) return false;
235
-
294
+ if (!selectedSection) return { ok: false, reason: 'Не удалось выбрать раздел.' };
295
+
236
296
  const changelog = fs.readFileSync('CHANGELOG.md', 'utf8');
237
297
  const lines = changelog.split('\n');
238
-
298
+
239
299
  let sectionIndex = -1;
240
300
  for (let i = 0; i < lines.length; i++) {
241
301
  if (lines[i] === selectedSection) {
@@ -243,27 +303,47 @@ async function changelogAppend(message) {
243
303
  break;
244
304
  }
245
305
  }
246
-
306
+
247
307
  if (sectionIndex === -1) {
248
- 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) {
249
331
  return false;
250
332
  }
251
-
252
- 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;
253
338
  let defaultTextLines = 0;
254
-
255
- // Count and remember default text lines to remove them
256
- while (insertIndex + defaultTextLines < lines.length &&
257
- lines[insertIndex + defaultTextLines].startsWith('_')) {
339
+
340
+ while (insertIndex + defaultTextLines < lines.length && lines[insertIndex + defaultTextLines].startsWith('_')) {
258
341
  defaultTextLines++;
259
342
  }
260
-
261
- // Insert the new entry
343
+
262
344
  lines.splice(insertIndex, 0, entry);
263
-
264
- // Remove default text lines if this is the first real entry in the section
345
+
265
346
  if (defaultTextLines > 0) {
266
- // Check if there are any non-default entries after our insertion
267
347
  let hasOtherEntries = false;
268
348
  for (let i = insertIndex + 1; i < insertIndex + 1 + defaultTextLines; i++) {
269
349
  if (lines[i] && lines[i].startsWith('- ')) {
@@ -271,21 +351,18 @@ async function changelogAppend(message) {
271
351
  break;
272
352
  }
273
353
  }
274
-
275
- // If no other entries, remove default text
354
+
276
355
  if (!hasOtherEntries) {
277
356
  lines.splice(insertIndex + 1, defaultTextLines);
278
357
  }
279
358
  }
280
-
281
- const updatedChangelog = lines.join('\n');
282
- fs.writeFileSync('CHANGELOG.md', updatedChangelog);
283
-
359
+
360
+ fs.writeFileSync('CHANGELOG.md', lines.join('\n'));
284
361
  displayContext(lines, insertIndex, entry);
285
- logSuccess('✅', `Entry added to ${selectedSection}`);
362
+ logSuccess('✅', 'Запись добавлена в %s', selectedSection);
286
363
  return true;
287
364
  } catch (error) {
288
- logError('❌', `Error adding changelog entry: ${error.message}`);
365
+ logError('❌', 'Ошибка при добавлении записи changelog: %s', error.message);
289
366
  return false;
290
367
  }
291
368
  }
@@ -301,5 +378,7 @@ module.exports = {
301
378
  getGitUser,
302
379
  formatMessage,
303
380
  detectSectionFromBranch,
304
- selectSection
381
+ selectSection,
382
+ prepareChangelogEntry,
383
+ appendPreparedChangelogEntry
305
384
  };