@mirta/cli 0.3.5 → 0.4.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/dist/index.mjs CHANGED
@@ -1,23 +1,761 @@
1
- import { g as getLocalized, S as ShellError, u as useLogger } from './shell.mjs';
2
- import { G as GitError, a as GithubError, W as WorkflowStatusError, P as PromptCanceledError } from './github.mjs';
3
- import 'node:child_process';
4
- import 'node:url';
5
- import 'node:fs';
6
- import 'node:path';
7
- import 'lodash.merge';
8
- import 'chalk';
9
- import 'prompts';
10
-
11
- const messages = await getLocalized();
12
- const logger = useLogger(messages);
1
+ import p from 'prompts';
2
+ import { createStagedArgs } from '@mirta/staged-args';
3
+ import { spawn } from 'node:child_process';
4
+ import chalk from 'chalk';
5
+ import { initLocalizationAsync } from '@mirta/i18n';
6
+ import { resolve } from 'node:path';
7
+ import cliPackage from '../package.json' with { type: 'json' };
8
+ import { suggestClosest } from '@mirta/basics/fuzzy';
9
+
10
+ class PromptCanceledError extends Error {
11
+ constructor() {
12
+ super();
13
+ // Убедимся, что экземпляр имеет правильный прототип
14
+ Object.setPrototypeOf(this, PromptCanceledError.prototype);
15
+ this.name = 'PromptCanceledError';
16
+ Error.captureStackTrace(this, PromptCanceledError);
17
+ }
18
+ }
19
+ /**
20
+ * @param {import('prompts').PromptObject<string> | Array<import('prompts').PromptObject<string>>} questions
21
+ * @param {import('prompts').Options} options
22
+ */
23
+ async function prompts(questions, options) {
24
+ const po = {
25
+ onCancel: () => {
26
+ throw new PromptCanceledError();
27
+ },
28
+ };
29
+ return await p(questions, po);
30
+ }
31
+
32
+ const { t, getLocale, setLocaleAsync } = await initLocalizationAsync({
33
+ cwd: resolve(import.meta.dirname, '../'),
34
+ });
35
+
36
+ /**
37
+ * Символ-разделитель, используемый в префиксе логов.
38
+ *
39
+ * @since 0.3.0
40
+ *
41
+ **/
42
+ const dot = '•';
43
+ /**
44
+ * Баннер, отображаемый в начале логов по умолчанию.
45
+ *
46
+ * @since 0.3.0
47
+ *
48
+ **/
49
+ const banner = `Mirta ${dot}`;
50
+ /**
51
+ * Цвета текста для каждого уровня логирования.
52
+ *
53
+ * @since 0.4.0
54
+ *
55
+ **/
56
+ const colors = {
57
+ debug: chalk.magenta,
58
+ info: chalk.cyan,
59
+ warn: chalk.yellow,
60
+ error: chalk.red,
61
+ success: chalk.green,
62
+ cancel: chalk.red,
63
+ step: chalk.dim,
64
+ note: chalk.yellowBright,
65
+ };
66
+ /**
67
+ * Цвета фона для "pill" (подсветки метки уровня).
68
+ *
69
+ * @since 0.4.0
70
+ *
71
+ **/
72
+ const bgColors = {
73
+ debug: chalk.bgMagenta.black,
74
+ info: chalk.bgCyan.black,
75
+ warn: chalk.bgYellow.black,
76
+ error: chalk.bgRed.white,
77
+ success: chalk.bgGreen.black,
78
+ cancel: chalk.bgRed,
79
+ };
80
+ /**
81
+ * Приоритет уровней логирования. Определяет, какие сообщения будут отображаться
82
+ * при установленном уровне детализации.
83
+ *
84
+ * @since 0.4.0
85
+ *
86
+ **/
87
+ const levelPriority = [
88
+ 'debug',
89
+ 'info',
90
+ 'warn',
91
+ 'error',
92
+ 'success',
93
+ 'cancel',
94
+ 'step',
95
+ 'note',
96
+ ];
97
+ /**
98
+ * Целевой уровень логирования. Сообщения с уровнем ниже указанного — игнорируются.
99
+ *
100
+ * @since 0.4.0
101
+ *
102
+ **/
103
+ let targetLevel = 0;
104
+ /**
105
+ * Проверяет, должно ли сообщение быть залогировано, исходя из текущего уровня.
106
+ *
107
+ * @param level - Уровень логирования сообщения.
108
+ * @returns {boolean} `true`, если сообщение удовлетворяет текущему уровню детализации.
109
+ *
110
+ * @since 0.4.0
111
+ *
112
+ **/
113
+ function shouldLog(level) {
114
+ const currentLevel = levelPriority.indexOf(level);
115
+ return currentLevel === -1 || currentLevel >= targetLevel;
116
+ }
117
+ /**
118
+ * Создаёт функцию для формирования "pill" — цветной метки с названием уровня.
119
+ *
120
+ * @param level - Уровень логирования.
121
+ * @returns Функция, возвращающая отформатированную метку.
122
+ *
123
+ * @since 0.4.0
124
+ *
125
+ **/
126
+ function createPill(level) {
127
+ const bgColor = bgColors[level] ?? ((text) => text);
128
+ return (...text) => {
129
+ const filteredText = text
130
+ .filter(x => x !== undefined)
131
+ .join(' ');
132
+ if (filteredText.length === 0)
133
+ return '';
134
+ return bgColor(` ${filteredText} `) + ` ${dot} `;
135
+ };
136
+ }
137
+ /**
138
+ * Определяет, нужно ли применять цвет к строке сообщения.
139
+ *
140
+ * @param colorScope - Режим применения цвета.
141
+ * @param lineIndex - Индекс строки (для многострочных сообщений).
142
+ * @returns `true`, если цвет следует применить.
143
+ *
144
+ * @since 0.4.0
145
+ *
146
+ **/
147
+ function shouldColorLine(colorScope, lineIndex) {
148
+ if (colorScope === 'all')
149
+ return true;
150
+ if (colorScope === 'first-line')
151
+ return lineIndex === 0;
152
+ return false;
153
+ }
154
+ /**
155
+ * Форматирует сообщение с учётом уровня, опций и цветов.
156
+ *
157
+ * @param level - Уровень логирования.
158
+ * @param message - Сообщение для логирования. Может быть любого типа.
159
+ * @param labelOrOptions - Метка (строка) или опции форматирования.
160
+ * @param options - Опции форматирования (если первый параметр — метка).
161
+ * @returns Отформатированная строка для вывода в консоль.
162
+ *
163
+ * @since 0.4.0
164
+ *
165
+ **/
166
+ function formatMessage(level, message, labelOrOptions, options) {
167
+ let label;
168
+ let finalOptions;
169
+ if (typeof labelOrOptions === 'string') {
170
+ label = labelOrOptions;
171
+ finalOptions = {};
172
+ }
173
+ else {
174
+ finalOptions = labelOrOptions ?? {};
175
+ }
176
+ const { indent = 0, includePrefix = true, colorScope = 'first-line', colorOverride, } = finalOptions;
177
+ const actualLevel = colorOverride ?? level;
178
+ const color = colors[actualLevel];
179
+ const pill = createPill(actualLevel);
180
+ let text = '';
181
+ if (Array.isArray(message)) {
182
+ text = message.map(x => String(x)).join(' ');
183
+ }
184
+ else {
185
+ text = String(message);
186
+ }
187
+ const lineIndent = ' '.repeat(indent);
188
+ const lines = text.split('\n');
189
+ let prefix = includePrefix ? `${banner} ${pill(label)}` : '';
190
+ if (prefix && colorScope !== 'none')
191
+ prefix = color(prefix);
192
+ return lines
193
+ .map((line, lineIndex) => {
194
+ line = line.trim();
195
+ if (shouldColorLine(colorScope, lineIndex))
196
+ line = color(line);
197
+ return lineIndex === 0
198
+ ? lineIndent + prefix + line
199
+ : lineIndent + `${' '.repeat(2)}${line}`;
200
+ })
201
+ .join('\n');
202
+ }
203
+ /**
204
+ * Основная функция логирования. Проверяет уровень и выводит сообщение.
205
+ *
206
+ * @param level - Уровень логирования.
207
+ * @param value - Сообщение.
208
+ * @param labelOrOptions - Метка или опции.
209
+ * @param options - Опции (если метка передана отдельно).
210
+ *
211
+ * @since 0.4.0
212
+ *
213
+ **/
214
+ function log(level, value, labelOrOptions, options) {
215
+ if (!shouldLog(level))
216
+ return;
217
+ console.log(formatMessage(level, value, labelOrOptions));
218
+ }
219
+ /**
220
+ * Публичный интерфейс логгера. Предоставляет методы для логирования на разных уровнях.
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * logger.info('Команда запущена')
225
+ * logger.warn('Устаревший режим', 'DEPRECATED')
226
+ * logger.step('Сборка...', { indent: 2 })
227
+ * ```
228
+ *
229
+ * @since 0.4.0
230
+ *
231
+ **/
232
+ const logger = {
233
+ /**
234
+ * Устанавливает минимальный уровень логирования.
235
+ *
236
+ * @param level - Уровень, начиная с которого выводятся сообщения.
237
+ *
238
+ **/
239
+ setLevel: (level) => {
240
+ targetLevel = levelPriority.indexOf(level);
241
+ },
242
+ /**
243
+ * Логирует нейтральное сообщение с визуальным оформлением успеха.
244
+ * Использует уровень `info`, но цвет `success` (только в префиксе).
245
+ *
246
+ **/
247
+ log: (value) => {
248
+ log('info', value, {
249
+ colorScope: 'prefix',
250
+ colorOverride: 'success',
251
+ });
252
+ },
253
+ /**
254
+ * Логирует отладочное сообщение.
255
+ *
256
+ * @param value - Сообщение.
257
+ * @param label - Настраиваемая метка.
258
+ *
259
+ **/
260
+ debug: (value, label = t('label.debug')) => {
261
+ log('debug', value, label);
262
+ },
263
+ /**
264
+ * Логирует информационное сообщение.
265
+ *
266
+ * @param value - Сообщение.
267
+ * @param label - Настраиваемая метка.
268
+ *
269
+ **/
270
+ info: (value, label = t('label.info')) => {
271
+ log('info', value, label);
272
+ },
273
+ /**
274
+ * Логирует предупреждение.
275
+ *
276
+ * @param value - Сообщение.
277
+ * @param label - Настраиваемая метка.
278
+ *
279
+ **/
280
+ warn: (value, label = t('label.warning')) => {
281
+ log('warn', value, label);
282
+ },
283
+ /**
284
+ * Логирует ошибку.
285
+ *
286
+ * @param value - Сообщение.
287
+ * @param label - Настраиваемая метка.
288
+ *
289
+ **/
290
+ error: (value, label = t('label.error')) => {
291
+ log('error', value, label);
292
+ },
293
+ /**
294
+ * Логирует сообщение об успешном завершении.
295
+ *
296
+ * @param value - Сообщение.
297
+ * @param label - Настраиваемая метка.
298
+ *
299
+ **/
300
+ success: (value, label = t('label.success')) => {
301
+ log('success', value, label);
302
+ },
303
+ /**
304
+ * Логирует сообщение об отмене действия.
305
+ *
306
+ * @param value - Сообщение.
307
+ * @param label - Настраиваемая метка.
308
+ *
309
+ **/
310
+ cancel: (value, label = t('label.canceled')) => {
311
+ log('cancel', value, label);
312
+ },
313
+ /**
314
+ * Логирует шаг процесса. Без префикса, цветной текст, с отступом.
315
+ *
316
+ * @param value - Сообщение.
317
+ * @param options - Настройка отступа.
318
+ *
319
+ **/
320
+ step: (value, options = { indent: 0 }) => {
321
+ log('step', value, {
322
+ includePrefix: false,
323
+ colorScope: 'all',
324
+ indent: options.indent,
325
+ });
326
+ },
327
+ /**
328
+ * Логирует вспомогательную заметку. Цвет применяется только к префиксу.
329
+ *
330
+ * @param value - Сообщение.
331
+ * @param options - Опции форматирования.
332
+ *
333
+ **/
334
+ note: (value, options = { indent: 0, includePrefix: true }) => {
335
+ log('note', value, {
336
+ includePrefix: options.includePrefix,
337
+ colorScope: 'prefix',
338
+ indent: options.indent,
339
+ });
340
+ },
341
+ };
342
+
343
+ /**
344
+ * Режим `stdio`: ввод и вывод наследуются от родительского процесса (терминал), `stderr` перехватывается.
345
+ *
346
+ * Используется, когда важно видеть прогресс команды (например, `rsync`).
347
+ *
348
+ * @since 0.4.0
349
+ *
350
+ **/
351
+ const STDIO_INTERACTIVE = ['inherit', 'inherit', 'pipe'];
352
+ /**
353
+ * Режим `stdio`: ввод игнорируется, `stdout` и `stderr` перехватываются.
354
+ *
355
+ * Используется для полного захвата вывода команды.
356
+ *
357
+ * @since 0.4.0
358
+ *
359
+ **/
360
+ const STDIO_CAPTURE_OUTPUT = ['ignore', 'pipe', 'pipe'];
361
+ /**
362
+ * Режим `stdio`: ввод и `stdout` игнорируются, `stderr` перехватывается.
363
+ * Используется для проверки ошибок без сохранения основного вывода.
364
+ *
365
+ * @since 0.4.0
366
+ *
367
+ **/
368
+ const STDIO_CAPTURE_ERRORS = ['ignore', 'ignore', 'pipe'];
369
+ /**
370
+ * Ошибка выполнения команды в shell.
371
+ *
372
+ * Возникает, когда команда завершилась с кодом, не входящим в `doneCodes` или `cancelCodes`.
373
+ *
374
+ **/
375
+ class ShellError extends Error {
376
+ constructor(message) {
377
+ super(message);
378
+ // Убедимся, что экземпляр имеет правильный прототип
379
+ Object.setPrototypeOf(this, ShellError.prototype);
380
+ this.name = 'ShellError';
381
+ this.message = message;
382
+ Error.captureStackTrace(this, ShellError);
383
+ }
384
+ }
385
+ /**
386
+ * Ошибка, указывающая на отмену операции (например, через Ctrl+C).
387
+ *
388
+ * Соответствует коду выхода 130 (SIGINT).
389
+ *
390
+ * @since 0.4.0
391
+ *
392
+ **/
393
+ class OperationCanceledError extends Error {
394
+ constructor() {
395
+ super();
396
+ // Убедимся, что экземпляр имеет правильный прототип
397
+ Object.setPrototypeOf(this, OperationCanceledError.prototype);
398
+ this.name = 'OperationCanceledError';
399
+ Error.captureStackTrace(this, OperationCanceledError);
400
+ }
401
+ }
402
+ /**
403
+ * Асинхронно выполняет команду и возвращает результат.
404
+ *
405
+ * Перехватывает `stdout` и `stderr`.
406
+ * Поддерживает кастомные коды успеха и отмены.
407
+ *
408
+ * @param command - Команда (например, `ls`, `rsync`).
409
+ * @param args - Аргументы команды.
410
+ * @param options - Опции запуска процесса.
411
+ * @returns Результат выполнения: код, вывод, ошибки.
412
+ * @throws {ShellError} Если команда завершилась с ошибкой.
413
+ * @throws {OperationCanceledError} Если операция была отменена пользователем.
414
+ *
415
+ **/
416
+ async function execAsync(command, args = [], options = {}) {
417
+ return new Promise((resolve, reject) => {
418
+ const { doneCodes = [0], cancelCodes = [130], ...spawnOptions } = options;
419
+ spawnOptions.stdio ??= STDIO_CAPTURE_OUTPUT;
420
+ spawnOptions.shell ??= false;
421
+ const runner = spawn(command, args, spawnOptions);
422
+ const stdoutChunks = [];
423
+ const stderrChunks = [];
424
+ runner.stdout?.on('data', (chunk) => {
425
+ stdoutChunks.push(chunk);
426
+ });
427
+ runner.stderr?.on('data', (chunk) => {
428
+ stderrChunks.push(chunk);
429
+ });
430
+ runner.on('error', reject);
431
+ runner.on('exit', (code) => {
432
+ const isDone = code !== null && doneCodes.includes(code);
433
+ const stdout = Buffer.concat(stdoutChunks).toString().trim();
434
+ const stderr = Buffer.concat(stderrChunks).toString().trim();
435
+ if (isDone) {
436
+ resolve({ isDone, code, stdout, stderr });
437
+ }
438
+ else {
439
+ const isCanceled = code !== null && cancelCodes.includes(code);
440
+ reject(isCanceled
441
+ ? new OperationCanceledError()
442
+ : new ShellError(`Failed to execute command ${command} ${args.join(' ')}: ${stderr}`));
443
+ }
444
+ });
445
+ });
446
+ }
447
+ /**
448
+ * Универсальная функция для запуска команд.
449
+ *
450
+ * Расширяется методами:
451
+ * - `.inUnixShell()` — для выполнения в WSL2
452
+ * - `.dry()` — для режима симуляции
453
+ *
454
+ **/
455
+ const runCommandAsync = async (command, args, options = {}) => await execAsync(command, args, { ...options });
456
+ runCommandAsync.inUnixShell = (wsl) => (command, args = [], options = {}) => {
457
+ let cmd;
458
+ let fullArgs = [];
459
+ if (process.platform === 'win32') {
460
+ cmd = 'wsl';
461
+ if (wsl)
462
+ fullArgs.push('-d', wsl);
463
+ if (options.env) {
464
+ for (const [key, value] of Object.entries(options.env)) {
465
+ fullArgs.push(`${key}=${value}`);
466
+ }
467
+ }
468
+ fullArgs.push(command, ...args);
469
+ }
470
+ else {
471
+ cmd = command;
472
+ fullArgs = args;
473
+ }
474
+ return execAsync(cmd, fullArgs, { ...options });
475
+ };
476
+ runCommandAsync.dry = (isDryRun) => {
477
+ if (isDryRun === false)
478
+ return runCommandAsync;
479
+ return (command, args = []) => {
480
+ logger.info(`${command} ${args.join(' ')}`.trimEnd() + ' (DRY RUN)');
481
+ return Promise.resolve({
482
+ isDone: true,
483
+ code: 0,
484
+ stdout: '',
485
+ stderr: '',
486
+ });
487
+ };
488
+ };
489
+
490
+ const baseUrl = 'https://api.github.com/repos';
491
+ /** Класс ошибки уровня Git. */
492
+ class GitError extends Error {
493
+ constructor(message) {
494
+ super(message);
495
+ // Убедимся, что экземпляр имеет правильный прототип
496
+ Object.setPrototypeOf(this, GitError.prototype);
497
+ this.name = 'GitError';
498
+ this.message = message;
499
+ Error.captureStackTrace(this, GitError);
500
+ }
501
+ }
502
+ /** Класс ошибки уровня взаимодействия GitHub. */
503
+ class GithubError extends Error {
504
+ constructor(message) {
505
+ super(message);
506
+ // Убедимся, что экземпляр имеет правильный прототип
507
+ Object.setPrototypeOf(this, GithubError.prototype);
508
+ this.name = 'GithubError';
509
+ this.message = message;
510
+ Error.captureStackTrace(this, GithubError);
511
+ }
512
+ }
513
+ /** Класс ошибки уровня GitHub Workflow. */
514
+ class WorkflowStatusError extends Error {
515
+ constructor(message) {
516
+ super(message);
517
+ // Убедимся, что экземпляр имеет правильный прототип
518
+ Object.setPrototypeOf(this, WorkflowStatusError.prototype);
519
+ this.name = 'WorkflowStatusError';
520
+ this.message = message;
521
+ Error.captureStackTrace(this, WorkflowStatusError);
522
+ }
523
+ }
524
+ /** Определяет, находится ли текущее решение в рабочем дереве Git. */
525
+ async function checkIsInWorkTreeAsync() {
526
+ try {
527
+ const { stdout } = await runCommandAsync('git', ['rev-parse', '--is-inside-work-tree'], { stdio: 'pipe' });
528
+ return stdout === 'true';
529
+ }
530
+ catch {
531
+ return false;
532
+ }
533
+ }
534
+ const getConnectionType = (url) => {
535
+ if (url.startsWith('git@'))
536
+ return 'ssh';
537
+ if (url.startsWith('https://'))
538
+ return 'https';
539
+ };
540
+ /** Возвращает имя репозитория и тип подключения. */
541
+ async function getRepositoryDetails() {
542
+ const { stdout, stderr } = await runCommandAsync('git', ['config', 'remote.origin.url'], { stdio: 'pipe' });
543
+ if (stderr)
544
+ throw new GitError(stderr);
545
+ // Match the URLs like:
546
+ // git@github.com:wb-mirta/core.git
547
+ // https://github.com/wb-mirta/core.git
548
+ //
549
+ const regex = /^(?:git@|https:\/\/)(?:[^/:]+)[/:]?(.*?).git$/i;
550
+ const match = regex
551
+ .exec(stdout);
552
+ if (!match?.[1])
553
+ throw new GitError(`Unable to detect remote origin url`);
554
+ return {
555
+ name: match[1],
556
+ connectionType: getConnectionType(stdout),
557
+ };
558
+ }
559
+ /** Возвращает SHA текущего HEAD. */
560
+ async function getShaAsync() {
561
+ return (await runCommandAsync('git', ['rev-parse', 'HEAD'])).stdout;
562
+ }
563
+ /** Возвращает имя текущей ветки. */
564
+ async function getBranchAsync() {
565
+ return (await runCommandAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout;
566
+ }
567
+ /** Проверяет, синхронизирован ли текущий HEAD с удаленным. */
568
+ async function assertIsSyncedWithRemoteAsync(repository) {
569
+ let isSynced = false;
570
+ try {
571
+ const branch = await getBranchAsync();
572
+ const remote = await fetch(`${baseUrl}/${repository}/commits/${branch}?per_page=1`);
573
+ const data = await remote.json();
574
+ isSynced = data.sha === await getShaAsync();
575
+ }
576
+ catch {
577
+ throw new GithubError('Failed to check whether local HEAD is up-to-date with remote');
578
+ }
579
+ if (!isSynced)
580
+ throw new GithubError('Local HEAD is not up-to-date with remote');
581
+ }
582
+ /** Проверяет успешность CI-построения последнего коммита. */
583
+ async function assertWorkflowResultAsync(repository, name) {
584
+ let isBuildPassed = false;
585
+ try {
586
+ const sha = await getShaAsync();
587
+ const result = await fetch(`${baseUrl}/${repository}/actions/runs?head_sha=${sha}&status=completed&exclude_pull_requests=true`);
588
+ const data = await result.json();
589
+ isBuildPassed = data.workflow_runs.some(({ name: workflowName, conclusion }) => workflowName.toLowerCase() === name.toLowerCase()
590
+ && conclusion === 'success');
591
+ }
592
+ catch {
593
+ throw new GithubError('Unable to get CI status for the current commit');
594
+ }
595
+ if (!isBuildPassed)
596
+ throw new WorkflowStatusError('CI build of the latest commit has not passed yet');
597
+ }
598
+
599
+ const { dim, yellow } = chalk;
600
+ const helpMessageEn = `\
601
+ Performs operations over monorepo projects powered by the Mirta Framework.
602
+
603
+ ${yellow('Usage:')}
604
+ mirta [command] [options...]
605
+
606
+ ${yellow('Global flags:')}
607
+ --help, -h
608
+ ${dim('Displays help information about available commands and options.')}
609
+ --version, -v
610
+ ${dim('Prints the version of this CLI utility.')}
611
+
612
+ ${yellow('Commands:')}
613
+ release
614
+ ${dim('Increase package versions following semantic versioning rules.')}
615
+ publish
616
+ ${dim('Builds and publishes packages to npm registry.')}
617
+
618
+ ${yellow(`Options for 'release':`)}
619
+ --dry-run, --dry
620
+ ${dim('Runs the command in simulation mode. Shows what would change but does not apply modifications.')}
621
+ --preid <id>
622
+ ${dim('Sets a custom pre-release identifier (e.g., `alpha`, `beta.1`, `rc`).')}
623
+ --skip-prompts
624
+ ${dim('Skips interactive prompts, using default values.')}
625
+ --skip-git
626
+ ${dim('Disables creating a commit and tag. Git changes remain uncommitted.')}
627
+
628
+ ${yellow(`Options for 'publish':`)}
629
+ --dry-run, --dry
630
+ ${dim('Runs in simulation mode. Shows what would happen, but does not publish')}
631
+ --skip-build
632
+ ${dim('Skips running `pnpm run build` before publishing.')}
633
+ --skip-git
634
+ ${dim('Disables git state checks (equivalent to `--no-git-checks` in `pnpm publish`).')}
635
+ `;
636
+ const helpMessageRu = `\
637
+ Выполняет операции над проектами монорепозитория, работающими на базе фреймворка Mirta.
638
+
639
+ ${yellow('Использование:')}
640
+ mirta [command] [options...]
641
+
642
+ ${yellow('Команды:')}
643
+ release
644
+ ${dim('Повышение версий пакетов согласно правилам семантического версионирования.')}
645
+ publish
646
+ ${dim('Сборка и публикация пакетов в реестр npm.')}
647
+
648
+ ${yellow('Общие флаги:')}
649
+ --help, -h
650
+ ${dim('Отображает справку по доступным командам и параметрам.')}
651
+ --version, -v
652
+ ${dim('Выводит версию данной утилиты.')}
653
+
654
+ ${yellow(`Опции для 'release':`)}
655
+ --dry-run, --dry
656
+ ${dim('Запускает команду в режиме симуляции. Показывает изменения, но не применяет их.')}
657
+ --preid <id>
658
+ ${dim('Задаёт кастомный префикс для преверсии (например, `alpha`, `beta.1`, `rc`).')}
659
+ --skip-prompts
660
+ ${dim('Пропускает интерактивные запросы. Используются значения по умолчанию.')}
661
+ --skip-git
662
+ ${dim('Не создаёт коммит и тег. Git-изменения остаются в рабочей директории.')}
663
+
664
+ ${yellow(`Опции для 'publish':`)}
665
+ --dry-run, --dry
666
+ ${dim('Запускает команду в режиме симуляции. Показывает изменения, но не применяет их.')}
667
+ --skip-build
668
+ ${dim('Пропускает выполнение `pnpm run build` перед публикацией')}
669
+ --skip-git
670
+ ${dim('Отключает проверки git-состояния (аналог `--no-git-checks` в `pnpm publish`)')}
671
+ `;
672
+ const getHelpMessage = () => getLocale() === 'ru-RU'
673
+ ? helpMessageRu
674
+ : helpMessageEn;
675
+
676
+ const runners = {
677
+ release: async () => (await import('./release.mjs')).runAsync,
678
+ publish: async () => (await import('./publish.mjs')).runAsync,
679
+ deploy: async () => (await import('./deploy.mjs')).runAsync,
680
+ };
681
+ async function resolveRunnerAsync(nameInput) {
682
+ if (!(nameInput in runners)) {
683
+ const knownNames = Object.keys(runners);
684
+ const suggestion = nameInput.length > 1
685
+ ? suggestClosest(nameInput, knownNames, { maxDistance: 2 })
686
+ : undefined;
687
+ const errorInput = suggestion
688
+ ? t('command.suggest', { input: nameInput, suggestion })
689
+ : nameInput;
690
+ throw new Error(t('command.notFound', { input: errorInput }));
691
+ }
692
+ const runner = runners[nameInput];
693
+ return {
694
+ runAsync: await runner(),
695
+ };
696
+ }
697
+
698
+ function assertNoParseErrors(result) {
699
+ if (!result.hasErrors)
700
+ return;
701
+ const lines = [
702
+ t('args.errorHeader', { count: result.errors.length }),
703
+ ];
704
+ for (const error of result.errors) {
705
+ switch (error.type) {
706
+ case 'unknown-option':
707
+ if (error.suggestion) {
708
+ lines.push(t('args.unknownOptionSuggest', {
709
+ option: chalk.bold(error.option),
710
+ suggestion: chalk.bold(`--${error.suggestion}`),
711
+ }));
712
+ }
713
+ else {
714
+ lines.push(t('args.unknownOption', {
715
+ option: chalk.bold(error.option),
716
+ }));
717
+ }
718
+ break;
719
+ case 'missing-value':
720
+ lines.push(t('args.missingValue', {
721
+ option: chalk.bold(error.option),
722
+ }));
723
+ break;
724
+ }
725
+ }
726
+ throw new Error(lines.join('\n'));
727
+ }
728
+
729
+ const commonOptions = {
730
+ locale: {
731
+ type: 'string',
732
+ },
733
+ version: {
734
+ type: 'boolean',
735
+ short: 'v',
736
+ },
737
+ help: {
738
+ type: 'boolean',
739
+ short: 'h',
740
+ },
741
+ };
13
742
  async function run() {
14
- const command = process.argv[2];
15
- if (command === 'release') {
16
- await import('./release.mjs');
743
+ const args = createStagedArgs(process.argv.slice(2));
744
+ const parseResult = args.parse(commonOptions);
745
+ assertNoParseErrors(parseResult);
746
+ const { values: argv, positionals, stagedArgs: runnerArgs } = parseResult.data;
747
+ if (argv.locale)
748
+ await setLocaleAsync(argv.locale);
749
+ if (argv.version) {
750
+ console.log(`${cliPackage.name} v${cliPackage.version}`);
751
+ return;
17
752
  }
18
- else if (command === 'publish') {
19
- await import('./publish.mjs');
753
+ if (argv.help || !positionals.length) {
754
+ console.log(getHelpMessage());
755
+ return;
20
756
  }
757
+ const module = await resolveRunnerAsync(positionals[0]);
758
+ await module.runAsync(runnerArgs);
21
759
  }
22
760
  function prettify(message, name) {
23
761
  if (name && message.startsWith(name))
@@ -28,11 +766,10 @@ run().catch((e) => {
28
766
  if (e instanceof GitError || e instanceof GithubError || e instanceof WorkflowStatusError || e instanceof ShellError) {
29
767
  logger.error(prettify(e.message, e.name));
30
768
  }
31
- else if (e instanceof PromptCanceledError) {
32
- logger.cancel(messages.errors.operationCanceled);
769
+ else if (e instanceof PromptCanceledError || e instanceof OperationCanceledError) {
770
+ logger.cancel(t('step.canceled'));
33
771
  }
34
772
  else if (e instanceof Error && 'code' in e) {
35
- // if (e.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION')
36
773
  // if (e.code === 'ENOENT')
37
774
  logger.error(prettify(e.message, e.name));
38
775
  }
@@ -44,3 +781,5 @@ run().catch((e) => {
44
781
  }
45
782
  process.exit(1);
46
783
  });
784
+
785
+ export { STDIO_INTERACTIVE as S, assertNoParseErrors as a, assertIsSyncedWithRemoteAsync as b, assertWorkflowResultAsync as c, checkIsInWorkTreeAsync as d, STDIO_CAPTURE_ERRORS as e, getRepositoryDetails as g, logger as l, prompts as p, runCommandAsync as r, t };