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/CHANGELOG.md +29 -0
- package/README.md +78 -11
- package/index.js +65 -38
- package/package.json +5 -3
- package/src/changelog.js +130 -51
- package/src/chart.js +821 -0
- package/src/command-executor.js +70 -0
- package/src/development.js +11 -11
- package/src/git.js +59 -40
- package/src/preflight.js +556 -0
- package/src/release.js +102 -55
- package/src/utils.js +47 -9
- package/src/version.js +23 -17
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('🔄',
|
|
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('🧹', '
|
|
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('📋', '
|
|
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'
|
|
66
|
-
const commitSuccess = execCommand('git commit --message "📝
|
|
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('📝', '
|
|
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('❌', '
|
|
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}
|
|
102
|
-
const task = await askQuestion('📝
|
|
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('❌', '
|
|
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}
|
|
119
|
-
console.log(`💡
|
|
120
|
-
console.log(` git config user.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('👤
|
|
164
|
+
name = await askQuestion('👤 Введите ваше имя: ');
|
|
126
165
|
if (!name) {
|
|
127
|
-
logError('❌', '
|
|
166
|
+
logError('❌', 'Имя обязательно.');
|
|
128
167
|
return null;
|
|
129
168
|
}
|
|
130
169
|
}
|
|
131
170
|
|
|
132
171
|
if (!email) {
|
|
133
|
-
email = await askQuestion('📧
|
|
172
|
+
email = await askQuestion('📧 Введите ваш email: ');
|
|
134
173
|
if (!email || !email.includes('@')) {
|
|
135
|
-
logError('❌', '
|
|
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📋
|
|
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🔢
|
|
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('❌', '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
fs.writeFileSync('CHANGELOG.md', updatedChangelog);
|
|
283
|
-
|
|
359
|
+
|
|
360
|
+
fs.writeFileSync('CHANGELOG.md', lines.join('\n'));
|
|
284
361
|
displayContext(lines, insertIndex, entry);
|
|
285
|
-
logSuccess('✅',
|
|
362
|
+
logSuccess('✅', 'Запись добавлена в %s', selectedSection);
|
|
286
363
|
return true;
|
|
287
364
|
} catch (error) {
|
|
288
|
-
logError('❌',
|
|
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
|
};
|