spectrum-cli 3.2.0 → 5.0.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +61 -30
  3. package/index.js +6 -174
  4. package/package.json +4 -4
  5. package/src/changelog/commands.js +44 -0
  6. package/src/{changelog-config.js → changelog/config.js} +1 -1
  7. package/src/changelog/format.js +51 -0
  8. package/src/changelog/fragments.js +223 -0
  9. package/src/{hotfix-changelog.js → changelog/hotfix.js} +2 -2
  10. package/src/changelog/index.js +7 -0
  11. package/src/{open-release.js → changelog/open-release.js} +6 -6
  12. package/src/changelog/release-block.js +80 -0
  13. package/src/chart/deploy.js +216 -0
  14. package/src/chart/helmrelease.js +90 -0
  15. package/src/chart/index.js +30 -0
  16. package/src/chart/metadata.js +114 -0
  17. package/src/chart/registry.js +139 -0
  18. package/src/chart/routes.js +244 -0
  19. package/src/chart/start.js +98 -0
  20. package/src/chart/verify.js +166 -0
  21. package/src/cli/command-executor.js +181 -0
  22. package/src/cli/commands.js +136 -0
  23. package/src/{development.js → cli/development.js} +1 -1
  24. package/src/cli/help.js +55 -0
  25. package/src/cli/run-action.js +28 -0
  26. package/src/{update-check.js → cli/update-check.js} +8 -2
  27. package/src/{git.js → git/index.js} +4 -3
  28. package/src/hotfix/checks.js +144 -0
  29. package/src/hotfix/close.js +46 -0
  30. package/src/hotfix/deploy.js +37 -0
  31. package/src/hotfix/git.js +35 -0
  32. package/src/hotfix/index.js +5 -0
  33. package/src/hotfix/start.js +57 -0
  34. package/src/integrations/gitlab-url.js +16 -0
  35. package/src/preflight/changelog.js +130 -0
  36. package/src/preflight/chart.js +57 -0
  37. package/src/preflight/git.js +133 -0
  38. package/src/preflight/helmrelease.js +201 -0
  39. package/src/preflight/index.js +10 -0
  40. package/src/preflight/result.js +18 -0
  41. package/src/preflight/source.js +95 -0
  42. package/src/preflight/tags.js +139 -0
  43. package/src/release/close.js +39 -0
  44. package/src/release/index.js +7 -0
  45. package/src/{stable-tags.js → release/stable-tags.js} +1 -1
  46. package/src/release/start.js +51 -0
  47. package/src/release/steps.js +83 -0
  48. package/src/release/version.js +20 -0
  49. package/src/token/config.js +163 -0
  50. package/src/token/gitlab-api.js +59 -0
  51. package/src/token/index.js +6 -0
  52. package/src/token/rotate.js +157 -0
  53. package/src/changelog.js +0 -418
  54. package/src/chart.js +0 -1071
  55. package/src/command-executor.js +0 -70
  56. package/src/hotfix.js +0 -208
  57. package/src/preflight.js +0 -796
  58. package/src/release.js +0 -207
  59. package/src/token.js +0 -399
  60. /package/src/{utils.js → common/utils.js} +0 -0
  61. /package/src/{version.js → common/version.js} +0 -0
package/src/changelog.js DELETED
@@ -1,418 +0,0 @@
1
- #!/usr/bin/env node
2
- const fs = require('fs');
3
- const path = require('path');
4
- const readline = require('readline');
5
- const { logSuccess, logError, execSilent, execCommand, getCurrentBranch, colors } = require('./utils');
6
- const { runCommand } = require('./command-executor');
7
- const {
8
- requireGitRepo,
9
- requireFileExists,
10
- requirePrettierAvailable,
11
- requireChangelogFormatted,
12
- requireChangelogFragments
13
- } = require('./preflight');
14
- const {
15
- CHANGELOG_FILE,
16
- CHANGELOG_DIR,
17
- FRAGMENT_TYPES,
18
- SECTION_TO_TYPE
19
- } = require('./changelog-config');
20
-
21
- function createReadlineInterface() {
22
- return readline.createInterface({
23
- input: process.stdin,
24
- output: process.stdout
25
- });
26
- }
27
-
28
- function askQuestion(question) {
29
- return new Promise((resolve) => {
30
- const rl = createReadlineInterface();
31
- rl.question(question, (answer) => {
32
- rl.close();
33
- resolve(answer.trim());
34
- });
35
- });
36
- }
37
-
38
- async function extractTaskFromBranch() {
39
- const currentBranch = getCurrentBranch();
40
- if (!currentBranch) {
41
- logError('❌', 'Не удалось получить имя текущей ветки.');
42
- return null;
43
- }
44
-
45
- const taskMatch = currentBranch.match(/^[a-z][a-z0-9-]*\/([A-Z]+-[0-9]+)(?:-[A-Za-z0-9][A-Za-z0-9._-]*)?$/);
46
- if (!taskMatch) {
47
- logError(
48
- '❌',
49
- 'Имя ветки "%s" должно соответствовать <type>/<YOUTRACK-ID> или <type>/<YOUTRACK-ID>-<slug>.',
50
- currentBranch
51
- );
52
- return null;
53
- }
54
-
55
- return taskMatch[1];
56
- }
57
-
58
- async function getGitUser() {
59
- let name = execSilent('git config user.name');
60
- let email = execSilent('git config user.email');
61
-
62
- if (!name || !email) {
63
- console.log(`⚠️ ${colors.yellow}Имя и email пользователя git не настроены.${colors.reset}`);
64
- console.log('💡 Их можно настроить так:');
65
- console.log(' git config user.name "Ваше Имя"');
66
- console.log(' git config user.email "your.email@domain.com"');
67
- console.log('');
68
-
69
- if (!name) {
70
- name = await askQuestion('👤 Введите ваше имя: ');
71
- if (!name) {
72
- logError('❌', 'Имя обязательно.');
73
- return null;
74
- }
75
- }
76
-
77
- if (!email) {
78
- email = await askQuestion('📧 Введите ваш email: ');
79
- if (!email || !email.includes('@')) {
80
- logError('❌', 'Требуется корректный email.');
81
- return null;
82
- }
83
- }
84
- }
85
-
86
- return `[${name}](${email})`;
87
- }
88
-
89
- function formatMessage(message) {
90
- const trimmedMessage = String(message || '').trim();
91
- if (!trimmedMessage) return '';
92
- return /[.!?]$/.test(trimmedMessage) ? trimmedMessage : `${trimmedMessage}.`;
93
- }
94
-
95
- function detectSectionFromBranch() {
96
- const currentBranch = getCurrentBranch();
97
- if (!currentBranch) return [];
98
-
99
- const branchType = currentBranch.split('/', 1)[0].toLowerCase();
100
- const sections = {
101
- support: [FRAGMENT_TYPES.support.section, FRAGMENT_TYPES.security.section],
102
- bugfix: [FRAGMENT_TYPES.fixed.section],
103
- feature: [
104
- FRAGMENT_TYPES.breaking.section,
105
- FRAGMENT_TYPES.added.section,
106
- FRAGMENT_TYPES.changed.section,
107
- FRAGMENT_TYPES.deprecated.section,
108
- FRAGMENT_TYPES.removed.section
109
- ]
110
- };
111
-
112
- if (branchType === 'support') {
113
- return sections.support;
114
- }
115
- if (branchType === 'bugfix' || branchType === 'fix' || branchType === 'hotfix') {
116
- return sections.bugfix;
117
- }
118
- if (branchType === 'feature' || branchType === 'feat') {
119
- return sections.feature;
120
- }
121
-
122
- return [];
123
- }
124
-
125
- async function selectSection(availableSections) {
126
- const sections = availableSections.length > 0
127
- ? availableSections
128
- : Object.values(FRAGMENT_TYPES).map((config) => config.section);
129
-
130
- if (sections.length === 1) {
131
- return sections[0];
132
- }
133
-
134
- console.log('\n📋 Выберите раздел:');
135
- sections.forEach((section, index) => {
136
- console.log(` ${index + 1}. ${section}`);
137
- });
138
-
139
- const choice = await askQuestion('\n🔢 Введите номер раздела: ');
140
- const choiceNum = parseInt(choice, 10);
141
-
142
- if (Number.isNaN(choiceNum) || choiceNum < 1 || choiceNum > sections.length) {
143
- logError('❌', 'Неверный выбор.');
144
- return null;
145
- }
146
-
147
- return sections[choiceNum - 1];
148
- }
149
-
150
- function sanitizeFragmentSlug(branchName, task) {
151
- const branchWithoutTask = String(branchName || '').replace(new RegExp(task, 'i'), '-');
152
- const slug = branchWithoutTask
153
- .toLowerCase()
154
- .replace(/[^a-z0-9]+/g, '-')
155
- .replace(/^-+|-+$/g, '')
156
- .slice(0, 80);
157
- return slug || 'change';
158
- }
159
-
160
- function createFragmentPath(task, type, branchName = getCurrentBranch()) {
161
- const slug = sanitizeFragmentSlug(branchName, task);
162
- return path.posix.join(CHANGELOG_DIR, `${task}-${slug}.${type}.md`);
163
- }
164
-
165
- function displayFragment(fragmentPath, entry) {
166
- console.log('\n📝 Changelog fragment создан:');
167
- console.log(` ${colors.green}${fragmentPath}${colors.reset}`);
168
- console.log(` ${colors.green}${entry}${colors.reset}`);
169
- console.log('');
170
- }
171
-
172
- async function prepareChangelogEntry(message) {
173
- try {
174
- const formattedMessage = formatMessage(message);
175
- if (!formattedMessage) {
176
- return { ok: false, reason: 'Сообщение changelog не может быть пустым.' };
177
- }
178
-
179
- const task = await extractTaskFromBranch();
180
- if (!task) return { ok: false, reason: 'Не удалось определить ID задачи.' };
181
-
182
- const user = await getGitUser();
183
- if (!user) return { ok: false, reason: 'Не удалось определить пользователя git.' };
184
-
185
- const selectedSection = await selectSection(detectSectionFromBranch());
186
- if (!selectedSection) return { ok: false, reason: 'Не удалось выбрать раздел.' };
187
-
188
- const type = SECTION_TO_TYPE[selectedSection];
189
- if (!type) return { ok: false, reason: `Неизвестный раздел changelog: "${selectedSection}".` };
190
-
191
- return {
192
- ok: true,
193
- data: {
194
- fragmentState: {
195
- entry: `- ${task} ${formattedMessage} ${user}`,
196
- fragmentPath: createFragmentPath(task, type),
197
- selectedSection,
198
- type
199
- }
200
- }
201
- };
202
- } catch (error) {
203
- return { ok: false, reason: `Ошибка при подготовке changelog fragment: ${error.message}` };
204
- }
205
- }
206
-
207
- function appendPreparedChangelogEntry(context) {
208
- try {
209
- const fragmentState = context.fragmentState;
210
- if (!fragmentState) return false;
211
-
212
- fs.mkdirSync(CHANGELOG_DIR, { recursive: true });
213
- let entries = [];
214
- if (fs.existsSync(fragmentState.fragmentPath)) {
215
- entries = fs.readFileSync(fragmentState.fragmentPath, 'utf8')
216
- .split('\n')
217
- .map((line) => line.trim())
218
- .filter(Boolean);
219
- if (entries.some((line) => !line.startsWith('- '))) {
220
- logError('❌', 'Существующий fragment имеет неверный формат: %s', fragmentState.fragmentPath);
221
- return false;
222
- }
223
- }
224
-
225
- if (!entries.includes(fragmentState.entry)) {
226
- entries.push(fragmentState.entry);
227
- }
228
- fs.writeFileSync(fragmentState.fragmentPath, `${entries.join('\n')}\n`);
229
- displayFragment(fragmentState.fragmentPath, fragmentState.entry);
230
- logSuccess('✅', 'Запись добавлена в fragment раздела %s', fragmentState.selectedSection);
231
- return true;
232
- } catch (error) {
233
- logError('❌', 'Ошибка при записи changelog fragment: %s', error.message);
234
- return false;
235
- }
236
- }
237
-
238
- function changelogAppend(message) {
239
- return runCommand({
240
- name: 'changelog append',
241
- checks: [
242
- { name: 'git-repo', run: requireGitRepo },
243
- { name: 'changelog-exists', run: () => requireFileExists(CHANGELOG_FILE) },
244
- { name: 'prepare-fragment', run: () => prepareChangelogEntry(message) }
245
- ],
246
- steps: [
247
- { name: 'write-fragment', run: appendPreparedChangelogEntry }
248
- ]
249
- });
250
- }
251
-
252
- function changelogCheck() {
253
- return runCommand({
254
- name: 'changelog check',
255
- checks: [
256
- { name: 'git-repo', run: requireGitRepo },
257
- { name: 'changelog-exists', run: () => requireFileExists(CHANGELOG_FILE) },
258
- { name: 'changelog-prettier-check', run: requireChangelogFormatted },
259
- { name: 'changelog-fragments', run: requireChangelogFragments }
260
- ]
261
- });
262
- }
263
-
264
- function normalizeChangelogHeadings(text) {
265
- const version = '(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)';
266
- const date = '(\\d{4}-\\d{2}-\\d{2}|\\d{4}\\.\\d{2}\\.\\d{2}|\\d{2}\\.\\d{2}\\.\\d{4})';
267
- const releaseHeading = new RegExp(`^##[ \\t]*(?:(🚀|🩹)[ \\t]*)?\\[${version}\\](?:[ \\t]+-[ \\t]+${date})?[ \\t]*\\r?$`);
268
- return text.replace(/^##(?!#)[^\n]*$/gm, (heading) => {
269
- const match = releaseHeading.exec(heading);
270
- if (!match) throw new Error(`Некорректный заголовок ${CHANGELOG_FILE}: ${heading.trimEnd()}`);
271
- const marker = match[1] ? `${match[1]} ` : '';
272
- const releaseVersion = `${match[2]}.${match[3]}.${match[4]}`;
273
- const releaseDate = match[5] ? ` - ${match[5]}` : '';
274
- return `## ${marker}[${releaseVersion}]${releaseDate}${heading.endsWith('\r') ? '\r' : ''}`;
275
- });
276
- }
277
-
278
- function writeCurrentChangelog(context) {
279
- let original;
280
- let mayHaveChanged = false;
281
- try {
282
- original = fs.readFileSync(CHANGELOG_FILE, 'utf8');
283
- const normalized = normalizeChangelogHeadings(original);
284
- if (normalized !== original) {
285
- mayHaveChanged = true;
286
- fs.writeFileSync(CHANGELOG_FILE, normalized);
287
- }
288
- mayHaveChanged = true;
289
- if (!execCommand(`${context.prettierRunner} --write ${CHANGELOG_FILE}`) ||
290
- !execCommand(`${context.prettierRunner} --check ${CHANGELOG_FILE}`)) {
291
- throw new Error('Prettier не смог отформатировать CHANGELOG.md.');
292
- }
293
- const formatted = fs.readFileSync(CHANGELOG_FILE, 'utf8');
294
- if (normalizeChangelogHeadings(formatted) !== formatted) {
295
- throw new Error('После Prettier заголовки CHANGELOG.md не соответствуют формату релизов.');
296
- }
297
- return true;
298
- } catch (error) {
299
- if (mayHaveChanged && original !== undefined) {
300
- try {
301
- fs.writeFileSync(CHANGELOG_FILE, original);
302
- } catch (restoreError) {
303
- logError('❌', 'Не удалось восстановить %s: %s', CHANGELOG_FILE, restoreError.message);
304
- }
305
- }
306
- logError('❌', 'Не удалось отформатировать %s: %s', CHANGELOG_FILE, error.message);
307
- return false;
308
- }
309
- }
310
-
311
- function changelogWrite() {
312
- return runCommand({
313
- name: 'changelog write',
314
- checks: [
315
- { name: 'git-repo', run: requireGitRepo },
316
- { name: 'changelog-exists', run: () => requireFileExists(CHANGELOG_FILE) },
317
- { name: 'prettier-available', run: requirePrettierAvailable }
318
- ],
319
- steps: [{ name: 'write-changelog', run: writeCurrentChangelog }]
320
- });
321
- }
322
-
323
- function stripLegacyUnreleasedBlock(changelog) {
324
- const unreleasedMatch = /^## \[Unreleased\]\s*$/m.exec(changelog);
325
- if (!unreleasedMatch) return changelog;
326
-
327
- const afterUnreleased = unreleasedMatch.index + unreleasedMatch[0].length;
328
- const nextHeadingMatch = /^## /m.exec(changelog.slice(afterUnreleased));
329
- const end = nextHeadingMatch ? afterUnreleased + nextHeadingMatch.index : changelog.length;
330
- return `${changelog.slice(0, unreleasedMatch.index).trimEnd()}\n\n${changelog.slice(end).trimStart()}`;
331
- }
332
-
333
- function renderReleaseBlock(version, fragments, date = new Date().toISOString().slice(0, 10), marker = '🚀') {
334
- const fragmentsByType = new Map();
335
- for (const fragment of fragments) {
336
- const entries = fragmentsByType.get(fragment.type) || [];
337
- entries.push(...fragment.entries);
338
- fragmentsByType.set(fragment.type, entries);
339
- }
340
-
341
- const lines = [`## ${marker ? `${marker} ` : ''}[${version}]${date ? ` - ${date}` : ''}`];
342
- for (const [type, config] of Object.entries(FRAGMENT_TYPES)) {
343
- const entries = fragmentsByType.get(type);
344
- if (!entries || entries.length === 0) continue;
345
- lines.push('', config.section, '', ...entries);
346
- }
347
- return `${lines.join('\n')}\n`;
348
- }
349
-
350
- function insertReleaseBlock(changelog, releaseBlock, version) {
351
- const normalized = stripLegacyUnreleasedBlock(changelog).trimEnd();
352
- const escapedVersion = String(version).replace(/\./g, '\\.');
353
- const versionPattern = new RegExp(`^## 🚀 \\[${escapedVersion}\\](?:\\s|$)`, 'm');
354
- if (versionPattern.test(normalized)) {
355
- throw new Error(`Версия ${version} уже присутствует в ${CHANGELOG_FILE}.`);
356
- }
357
-
358
- const firstReleaseMatch = /^## /m.exec(normalized);
359
- if (!firstReleaseMatch) {
360
- return `${normalized}\n\n${releaseBlock}`;
361
- }
362
-
363
- return `${normalized.slice(0, firstReleaseMatch.index).trimEnd()}\n\n${releaseBlock}\n${normalized.slice(firstReleaseMatch.index).trimStart()}\n`;
364
- }
365
-
366
- function changelogBuildRelease(context, date) {
367
- try {
368
- const version = context.newVersion;
369
- const fragments = context.changelogFragments;
370
- if (!version || !Array.isArray(fragments) || fragments.length === 0) return false;
371
-
372
- const changelog = fs.readFileSync(CHANGELOG_FILE, 'utf8');
373
- const releaseBlock = renderReleaseBlock(version, fragments, date);
374
- const updatedChangelog = insertReleaseBlock(changelog, releaseBlock, version);
375
- fs.writeFileSync(CHANGELOG_FILE, updatedChangelog);
376
- logSuccess('📋', '%s собран из %s changelog fragments.', `CHANGELOG ${version}`, fragments.length);
377
- return true;
378
- } catch (error) {
379
- logError('❌', 'Не удалось собрать релизный changelog: %s', error.message);
380
- return false;
381
- }
382
- }
383
-
384
- function changelogRemoveFragments(context) {
385
- try {
386
- const fragments = context.changelogFragments;
387
- if (!Array.isArray(fragments) || fragments.length === 0) return false;
388
- for (const fragment of fragments) {
389
- fs.unlinkSync(fragment.filePath);
390
- }
391
- logSuccess('🧹', 'Использованные changelog fragments удалены: %s.', fragments.length);
392
- return true;
393
- } catch (error) {
394
- logError('❌', 'Не удалось удалить changelog fragments: %s', error.message);
395
- return false;
396
- }
397
- }
398
-
399
- module.exports = {
400
- changelogAppend,
401
- changelogCheck,
402
- changelogWrite,
403
- normalizeChangelogHeadings,
404
- extractTaskFromBranch,
405
- getGitUser,
406
- formatMessage,
407
- detectSectionFromBranch,
408
- selectSection,
409
- sanitizeFragmentSlug,
410
- createFragmentPath,
411
- prepareChangelogEntry,
412
- appendPreparedChangelogEntry,
413
- stripLegacyUnreleasedBlock,
414
- renderReleaseBlock,
415
- insertReleaseBlock,
416
- changelogBuildRelease,
417
- changelogRemoveFragments
418
- };