spectrum-cli 3.1.1 → 4.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 (60) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +81 -29
  3. package/index.js +6 -158
  4. package/package.json +4 -4
  5. package/src/changelog/commands.js +44 -0
  6. package/src/changelog/format.js +51 -0
  7. package/src/{changelog.js → changelog/fragments.js} +3 -136
  8. package/src/{hotfix-changelog.js → changelog/hotfix.js} +13 -18
  9. package/src/changelog/index.js +7 -0
  10. package/src/{open-release.js → changelog/open-release.js} +6 -6
  11. package/src/changelog/release-block.js +80 -0
  12. package/src/chart/deploy.js +216 -0
  13. package/src/chart/helmrelease.js +90 -0
  14. package/src/chart/index.js +30 -0
  15. package/src/chart/metadata.js +114 -0
  16. package/src/chart/registry.js +139 -0
  17. package/src/chart/routes.js +244 -0
  18. package/src/chart/start.js +98 -0
  19. package/src/chart/verify.js +166 -0
  20. package/src/cli/command-executor.js +181 -0
  21. package/src/cli/commands.js +136 -0
  22. package/src/{development.js → cli/development.js} +1 -1
  23. package/src/cli/help.js +55 -0
  24. package/src/cli/run-action.js +28 -0
  25. package/src/cli/update-check.js +51 -0
  26. package/src/{git.js → git/index.js} +4 -3
  27. package/src/hotfix/checks.js +144 -0
  28. package/src/hotfix/close.js +46 -0
  29. package/src/hotfix/deploy.js +37 -0
  30. package/src/hotfix/git.js +35 -0
  31. package/src/hotfix/index.js +5 -0
  32. package/src/hotfix/start.js +57 -0
  33. package/src/integrations/gitlab-url.js +16 -0
  34. package/src/preflight/changelog.js +130 -0
  35. package/src/preflight/chart.js +57 -0
  36. package/src/preflight/git.js +133 -0
  37. package/src/preflight/helmrelease.js +201 -0
  38. package/src/preflight/index.js +10 -0
  39. package/src/preflight/result.js +18 -0
  40. package/src/preflight/source.js +95 -0
  41. package/src/preflight/tags.js +139 -0
  42. package/src/release/close.js +39 -0
  43. package/src/release/index.js +7 -0
  44. package/src/{stable-tags.js → release/stable-tags.js} +1 -1
  45. package/src/release/start.js +51 -0
  46. package/src/release/steps.js +83 -0
  47. package/src/release/version.js +20 -0
  48. package/src/token/config.js +163 -0
  49. package/src/token/gitlab-api.js +59 -0
  50. package/src/token/index.js +6 -0
  51. package/src/token/rotate.js +157 -0
  52. package/src/chart.js +0 -1071
  53. package/src/command-executor.js +0 -70
  54. package/src/hotfix.js +0 -223
  55. package/src/preflight.js +0 -796
  56. package/src/release.js +0 -207
  57. package/src/token.js +0 -399
  58. /package/src/{changelog-config.js → changelog/config.js} +0 -0
  59. /package/src/{utils.js → common/utils.js} +0 -0
  60. /package/src/{version.js → common/version.js} +0 -0
@@ -0,0 +1,166 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const { logSuccess, logError, colors } = require('../common/utils');
5
+ const { runCommand } = require('../cli/command-executor');
6
+ const { requireGitRepo, requireCurrentBranchUpToDateWithRemote, requireSingleValuesYaml, requireIngressPathSections, requireSourcePathDirectory, requireNextProject, requireBuildCommandSupport } = require('../preflight');
7
+ const { normalizeList, normalizeToBeList, collectRoutesFromFilesystem, collectRoutesFromBuildArtifacts } = require('./routes');
8
+ const FIXED_ASSET_PATHS = [
9
+ '/_next/image$',
10
+ '/_next/static/media/[a-z0-9_\\.\\-]+\\.(woff|woff2|ttf|otf|eot)$',
11
+ '/_next/static/chunks/[a-z0-9_\\.\\-]+\\.(css|js)$',
12
+ '/_next/static/[a-zA-Z0-9_-]+/_ssgManifest.js$',
13
+ '/_next/static/[a-zA-Z0-9_-]+/_buildManifest.js$',
14
+ '/_next/static/[a-zA-Z0-9_-]+/_clientMiddlewareManifest.json$',
15
+ '/[^?]+\\.(?:ico|svg|png|jpg|jpeg|gif|webp|txt|map)$'
16
+ ];
17
+
18
+ function runBuildInSourcePath(sourcePath, packageManager) {
19
+ const manager = packageManager || 'npm';
20
+ const hasNodeModules = fs.existsSync(path.join(sourcePath, 'node_modules'));
21
+ const hasPackageLock = fs.existsSync(path.join(sourcePath, 'package-lock.json'));
22
+ const hasYarnLock = fs.existsSync(path.join(sourcePath, 'yarn.lock'));
23
+ const hasBunLock = fs.existsSync(path.join(sourcePath, 'bun.lockb'));
24
+ const installCommand = manager === 'yarn'
25
+ ? (hasYarnLock ? 'yarn install --frozen-lockfile' : 'yarn install')
26
+ : manager === 'bun'
27
+ ? 'bun install'
28
+ : (hasPackageLock ? 'npm ci' : 'npm install');
29
+ const buildCommand = manager === 'yarn' ? 'yarn build' : manager === 'bun' ? 'bun run build' : 'npm run build';
30
+
31
+ try {
32
+ if (!hasNodeModules) {
33
+ execSync(installCommand, { cwd: sourcePath, stdio: 'pipe', timeout: 600000 });
34
+ } else if (!hasPackageLock && !hasYarnLock && !hasBunLock) {
35
+ execSync(installCommand, { cwd: sourcePath, stdio: 'pipe', timeout: 600000 });
36
+ }
37
+
38
+ execSync(buildCommand, { cwd: sourcePath, stdio: 'pipe', timeout: 900000 });
39
+ return true;
40
+ } catch (error) {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ function buildGitLikeDiff(asIsList, toBeList, title) {
46
+ const asIs = normalizeList(asIsList);
47
+ const toBe = normalizeList(toBeList);
48
+ const asIsSet = new Set(asIs);
49
+ const toBeSet = new Set(toBe);
50
+ const removed = asIs.filter((item) => !toBeSet.has(item));
51
+ const added = toBe.filter((item) => !asIsSet.has(item));
52
+ const unchanged = asIs.filter((item) => toBeSet.has(item));
53
+
54
+ const lines = [
55
+ `--- AS_IS/${title}`,
56
+ `+++ TO_BE/${title}`,
57
+ `@@ ${title} @@`
58
+ ];
59
+ removed.forEach((item) => lines.push(`-${item}`));
60
+ added.forEach((item) => lines.push(`+${item}`));
61
+ if (removed.length === 0 && added.length === 0) {
62
+ lines.push(' (no changes)');
63
+ }
64
+
65
+ return {
66
+ removed,
67
+ added,
68
+ unchanged,
69
+ text: lines.join('\n')
70
+ };
71
+ }
72
+
73
+ function printSection(title, asIsList, toBeList) {
74
+ const asIs = normalizeList(asIsList);
75
+ const toBe = normalizeList(toBeList);
76
+ const diff = buildGitLikeDiff(asIs, toBe, title);
77
+
78
+ console.log(`\nСравнение путей ${title} (КАК ЕСТЬ -> КАК ДОЛЖНО БЫТЬ):`);
79
+ console.log(`--- AS_IS/${title}`);
80
+ console.log(`+++ TO_BE/${title}`);
81
+ console.log(`@@ ${title} @@`);
82
+ if (diff.removed.length === 0 && diff.added.length === 0) {
83
+ console.log(' (без изменений)');
84
+ } else {
85
+ diff.removed.forEach((item) => console.log(`${colors.yellow}-${item}${colors.reset}`));
86
+ diff.added.forEach((item) => console.log(`${colors.red}+${item}${colors.reset}`));
87
+ }
88
+ console.log(`${title} сводка: добавлено=${diff.added.length}, удалено=${diff.removed.length}, без изменений=${diff.unchanged.length}`);
89
+
90
+ return diff;
91
+ }
92
+
93
+ function chartVerify(sourcePath) {
94
+ return runCommand({
95
+ name: 'chart verify',
96
+ checks: [
97
+ { name: 'git-repo', run: requireGitRepo },
98
+ { name: 'branch-up-to-date', run: requireCurrentBranchUpToDateWithRemote },
99
+ { name: 'single-values-yaml', run: requireSingleValuesYaml },
100
+ { name: 'values-ingress-sections', requires: ['valuesYamlPath'], run: (ctx) => requireIngressPathSections(ctx.valuesYamlPath) },
101
+ { name: 'source-path-directory', run: () => requireSourcePathDirectory(sourcePath) },
102
+ { name: 'next-project', requires: ['sourcePath'], run: (ctx) => requireNextProject(ctx.sourcePath) },
103
+ { name: 'build-command-support', requires: ['sourcePath'], run: (ctx) => requireBuildCommandSupport(ctx.sourcePath) }
104
+ ],
105
+ steps: [
106
+ {
107
+ name: 'collect-routes',
108
+ run: (ctx) => {
109
+ const fromBuild = collectRoutesFromBuildArtifacts(ctx.sourcePath);
110
+ let api = normalizeList(fromBuild.api);
111
+ let pages = normalizeList(fromBuild.pages);
112
+
113
+ if (api.length === 0 || pages.length === 0) {
114
+ const didBuild = runBuildInSourcePath(ctx.sourcePath, ctx.sourcePackageManager);
115
+ if (didBuild) {
116
+ logSuccess('🛠️', 'Сборка успешно завершена в %s.', ctx.sourcePath);
117
+ const rebuilt = collectRoutesFromBuildArtifacts(ctx.sourcePath);
118
+ api = normalizeList(api.concat(rebuilt.api));
119
+ pages = normalizeList(pages.concat(rebuilt.pages));
120
+ } else {
121
+ logError('⚠️', 'Сборка в %s не удалась. Переходим к маршрутам из файловой системы.', ctx.sourcePath);
122
+ }
123
+ }
124
+
125
+ const fromFs = collectRoutesFromFilesystem(ctx.sourcePath);
126
+ api = normalizeList(api.concat(fromFs.api));
127
+ pages = normalizeList(pages.concat(fromFs.pages));
128
+
129
+ ctx.toBeIngressPaths = {
130
+ api: normalizeToBeList(api),
131
+ pages: normalizeToBeList(pages),
132
+ assets: normalizeToBeList(FIXED_ASSET_PATHS)
133
+ };
134
+ return true;
135
+ }
136
+ },
137
+ {
138
+ name: 'compare-and-report',
139
+ run: (ctx) => {
140
+ const asIs = ctx.valuesIngressPaths || { api: [], pages: [], assets: [] };
141
+ const toBe = ctx.toBeIngressPaths || { api: [], pages: [], assets: [] };
142
+
143
+ const apiDiff = printSection('api', asIs.api, toBe.api);
144
+ const pagesDiff = printSection('pages', asIs.pages, toBe.pages);
145
+ const assetsDiff = printSection('assets', asIs.assets, toBe.assets);
146
+
147
+ const hasChanges = (
148
+ apiDiff.added.length + apiDiff.removed.length +
149
+ pagesDiff.added.length + pagesDiff.removed.length +
150
+ assetsDiff.added.length + assetsDiff.removed.length
151
+ ) > 0;
152
+
153
+ if (hasChanges) {
154
+ logError('❌', 'Обнаружены различия.');
155
+ return false;
156
+ }
157
+
158
+ logSuccess('✅', 'КАК ЕСТЬ совпадает с КАК ДОЛЖНО БЫТЬ.');
159
+ return true;
160
+ }
161
+ }
162
+ ]
163
+ });
164
+ }
165
+
166
+ module.exports = { chartVerify, buildGitLikeDiff };
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ const { logError, logSuccess } = require('../common/utils');
3
+ const { AsyncLocalStorage } = require('async_hooks');
4
+
5
+ const commandContext = new AsyncLocalStorage();
6
+
7
+ const PREFLIGHT_DESCRIPTIONS = Object.freeze({
8
+ 'git-repo': 'текущий каталог находится в Git-репозитории',
9
+ 'clean-working-tree': 'рабочее дерево Git чистое',
10
+ 'branch-up-to-date': 'текущая ветка не отстаёт от upstream',
11
+ 'main-and-dev-branches': 'найдены production- и integration-ветки',
12
+ 'on-main-branch': 'выбрана production-ветка main/master',
13
+ 'on-dev-branch': 'выбрана integration-ветка dev/develop',
14
+ 'dev-contains-main': 'integration-ветка содержит актуальную production-ветку',
15
+ 'stable-version': 'найден последний стабильный тег production',
16
+ 'changelog-exists': 'найден CHANGELOG.md',
17
+ 'release-state': 'состояние опубликованных и открытых релизов согласовано',
18
+ 'changelog-prettier-check': 'CHANGELOG.md проходит проверку Prettier',
19
+ 'changelog-fragments': 'проверены changelog fragments и их формат',
20
+ 'detect-bump-type': 'определена целевая версия и проверена её доступность',
21
+ 'changelog-release-version': 'прочитана стабильная версия из CHANGELOG.md',
22
+ 'stable-tag-at-head': 'стабильный тег указывает на текущий commit',
23
+ 'prettier-available': 'Prettier доступен',
24
+ 'prepare-fragment': 'проверены ветка, ID задачи, Git identity и раздел fragment',
25
+ 'single-chart': 'найден ровно один Chart.yaml с именем чарта',
26
+ 'chart-changelog-version': 'версия прочитана из верхнего заголовка changelog чарта',
27
+ 'tag-missing': 'целевой тег отсутствует локально и на origin',
28
+ 'version-not-downgrade': 'версия сопоставлена с последним chart-тегом на origin',
29
+ 'registry-version-missing': 'целевая версия чарта отсутствует в Helm-registry',
30
+ 'remote-origin': 'настроен remote origin',
31
+ 'remote-reachable': 'remote origin доступен',
32
+ 'helmrelease-files': 'найдены файлы helmrelease.yaml',
33
+ 'filter-instances': 'параметр --instances проверен (или не задан)',
34
+ 'single-values-yaml': 'найден единственный values.yaml чарта',
35
+ 'values-ingress-sections': 'найдены секции ingress paths',
36
+ 'source-path-directory': 'исходный каталог существует',
37
+ 'next-project': 'исходники принадлежат Next.js-проекту',
38
+ 'build-command-support': 'доступна команда сборки проекта',
39
+ 'load-config': 'конфигурация ротации токена загружена и проверена',
40
+ 'ask-tokens': 'необходимые токены получены без вывода их значений',
41
+ 'check-access': 'проверен доступ к целевым группам и проектам GitLab'
42
+ });
43
+
44
+ function describePreflight(checkName, context) {
45
+ const description = PREFLIGHT_DESCRIPTIONS[checkName] || checkName;
46
+ const details = {
47
+ 'branch-up-to-date': context.currentBranch,
48
+ 'stable-version': context.stableVersion,
49
+ 'changelog-release-version': context.version,
50
+ 'detect-bump-type': context.newVersion,
51
+ 'single-chart': context.chartName,
52
+ 'chart-changelog-version': context.version,
53
+ 'changelog-fragments': Array.isArray(context.changelogFragments)
54
+ ? `${context.changelogFragments.length} файл(ов)` : null,
55
+ 'helmrelease-files': Array.isArray(context.helmReleaseFiles)
56
+ ? `${context.helmReleaseFiles.length} файл(ов)` : null
57
+ }[checkName];
58
+ return details ? `${description}: ${details}` : description;
59
+ }
60
+
61
+ function withCommandOptions(options, action) {
62
+ return commandContext.run({ dry: Boolean(options?.dry), silence: Boolean(options?.silence) }, action);
63
+ }
64
+
65
+ function withDryRun(dry, action) {
66
+ return withCommandOptions({ dry }, action);
67
+ }
68
+
69
+ function isDryRun() {
70
+ return Boolean(commandContext.getStore()?.dry);
71
+ }
72
+
73
+ function reportDryRun(_name) {
74
+ logSuccess('✅', 'Все предпроверки пройдены, проблем нет. Действия не выполнялись.');
75
+ }
76
+
77
+ function reportPreflight(_commandName, checkName, description = PREFLIGHT_DESCRIPTIONS[checkName] || checkName) {
78
+ if (!commandContext.getStore()?.silence) {
79
+ logSuccess('🔎', 'Проверено (%s): %s.', checkName, description);
80
+ }
81
+ }
82
+
83
+ function reportNoPreflights(_name) {
84
+ logSuccess('🔎', 'Предпроверок нет.');
85
+ }
86
+
87
+ function normalizeCheckResult(result, fallbackReason) {
88
+ if (typeof result === 'boolean') {
89
+ return { ok: result, reason: result ? null : fallbackReason };
90
+ }
91
+
92
+ if (typeof result === 'string') {
93
+ return { ok: false, reason: result };
94
+ }
95
+
96
+ if (result && typeof result === 'object') {
97
+ return {
98
+ ok: Boolean(result.ok),
99
+ reason: result.reason || fallbackReason,
100
+ data: result.data
101
+ };
102
+ }
103
+
104
+ return { ok: false, reason: fallbackReason };
105
+ }
106
+
107
+ async function runCommand(spec) {
108
+ const {
109
+ name,
110
+ checks = [],
111
+ steps = [],
112
+ context = {}
113
+ } = spec || {};
114
+
115
+ if (!name) {
116
+ logError('❌', 'Требуется имя команды.');
117
+ return false;
118
+ }
119
+
120
+ let failedPreflights = 0;
121
+ const results = new Map();
122
+ for (const check of checks) {
123
+ const checkName = check.name || 'unknown-check';
124
+ const failedDependencies = (check.dependsOn || []).filter((dependency) => results.get(dependency) !== true);
125
+ const missingInputs = (check.requires || []).filter((key) => context[key] === undefined || context[key] === null);
126
+ if (failedDependencies.length || missingInputs.length) {
127
+ const reason = [
128
+ failedDependencies.length ? `не пройдены проверки: ${failedDependencies.join(', ')}` : null,
129
+ missingInputs.length ? `отсутствуют данные: ${missingInputs.join(', ')}` : null
130
+ ].filter(Boolean).join('; ');
131
+ logError('❌', 'Предпроверка не выполнена (%s): %s', checkName, reason);
132
+ results.set(checkName, false);
133
+ failedPreflights += 1;
134
+ continue;
135
+ }
136
+
137
+ let checkResult;
138
+ try {
139
+ checkResult = normalizeCheckResult(
140
+ await check.run(context),
141
+ `Проверка не пройдена: ${checkName}`
142
+ );
143
+ } catch (error) {
144
+ checkResult = { ok: false, reason: error?.message || String(error) };
145
+ }
146
+
147
+ if (!checkResult.ok) {
148
+ logError('❌', 'Предпроверка не пройдена (%s): %s', checkName, checkResult.reason || 'неизвестная причина');
149
+ results.set(checkName, false);
150
+ failedPreflights += 1;
151
+ continue;
152
+ }
153
+
154
+ results.set(checkName, true);
155
+ if (checkResult.data && typeof checkResult.data === 'object') {
156
+ Object.assign(context, checkResult.data);
157
+ }
158
+ reportPreflight(name, checkName, check.description || describePreflight(checkName, context));
159
+ }
160
+
161
+ if (failedPreflights > 0) return false;
162
+
163
+ if (isDryRun()) {
164
+ reportDryRun(name);
165
+ return true;
166
+ }
167
+
168
+ for (const step of steps) {
169
+ const stepName = step.name || 'unknown-step';
170
+ const ok = await step.run(context);
171
+ if (!ok) {
172
+ logError('❌', 'Шаг не выполнен: %s', stepName);
173
+ return false;
174
+ }
175
+ }
176
+
177
+ logSuccess('✅', 'Выполнено.');
178
+ return true;
179
+ }
180
+
181
+ module.exports = { runCommand, withCommandOptions, withDryRun, isDryRun, reportDryRun, reportPreflight, reportNoPreflights };
@@ -0,0 +1,136 @@
1
+ const git = require('../git');
2
+ const release = require('../release');
3
+ const hotfix = require('../hotfix');
4
+ const changelog = require('../changelog');
5
+ const chart = require('../chart');
6
+ const token = require('../token');
7
+ const { upgrade } = require('./update-check');
8
+ const { version: pkgVersion } = require('../../package.json');
9
+ const { runAction } = require('./run-action');
10
+
11
+ function registerCommands(program) {
12
+ program
13
+ .name("spectrum")
14
+ .description("🚀 Spectrum CLI для процесса разработки")
15
+ .version(pkgVersion, "-v, --version");
16
+
17
+ program
18
+ .command("upgrade")
19
+ .description("Обновить Spectrum CLI через npm install -g spectrum-cli")
20
+ .option("-d, --dry", "Запустить только предпроверки")
21
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
22
+ .action((command) => runAction(upgrade, command));
23
+
24
+ // Команды релиза
25
+ const releaseCmd = program
26
+ .command("release")
27
+ .description("Команды управления релизом");
28
+
29
+ releaseCmd
30
+ .command("start")
31
+ .description("Схлопнуть fragments и атомарно отправить release commit в dev и main/master")
32
+ .option("-d, --dry", "Запустить только предпроверки")
33
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
34
+ .action((command) => runAction(release.releaseStart, command));
35
+
36
+ releaseCmd
37
+ .command("close")
38
+ .description("Свести стабильный релиз из main/master в dev")
39
+ .option("-d, --dry", "Запустить только предпроверки")
40
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
41
+ .action((command) => runAction(release.releaseClose, command));
42
+
43
+ releaseCmd
44
+ .command("deploy")
45
+ .description("Создать и отправить только стабильный тег release/X.Y.Z")
46
+ .option("-d, --dry", "Запустить только предпроверки")
47
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
48
+ .action((command) => runAction(git.gitCreateTagAndPush, command));
49
+
50
+ const hotfixCmd = program.command("hotfix").description("Изолированный цикл срочного patch-релиза");
51
+
52
+ hotfixCmd.command("start")
53
+ .description("Подготовить 🩹 CHANGELOG на hotfix/* или main/master, без commit, push и merge")
54
+ .option("-d, --dry", "Запустить только предпроверки")
55
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
56
+ .action((command) => runAction(hotfix.hotfixStart, command));
57
+ hotfixCmd.command("deploy")
58
+ .description("После merge хотфикса отправить stable-тег с main/master")
59
+ .option("-d, --dry", "Запустить только предпроверки")
60
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
61
+ .action((command) => runAction(hotfix.hotfixDeploy, command));
62
+ hotfixCmd.command("close")
63
+ .description("После stable pipeline свести production в dev")
64
+ .option("-d, --dry", "Запустить только предпроверки")
65
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
66
+ .action((command) => runAction(hotfix.hotfixClose, command));
67
+
68
+ // Команды changelog
69
+ const changelogCmd = program
70
+ .command("changelog")
71
+ .description("Команды управления changelog fragments");
72
+
73
+ changelogCmd
74
+ .command("append <message>")
75
+ .description("Создать changelog fragment с номером задачи из ветки")
76
+ .option("-d, --dry", "Запустить только предпроверки")
77
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
78
+ .action((message, command) => runAction(() => changelog.changelogAppend(message), command));
79
+
80
+ changelogCmd
81
+ .command("check")
82
+ .description("Проверить CHANGELOG.md и changelog fragments")
83
+ .option("-d, --dry", "Запустить только предпроверки")
84
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
85
+ .action((command) => runAction(changelog.changelogCheck, command));
86
+
87
+ changelogCmd
88
+ .command("write")
89
+ .description("Привести заголовки CHANGELOG.md к формату релиза и запустить Prettier")
90
+ .option("-d, --dry", "Запустить только предпроверки")
91
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
92
+ .action((command) => runAction(changelog.changelogWrite, command));
93
+
94
+ // Команды chart
95
+ const chartCmd = program
96
+ .command("chart")
97
+ .description("Команды управления тегами chart");
98
+
99
+ chartCmd
100
+ .command("start")
101
+ .description("Создать и отправить chart-тег по верхней версии CHANGELOG.md чарта")
102
+ .allowExcessArguments(false)
103
+ .option("-f, --force", "Разрешить версию не больше последней версии тега на origin")
104
+ .option("-n, --no-wait", "Не ждать публикации версии чарта в Helm-registry")
105
+ .option("-d, --dry", "Запустить только предпроверки")
106
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
107
+ .action((command) => runAction(chart.chartStart, command));
108
+
109
+ chartCmd
110
+ .command("verify <source_path>")
111
+ .description("Проверить пути ingress chart относительно исходников Next.js")
112
+ .option("-d, --dry", "Запустить только предпроверки")
113
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
114
+ .action((sourcePath, command) => runAction(() => chart.chartVerify(sourcePath), command));
115
+
116
+ chartCmd
117
+ .command("deploy")
118
+ .description("Задеплоить последнюю версию chart в файлы helmrelease")
119
+ .option("-i, --instances <names>", "Список инстансов через запятую (по умолчанию все)")
120
+ .option("-d, --dry", "Запустить только предпроверки")
121
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
122
+ .action((command) => runAction(chart.chartDeploy, command));
123
+
124
+ const tokenCmd = program
125
+ .command("token")
126
+ .description("Команды управления GitLab токенами");
127
+
128
+ tokenCmd
129
+ .command("rotate")
130
+ .description("Пролить GITLAB_PRIVATE_TOKEN в CI variables через owner PAT")
131
+ .option("-d, --dry", "Запустить только предпроверки")
132
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
133
+ .action((command) => runAction(token.tokenRotate, command));
134
+ }
135
+
136
+ module.exports = { registerCommands };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const { getPackageManager, execCommand, logError } = require('./utils');
2
+ const { getPackageManager, execCommand, logError } = require('../common/utils');
3
3
 
4
4
  function dev() {
5
5
  const pm = getPackageManager();
@@ -0,0 +1,55 @@
1
+ function configureHelp(program) {
2
+ // Переопределяем help, чтобы показать кастомный формат с алиасами
3
+ program.configureHelp({
4
+ formatHelp: (cmd, helper) => {
5
+ const cmdUsage = helper.commandUsage(cmd);
6
+ const cmdDescription = helper.commandDescription(cmd);
7
+ const options = helper.visibleOptions(cmd);
8
+ const commands = helper.visibleCommands(cmd);
9
+
10
+ let output = "";
11
+
12
+ // Использование
13
+ if (cmdUsage) {
14
+ output += `Использование: ${cmdUsage}\n\n`;
15
+ }
16
+
17
+ // Описание
18
+ if (cmdDescription) {
19
+ output += `${cmdDescription}\n\n`;
20
+ }
21
+
22
+ // Команды с алиасами
23
+ if (commands.length > 0) {
24
+ output += "Команды:\n";
25
+ commands.forEach((cmd) => {
26
+ const nameAndArgs = cmd.name() + cmd.usage().replace(/^[^\s]+\s*/, " ");
27
+ const aliases = cmd.aliases();
28
+ const description = cmd.description();
29
+
30
+ let cmdLine = ` ${nameAndArgs}`;
31
+ if (aliases.length > 0) {
32
+ cmdLine = cmdLine.padEnd(25) + `[${aliases.join(", ")}]`;
33
+ }
34
+ cmdLine = cmdLine.padEnd(40) + description;
35
+ output += cmdLine + "\n";
36
+ });
37
+ output += "\n";
38
+ }
39
+
40
+ // Опции
41
+ if (options.length > 0) {
42
+ output += "Опции:\n";
43
+ options.forEach((option) => {
44
+ const flags = helper.optionTerm(option);
45
+ const description = helper.optionDescription(option);
46
+ output += ` ${flags.padEnd(25)} ${description}\n`;
47
+ });
48
+ }
49
+
50
+ return output;
51
+ },
52
+ });
53
+ }
54
+
55
+ module.exports = { configureHelp };
@@ -0,0 +1,28 @@
1
+ const { checkForUpdates } = require('./update-check');
2
+ const { withCommandOptions } = require('./command-executor');
3
+
4
+ function commandOptions(command) {
5
+ return command && typeof command.opts === "function" ? command.opts() : command || {};
6
+ }
7
+
8
+ async function runAction(action, command) {
9
+ const options = commandOptions(command);
10
+ if (!options.dry) {
11
+ try {
12
+ await checkForUpdates();
13
+ } catch {
14
+ // A failed update check must not affect the command.
15
+ }
16
+ }
17
+ try {
18
+ const result = await withCommandOptions(options, () => action(options));
19
+ if (!result) {
20
+ process.exit(1);
21
+ }
22
+ } catch (error) {
23
+ console.error(`❌ Ошибка: ${error.message}`);
24
+ process.exit(1);
25
+ }
26
+ }
27
+
28
+ module.exports = { runAction };
@@ -0,0 +1,51 @@
1
+ const childProcess = require('child_process');
2
+ const { name, version: currentVersion } = require('../../package.json');
3
+ const { compareVersions } = require('../common/version');
4
+ const { isDryRun, reportDryRun, reportNoPreflights } = require('./command-executor');
5
+
6
+ const UPDATE_CHECK_TIMEOUT_MS = 200;
7
+
8
+ async function checkForUpdates() {
9
+ const controller = new AbortController();
10
+ let timer;
11
+ try {
12
+ const timeout = new Promise((resolve) => {
13
+ timer = setTimeout(() => {
14
+ controller.abort();
15
+ resolve(null);
16
+ }, UPDATE_CHECK_TIMEOUT_MS);
17
+ });
18
+ const request = (async () => {
19
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, {
20
+ signal: controller.signal,
21
+ headers: { accept: 'application/json' }
22
+ });
23
+ if (!response.ok) return null;
24
+ return (await response.json()).version;
25
+ })();
26
+ const latestVersion = await Promise.race([request, timeout]);
27
+ if (compareVersions(latestVersion, currentVersion) > 0) {
28
+ console.log(`⚠️ Доступна новая версия Spectrum CLI: сейчас ${currentVersion}, доступна ${latestVersion}.\nОбновить: spectrum upgrade`);
29
+ }
30
+ } catch {
31
+ // Network and registry errors must never block a command.
32
+ } finally {
33
+ clearTimeout(timer);
34
+ }
35
+ }
36
+
37
+ function upgrade() {
38
+ reportNoPreflights('upgrade');
39
+ if (isDryRun()) {
40
+ reportDryRun('upgrade');
41
+ return true;
42
+ }
43
+ const result = childProcess.spawnSync('npm', ['install', '-g', name], {
44
+ stdio: 'inherit',
45
+ shell: process.platform === 'win32'
46
+ });
47
+ if (result.error) throw result.error;
48
+ return result.status === 0;
49
+ }
50
+
51
+ module.exports = { checkForUpdates, upgrade, UPDATE_CHECK_TIMEOUT_MS };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- const { logSuccess, execCommand, getCurrentBranch, getMainBranch, getDevelopBranch } = require('./utils');
3
- const { runCommand } = require('./command-executor');
2
+ const { logSuccess, execCommand, getCurrentBranch, getMainBranch, getDevelopBranch } = require('../common/utils');
3
+ const { runCommand } = require('../cli/command-executor');
4
4
  const {
5
5
  requireGitRepo,
6
6
  requireCleanWorkingTree,
@@ -8,7 +8,7 @@ const {
8
8
  requireCurrentBranchUpToDateWithRemote,
9
9
  requireChangelogReleaseVersion,
10
10
  requireTagMissing
11
- } = require('./preflight');
11
+ } = require('../preflight');
12
12
 
13
13
  function goToMainBranch() {
14
14
  const mainBranch = getMainBranch();
@@ -89,6 +89,7 @@ function gitCreateTagAndPush() {
89
89
  { name: 'changelog-release-version', run: requireChangelogReleaseVersion },
90
90
  {
91
91
  name: 'tag-missing',
92
+ requires: ['version'],
92
93
  run: (ctx) => requireTagMissing(`release/${ctx.version}`)
93
94
  }
94
95
  ],