spectrum-cli 5.0.2 → 5.1.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 CHANGED
@@ -9,6 +9,19 @@ This is the changelog of the `spectrum-cli` npm package. Its version comes from
9
9
  `package.json`; the release commands use Git tags and `CHANGELOG.md` in the
10
10
  **target project** and do not update that project's `package.json`.
11
11
 
12
+ ## 🚀 [5.1.0]
13
+
14
+ ### Changed
15
+
16
+ - Deployment-адреса и GitLab-проекты задаются окружением: встроенные URL/namespace удалены, примеры и fixtures используют example-домены. Добавлены preflight наличия настроек, доступности, GitLab token scope/роли, YouTrack identity/проектов/полей и registry login/чтения. Права записи не объявляются доказанными по успешному GET.
17
+
18
+ ### Added
19
+
20
+ - Все новые automation-команды поддерживают `-d, --dry` и `-s, --silence`: dry выполняет только предпроверки без изменения исходников, отчётов и удалённых сущностей; silence скрывает успешные предпроверки, сохраняя ошибки и результаты.
21
+
22
+ - Команды `nextjs licenses`, `sbom catalog resolve/populate` и `security scan/track/reconcile/repair` перенесены из CI-компонентов в JavaScript-модули CLI. Сохранены OCI provenance/integrity, CVE и проектные задачи, Priority, изоляция проверок, проверки исправлений и отчёты для Mattermost.
23
+ - Автоматизация работает без интерактивных запросов и проверки обновлений; версии CLI в CI закрепляются служебными образами. Python для новых команд не требуется.
24
+
12
25
  ## 🚀 [5.0.2] - 2026-10-23
13
26
 
14
27
  ### Changed
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install -g spectrum-cli
14
14
  spectrum --help
15
15
  ```
16
16
 
17
- Перед выполнением каждой команды CLI проверяет опубликованную версию `spectrum-cli`
17
+ Перед выполнением команд workflow CLI проверяет опубликованную версию `spectrum-cli`
18
18
  в npm Registry. Если доступна новая версия, показывает текущую и новую версии и
19
19
  команду `spectrum upgrade`. Она запускает `npm install -g spectrum-cli`.
20
20
  Проверка ограничена 200 мс; недоступность Registry не влияет на выполнение
@@ -476,3 +476,134 @@ MIT License - см. [LICENSE](./LICENSE)
476
476
  ---
477
477
 
478
478
  _Создано для автоматизации повседневных задач разработки_ 🛠️
479
+
480
+ ## Команды для CI: Next.js, SBOM и security
481
+
482
+ Начиная с 5.1.0 логика команд написана на JavaScript и входит в npm-пакет.
483
+ Команды не задают вопросов и не проверяют обновления CLI. Они не запускают
484
+ release/hotfix preflight, рассчитанный на интерактивную работу разработчика.
485
+
486
+ | Команда | Назначение | Результат |
487
+ | --- | --- | --- |
488
+ | `spectrum nextjs licenses` | Сохранить исходные LICENSE уже включённых compiled-пакетов Next.js | JSON summary в stdout; код 2 при неполном результате |
489
+ | `spectrum sbom catalog resolve` | Найти Next в immutable образе, проверить OCI-каталоги, запросить отсутствующие версии | `sbom-catalog/<prefix>/{selection.json,report.json,report.md}` |
490
+ | `spectrum sbom catalog populate` | Построить каталог по официальному npm tarball и историческому Git tree | `sbom-catalog-updater/{report.json,report.md}` |
491
+ | `spectrum security scan` | Проверить `npm audit --omit=dev`, runtime и фактическую версию Node | `security-report/findings.json`, raw evidence, `scan.env` |
492
+ | `spectrum security reconcile` | Запустить центральный tracker и получить ссылки на задачи | `security-report/tracked.json` |
493
+ | `spectrum security track` | Найти/создать общие CVE и проектные задачи | `security-report/tracked.json` в центральном pipeline |
494
+ | `spectrum security repair` | Исправить одну проектную задачу за MR, выполнить проверки и повторный аудит | `security-report/{final.json,message.json}` |
495
+
496
+ Код 0 означает успешное выполнение, 1 — ошибку security/запуска, 2 — неполный
497
+ результат SBOM или LICENSE. Команды работают в текущем каталоге. Отсутствующие
498
+ credentials/некорректные отчёты приводят к ошибке, а не к чистому аудиту.
499
+ Автоматическое слияние MR отключено; сетевые записи не повторяются вслепую.
500
+
501
+ Компоненты передают свои inputs через job-local окружение. `security scan`
502
+ читает `NIGHTLY_YOUTRACK_PROJECT`, `NIGHTLY_YOUTRACK_TIME_TRACKING_ARTICLE`,
503
+ `NIGHTLY_YOUTRACK_TESTER`, необязательный `NIGHTLY_YOUTRACK_SUBSYSTEM`,
504
+ `NIGHTLY_DOCKERFILE`, `NIGHTLY_PLATFORMS` и `NIGHTLY_CHECK_COMMANDS` (JSON argv).
505
+ Токены `GLAB_BOT_PAT` и `YOUTRACK_PRIVATE_TOKEN` остаются секретными CI variables;
506
+ YouTrack-токен нужен только центральному tracker. Для registry используются
507
+ `NEXUS_USER`/`NEXUS_PASSWORD` либо стандартные CI registry credentials.
508
+ Секреты автоматизации исключаются из окружения выполняемых проверок проекта.
509
+
510
+ `sbom catalog resolve` читает `SBOM_CATALOG_CLIENT_ENABLED=true`,
511
+ `SBOM_CATALOG_FRAMEWORK=nextjs`, `SBOM_CATALOG_PREFIX` (по умолчанию `site:`),
512
+ `SBOM_CATALOG_REPOSITORY`, `DOCKER_IMAGE`, `APP_VERSION` и стандартные GitLab
513
+ `CI_*` variables. Чтение каталога использует `SBOM_CATALOG_USER` и
514
+ `SBOM_CATALOG_PASSWORD`, если заданы. `populate` требует защищённый `main`,
515
+ `NEXT_CATALOG_VERSIONS` (JSON-массив 1..10 точных версий) и центральные
516
+ `NEXUS_USER`/`NEXUS_PASSWORD`. Namespace записи задаёт только доверенная
517
+ project variable `SBOM_CATALOG_PUBLISH_REPOSITORY`.
518
+
519
+ Внешние инструменты: Linux amd64 для SBOM bootstrap; Git для каталога,
520
+ Trivy/Skopeo/Buildah для runtime-аудита и исправлений. SBOM-команды скачивают
521
+ закреплённые ORAS/Syft с проверкой SHA-256, ограничениями размера и времени.
522
+ Python для этих команд не требуется. Они не выполняют код из npm tarball
523
+ или upstream build-рецептов. Неоднозначное происхождение остаётся unresolved.
524
+ Поля CVE, Severity, Priority и Type=Group проверяются до создания подзадач;
525
+ CRITICAL получает Stopper (Неотложная), HIGH — Critical (Критическая).
526
+
527
+ Образы и адреса сервисов задаёт CI-конфигурация конкретной установки.
528
+ CLI не содержит дефолтов для YouTrack, registry или внутренних GitLab-проектов.
529
+ Само наличие команды `track` не сериализует процессы: job должен оставаться
530
+ в одном центральном проекте с `resource_group: nightly-youtrack-global`.
531
+ Отдельный consumer lock `nightly-security-fix` защищает исправления проекта.
532
+
533
+ Для проверки миграции:
534
+
535
+ ```sh
536
+ npm ci
537
+ npm test -- --runInBand
538
+ npm pack --dry-run
539
+ ```
540
+
541
+ Тесты используют локальные Git-репозитории и fake API. Зафиксированные parity
542
+ fixtures получены из прежнего Python-кода; исполняемый Python в пакет не входит.
543
+ Публикация CLI, образов, компонентов и запуск живого pipeline — отдельные шаги.
544
+
545
+ ### Preflight для команд автоматизации
546
+
547
+ Обязательные deployment-настройки задаются явно:
548
+
549
+ | Команда | Настройки |
550
+ | --- | --- |
551
+ | `security track` | `YOUTRACK_URL`, `YOUTRACK_PRIVATE_TOKEN`, `CI_API_V4_URL`, `GLAB_BOT_PAT`, идентификаторы consumer job/project и component SHA |
552
+ | `security reconcile` | `NIGHTLY_TRACKER_PROJECT`, `NIGHTLY_COMPONENT_SHA`, `NIGHTLY_SCAN_JOB_ID`, `CI_PROJECT_ID`, GitLab API/token; при пустом отчёте сеть не нужна |
553
+ | `security repair` | GitLab API/token и результаты scan/tracking; при пустом отчёте запись и проверка токена не нужны |
554
+ | `sbom catalog resolve` | `SBOM_CATALOG_REPOSITORY`, `DOCKER_IMAGE`, `APP_VERSION`; `SBOM_CATALOG_UPDATER_PROJECT` для запроса отсутствующих версий |
555
+ | `sbom catalog populate` | `SBOM_CATALOG_PUBLISH_REPOSITORY`, `NEXUS_USER`, `NEXUS_PASSWORD`, защищённый main и точные `NEXT_CATALOG_VERSIONS` |
556
+ | `security scan/repair` с `NEXUS_*` | Дополнительно явный `NIGHTLY_REGISTRY_HOSTS`; один логин без пароля или наоборот считается ошибкой |
557
+
558
+ В GitLab выполняются GET `/user`, `/personal_access_tokens/self` и чтение
559
+ целевых проектов. Проверяются активность токена, scope `api` для записи
560
+ (`api`/`read_api` для чтения), эффективная роль Developer/Reporter и состояние
561
+ проекта. Fine-grained/прочие токены, для которых introspection не подтверждает
562
+ эти scope, не считаются проверенными. Защита веток/тегов дополнительно
563
+ проверяется сервером при конкретной операции.
564
+
565
+ В YouTrack выполняются GET `/users/me`, чтение двух проектов, их полей и
566
+ поиск задач. До создания первой задачи проверяются активность пользователя,
567
+ доступность проектов и обязательные поля. Ответы 401/403, недоступность и
568
+ неверный TLS приводят к ошибке с названием проверки; секреты/тела ответов
569
+ не выводятся. Инспекция полей не требует тестового изменения проекта.
570
+
571
+ Writer выполняет ORAS login и чтение точного namespace до скачивания исходников
572
+ и построения каталога. Preflight сохраняется в отчёте. Чтение/логин не доказывают
573
+ права Create/Update/Link Issue, доступ к приватным полям, workflow-переходам или
574
+ registry push: эти права проверяются соответствующими операциями. Они явно
575
+ помечены `checked_by_operations`/`checked_by_publish`, без ложного статуса
576
+ «запись проверена». Preflight не создаёт задач, MR, upload-сессий или пробных
577
+ каталогов. Ошибка preflight останавливает следующие изменения и остаётся
578
+ в JSON/Markdown/notification-артефактах, когда исходный отчёт уже доступен.
579
+
580
+ Источник контрактов: [GitLab token scopes](https://docs.gitlab.com/security/tokens/access_token_scopes/),
581
+ [YouTrack My User Profile](https://www.jetbrains.com/help/youtrack/devportal/resource-api-users-me.html),
582
+ [YouTrack project fields](https://www.jetbrains.com/help/youtrack/devportal/resource-api-admin-projects-projectID-customFields.html).
583
+
584
+ ### Dry и silence для команд автоматизации
585
+
586
+ Все семь команд `nextjs licenses`, `sbom catalog resolve/populate` и
587
+ `security scan/track/reconcile/repair` поддерживают `-d, --dry` и
588
+ `-s, --silence`, включая сочетание `-ds`.
589
+
590
+ ```sh
591
+ spectrum nextjs licenses --dry
592
+ spectrum sbom catalog populate --dry --silence
593
+ spectrum security repair -ds
594
+ ```
595
+
596
+ `--dry` выполняет предпроверки настроек, файлов, инструментов и доступов,
597
+ после чего останавливается перед действиями команды. Он не копирует LICENSE,
598
+ не выполняет npm audit/Trivy/Syft scan или исправления, не создаёт задачи,
599
+ MR и downstream pipeline, не публикует каталоги и не перезаписывает отчёты.
600
+ Для проверки используются GET-запросы, чтение image descriptor и metadata
601
+ Git/проектов; SBOM preflight может загрузить закреплённые инструменты во
602
+ временную папку и выполнить registry login с временным auth-файлом.
603
+ Временные файлы удаляются; исходники, lockfiles и существующие артефакты
604
+ сохраняются. При неполных входных отчётах dry завершается с ошибкой.
605
+
606
+ `--silence` скрывает только сообщения об успешно пройденных предпроверках.
607
+ Ошибки, предупреждения, результаты команды и итог dry остаются видимыми.
608
+ Это тот же смысл флага, что у release/hotfix команд. Без dry выполняются
609
+ обычные действия и сохраняются прежние коды завершения 0/1/2.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrum-cli",
3
- "version": "5.0.2",
3
+ "version": "5.1.0",
4
4
  "description": "🚀 Modern CLI for development workflow automation - release management, versioning, and changelog generation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -49,7 +49,9 @@
49
49
  "url": "https://github.com/dnwSilver/spectrum-cli/issues"
50
50
  },
51
51
  "dependencies": {
52
- "commander": "^11.1.0"
52
+ "commander": "^11.1.0",
53
+ "semver": "7.8.5",
54
+ "yaml": "2.9.1"
53
55
  },
54
56
  "engines": {
55
57
  "node": ">=20.0.0",
@@ -0,0 +1,297 @@
1
+ // Shared bounded IO for noninteractive CI commands. Never execute shell strings.
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const crypto = require("node:crypto");
6
+ const { spawnSync } = require("node:child_process");
7
+ const { gunzipSync } = require("node:zlib");
8
+ const { isDeepStrictEqual } = require("node:util");
9
+
10
+ const SEMVER =
11
+ /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?![\s\S])/;
12
+ const DIGEST = /^sha256:[a-f0-9]{64}(?![\s\S])/;
13
+ const HEX64 = /^[a-f0-9]{64}(?![\s\S])/;
14
+ const COMMIT = /^[a-f0-9]{40}(?![\s\S])/;
15
+ function ensure(condition, message) {
16
+ if (!condition) throw new Error(message);
17
+ }
18
+ function sha(data, algorithm = "sha256") {
19
+ return crypto.createHash(algorithm).update(data).digest("hex");
20
+ }
21
+ function sorted(value) {
22
+ if (Array.isArray(value)) return value.map(sorted);
23
+ if (value && typeof value === "object" && !Buffer.isBuffer(value))
24
+ return Object.fromEntries(
25
+ Object.keys(value)
26
+ .sort()
27
+ .map((key) => [key, sorted(value[key])]),
28
+ );
29
+ return value;
30
+ }
31
+ function stable(value) {
32
+ return JSON.stringify(sorted(value));
33
+ }
34
+ function json(value) {
35
+ try {
36
+ return JSON.parse(Buffer.isBuffer(value) ? value.toString("utf8") : value);
37
+ } catch {
38
+ throw new Error("Invalid JSON data");
39
+ }
40
+ }
41
+ function read(file) {
42
+ return json(fs.readFileSync(file));
43
+ }
44
+ function save(file, value) {
45
+ fs.mkdirSync(path.dirname(file), { recursive: true });
46
+ const tmp = file + ".tmp";
47
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n");
48
+ fs.renameSync(tmp, file);
49
+ }
50
+ function temp(prefix) {
51
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
52
+ }
53
+ function remove(directory) {
54
+ fs.rmSync(directory, { recursive: true, force: true });
55
+ }
56
+ async function temporary(prefix, action) {
57
+ const dir = temp(prefix);
58
+ try {
59
+ return await action(dir);
60
+ } finally {
61
+ remove(dir);
62
+ }
63
+ }
64
+ function safePath(value, absolute = false) {
65
+ return (
66
+ typeof value === "string" &&
67
+ !!value &&
68
+ !/[\\\x00-\x1f]/.test(value) &&
69
+ value.startsWith("/") === absolute &&
70
+ value
71
+ .replace(/^\//, "")
72
+ .split("/")
73
+ .every((part) => part && part !== "." && part !== "..")
74
+ );
75
+ }
76
+ function repositoryPath(root, relative) {
77
+ ensure(safePath(relative), "Expected a repository-relative path");
78
+ const base = fs.realpathSync(root);
79
+ const target = path.resolve(base, relative);
80
+ let cursor = base;
81
+ for (const part of relative.split("/")) {
82
+ cursor = path.join(cursor, part);
83
+ if (
84
+ fs.existsSync(cursor) ||
85
+ (() => {
86
+ try {
87
+ fs.lstatSync(cursor);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ })()
93
+ ) {
94
+ const real = fs.realpathSync(cursor);
95
+ ensure(real.startsWith(base + path.sep), "Path leaves repository");
96
+ }
97
+ }
98
+ return target;
99
+ }
100
+ function exec(
101
+ args,
102
+ {
103
+ cwd,
104
+ env = process.env,
105
+ input,
106
+ allowed = [0],
107
+ timeout = 1800000,
108
+ maxBuffer = 32 * 1024 * 1024,
109
+ binary = false,
110
+ label,
111
+ } = {},
112
+ ) {
113
+ ensure(
114
+ Array.isArray(args) &&
115
+ args.length &&
116
+ args.every((arg) => typeof arg === "string" && !arg.includes("\0")),
117
+ "Expected command argv",
118
+ );
119
+ const result = spawnSync(args[0], args.slice(1), {
120
+ cwd,
121
+ env,
122
+ input,
123
+ timeout,
124
+ maxBuffer,
125
+ encoding: binary ? null : "utf8",
126
+ windowsHide: true,
127
+ });
128
+ ensure(
129
+ !result.error && allowed.includes(result.status),
130
+ `${label || path.basename(args[0]) + " " + (args[1] || "")} failed (exit ${result.status})`,
131
+ );
132
+ return {
133
+ stdout: result.stdout,
134
+ stderr: result.stderr,
135
+ status: result.status,
136
+ };
137
+ }
138
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
139
+ async function responseBytes(response, limit) {
140
+ const chunks = [];
141
+ let size = 0;
142
+ for await (const chunk of response.body || []) {
143
+ size += chunk.length;
144
+ if (size > limit) {
145
+ await response.body?.cancel?.().catch(() => {});
146
+ throw new Error("Download limit exceeded");
147
+ }
148
+ chunks.push(Buffer.from(chunk));
149
+ }
150
+ return Buffer.concat(chunks);
151
+ }
152
+ async function request(
153
+ url,
154
+ {
155
+ method = "GET",
156
+ headers = {},
157
+ body,
158
+ limit = 32 * 1024 * 1024,
159
+ timeout = 60000,
160
+ fetchImpl = fetch,
161
+ } = {},
162
+ ) {
163
+ const controller = new AbortController();
164
+ const timer = setTimeout(() => controller.abort(), timeout);
165
+ try {
166
+ const response = await fetchImpl(url, {
167
+ method,
168
+ headers,
169
+ body,
170
+ redirect: "manual",
171
+ signal: controller.signal,
172
+ });
173
+ const bytes = await responseBytes(response, limit);
174
+ return { status: response.status, headers: response.headers, body: bytes };
175
+ } finally {
176
+ clearTimeout(timer);
177
+ }
178
+ }
179
+ // Extract data only. No archive entry is ever written to the filesystem.
180
+ function tarEntries(
181
+ gzip,
182
+ {
183
+ maxUnpacked = 1024 * 1024 * 1024,
184
+ maxFile = 32 * 1024 * 1024,
185
+ maxEntries = 30000,
186
+ } = {},
187
+ ) {
188
+ const bytes = gunzipSync(gzip, {
189
+ maxOutputLength: maxUnpacked + maxEntries * 1024,
190
+ });
191
+ const entries = [];
192
+ let offset = 0;
193
+ let total = 0;
194
+ let pax = {};
195
+ let longName;
196
+ const str = (data) => data.toString("utf8").replace(/\0.*(?![\s\S])/s, "");
197
+ while (offset + 512 <= bytes.length) {
198
+ const header = bytes.subarray(offset, offset + 512);
199
+ offset += 512;
200
+ if (header.every((byte) => byte === 0)) break;
201
+ const sizeText = str(header.subarray(124, 136)).trim();
202
+ ensure(/^[0-7]+(?![\s\S])/.test(sizeText), "Invalid tar entry size");
203
+ const size = parseInt(sizeText, 8);
204
+ const expected = parseInt(str(header.subarray(148, 156)).trim(), 8);
205
+ const sum = header.reduce(
206
+ (sum, b, i) => sum + (i >= 148 && i < 156 ? 32 : b),
207
+ 0,
208
+ );
209
+ ensure(
210
+ expected === sum && size <= maxFile && offset + size <= bytes.length,
211
+ "Invalid or oversized tar entry",
212
+ );
213
+ const data = bytes.subarray(offset, offset + size);
214
+ offset += Math.ceil(size / 512) * 512;
215
+ const type = String.fromCharCode(header[156] || 48);
216
+ if (type === "x") {
217
+ pax = {};
218
+ let pos = 0;
219
+ while (pos < data.length) {
220
+ const space = data.indexOf(32, pos);
221
+ ensure(space > pos, "Invalid PAX record");
222
+ const len = Number(data.subarray(pos, space).toString());
223
+ ensure(
224
+ Number.isInteger(len) &&
225
+ len > space - pos + 1 &&
226
+ pos + len <= data.length &&
227
+ data[pos + len - 1] === 10,
228
+ "Invalid PAX length",
229
+ );
230
+ const row = data.subarray(space + 1, pos + len - 1).toString();
231
+ const split = row.indexOf("=");
232
+ ensure(split > 0, "Invalid PAX value");
233
+ pax[row.slice(0, split)] = row.slice(split + 1);
234
+ pos += len;
235
+ }
236
+ continue;
237
+ }
238
+ if (type === "L") {
239
+ longName = str(data).replace(/\n(?![\s\S])/, "");
240
+ continue;
241
+ }
242
+ const prefix =
243
+ str(header.subarray(257, 263)) === "ustar"
244
+ ? str(header.subarray(345, 500))
245
+ : "";
246
+ const name =
247
+ pax.path ||
248
+ longName ||
249
+ (prefix ? prefix + "/" : "") + str(header.subarray(0, 100));
250
+ ensure(
251
+ pax.size === undefined || Number(pax.size) === size,
252
+ "Unsupported PAX size",
253
+ );
254
+ pax = {};
255
+ longName = undefined;
256
+ ensure(
257
+ type === "0" || type === "5",
258
+ "Links and special archive entries are forbidden",
259
+ );
260
+ total += size;
261
+ ensure(
262
+ total <= maxUnpacked && entries.length < maxEntries,
263
+ "Archive expansion limit exceeded",
264
+ );
265
+ entries.push({
266
+ name: name.replace(/\/(?![\s\S])/, ""),
267
+ directory: type === "5",
268
+ data,
269
+ });
270
+ }
271
+ return entries;
272
+ }
273
+ module.exports = {
274
+ fs,
275
+ path,
276
+ SEMVER,
277
+ DIGEST,
278
+ HEX64,
279
+ COMMIT,
280
+ ensure,
281
+ sha,
282
+ stable,
283
+ sorted,
284
+ json,
285
+ read,
286
+ save,
287
+ temp,
288
+ remove,
289
+ temporary,
290
+ safePath,
291
+ repositoryPath,
292
+ exec,
293
+ sleep,
294
+ request,
295
+ tarEntries,
296
+ equal: isDeepStrictEqual,
297
+ };
@@ -4,9 +4,12 @@ const hotfix = require('../hotfix');
4
4
  const changelog = require('../changelog');
5
5
  const chart = require('../chart');
6
6
  const token = require('../token');
7
+ const nextjs = require('../nextjs');
8
+ const sbom = require('../sbom');
9
+ const security = require('../security');
7
10
  const { upgrade } = require('./update-check');
8
11
  const { version: pkgVersion } = require('../../package.json');
9
- const { runAction } = require('./run-action');
12
+ const { runAction, runAutomationAction } = require('./run-action');
10
13
 
11
14
  function registerCommands(program) {
12
15
  program
@@ -21,6 +24,81 @@ program
21
24
  .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
22
25
  .action((command) => runAction(upgrade, command));
23
26
 
27
+ // Команды Next.js
28
+ const nextjsCmd = program
29
+ .command("nextjs")
30
+ .description("Обработка Next.js build artifacts");
31
+
32
+ nextjsCmd
33
+ .command("licenses")
34
+ .description("Сохранить LICENSE встроенных пакетов в standalone")
35
+ .allowExcessArguments(false)
36
+ .option("-d, --dry", "Запустить только предпроверки")
37
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
38
+ .action((command) => runAutomationAction(nextjs.licenses, command));
39
+
40
+ // Команды SBOM
41
+ const sbomCmd = program
42
+ .command("sbom")
43
+ .description("Работа с SBOM");
44
+
45
+ const catalogCmd = sbomCmd
46
+ .command("catalog")
47
+ .description("Каталоги встроенных зависимостей Next.js");
48
+
49
+ catalogCmd
50
+ .command("resolve")
51
+ .description("Найти проверенные OCI-каталоги для образа приложения")
52
+ .allowExcessArguments(false)
53
+ .option("-d, --dry", "Запустить только предпроверки")
54
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
55
+ .action((command) => runAutomationAction(sbom.resolve, command));
56
+
57
+ catalogCmd
58
+ .command("populate")
59
+ .description("Построить и опубликовать каталог из официальных исходников")
60
+ .allowExcessArguments(false)
61
+ .option("-d, --dry", "Запустить только предпроверки")
62
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
63
+ .action((command) => runAutomationAction(sbom.populate, command));
64
+
65
+ // Команды security
66
+ const securityCmd = program
67
+ .command("security")
68
+ .description("Ночная проверка и исправление HIGH/CRITICAL");
69
+
70
+ securityCmd
71
+ .command("scan")
72
+ .description("Проверить production npm-зависимости и runtime-образ")
73
+ .allowExcessArguments(false)
74
+ .option("-d, --dry", "Запустить только предпроверки")
75
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
76
+ .action((command) => runAutomationAction(security.scan, command));
77
+
78
+ securityCmd
79
+ .command("track")
80
+ .description("Создать или найти YouTrack-задачи в центральном pipeline")
81
+ .allowExcessArguments(false)
82
+ .option("-d, --dry", "Запустить только предпроверки")
83
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
84
+ .action((command) => runAutomationAction(security.track, command));
85
+
86
+ securityCmd
87
+ .command("reconcile")
88
+ .description("Дождаться центрального tracker и получить номера задач")
89
+ .allowExcessArguments(false)
90
+ .option("-d, --dry", "Запустить только предпроверки")
91
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
92
+ .action((command) => runAutomationAction(security.reconcile, command));
93
+
94
+ securityCmd
95
+ .command("repair")
96
+ .description("Исправить, проверить и создать отдельный MR на проектную задачу")
97
+ .allowExcessArguments(false)
98
+ .option("-d, --dry", "Запустить только предпроверки")
99
+ .option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
100
+ .action((command) => runAutomationAction(security.repair, command));
101
+
24
102
  // Команды релиза
25
103
  const releaseCmd = program
26
104
  .command("release")
@@ -1,5 +1,5 @@
1
1
  const { checkForUpdates } = require('./update-check');
2
- const { withCommandOptions } = require('./command-executor');
2
+ const { withCommandOptions, reportDryRun } = require('./command-executor');
3
3
 
4
4
  function commandOptions(command) {
5
5
  return command && typeof command.opts === "function" ? command.opts() : command || {};
@@ -25,4 +25,20 @@ async function runAction(action, command) {
25
25
  }
26
26
  }
27
27
 
28
- module.exports = { runAction };
28
+ // Automation handlers return numeric exit codes, including 0 (success)
29
+ // and 2 (partial result). They run without the interactive update check.
30
+ async function runAutomationAction(action, command) {
31
+ const options = commandOptions(command);
32
+ try {
33
+ process.exitCode = await withCommandOptions(options, async () => {
34
+ const code = await action(options);
35
+ if (options.dry && code === 0) reportDryRun();
36
+ return code;
37
+ });
38
+ } catch (error) {
39
+ console.error('ERROR: ' + error.message);
40
+ process.exitCode = 1;
41
+ }
42
+ }
43
+
44
+ module.exports = { runAction, runAutomationAction };