@mirta/cli 0.3.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.
@@ -0,0 +1,256 @@
1
+ import chalk from 'chalk';
2
+ import { a as getLocale, g as getLocalized, u as useLogger, r as runCommandAsync } from './shell.mjs';
3
+ import nodePath from 'node:path';
4
+ import { readdirSync, statSync, readFileSync, existsSync, writeFileSync } from 'node:fs';
5
+
6
+ const { dim, yellow: yellow$1 } = chalk;
7
+ const locale = getLocale();
8
+ const helpMessageEn = `\
9
+ Performs operations over monorepo projects powered by the Mirta Framework.
10
+
11
+ ${yellow$1('Usage:')}
12
+ mirta [command] [options...]
13
+
14
+ ${yellow$1('Commands:')}
15
+ - release:
16
+ ${dim('Increase package versions following semantic versioning rules.')}
17
+ - publish:
18
+ ${dim('Builds and publishes packages to npm registry.')}
19
+
20
+ ${yellow$1(`Options for 'release':`)}
21
+ --dry
22
+ ${dim('Runs the command in dry run mode, showing what would be done but not performing any actual changes. Useful for previewing changes before applying them.')}
23
+ --preid <custom-pre-release-id>
24
+ ${dim('Sets a custom pre-release identifier that will be appended to the version string (for example, beta.1). This option allows creating pre-release versions like alpha, beta, rc etc., prior to official stable releases.')}
25
+ --skipPrompts
26
+ ${dim('Skips user interaction prompts entirely. The command runs non-interactively, automatically proceeding with defaults or configured values where applicable.')}
27
+ --skipGit
28
+ ${dim('Omits Git-related actions such as committing changes, tagging commits, or pushing updates to remote repositories. This can be useful if you want to manually manage Git operations later.')}
29
+
30
+ ${yellow$1(`Options for 'publish':`)}
31
+ --dry
32
+ ${dim('Runs the command in dry run mode, showing what would be done but not performing any actual changes. Useful for previewing changes before applying them.')}
33
+ --skipBuild
34
+ ${dim('Excludes running the build process after version bumps. Bypasses execution of tasks defined in the build pipeline, allowing users to control whether they need a rebuild after updating package versions.')}
35
+ --skipGit
36
+ ${dim('Omits Git-related actions such as committing changes, tagging commits, or pushing updates to remote repositories. This can be useful if you want to manually manage Git operations later.')}
37
+
38
+ `;
39
+ const helpMessageRu = `\
40
+ Выполняет операции над проектами монорепозитория, работающими на базе фреймворка Mirta.
41
+
42
+ ${yellow$1('Использование:')}
43
+ mirta [command] [options...]
44
+
45
+ ${yellow$1('Команды:')}
46
+ - release:
47
+ ${dim('Повышение версий пакетов согласно правилам семантического версионирования.')}
48
+ - publish:
49
+ ${dim('Сборка и публикация пакетов в реестр npm.')}
50
+
51
+ ${yellow$1(`Опции для 'release':`)}
52
+ --dry
53
+ ${dim('Запускает команду в режиме симуляции ("dry run"), показывая изменения, которые будут произведены, но фактически ничего не меняя. Полезно для предварительного просмотра изменений перед применением.')}
54
+ --preid <custom-pre-release-id>
55
+ ${dim('Устанавливает кастомный префикс для предварительной версии, который добавляется к номеру версии пакета (например, beta.1). Эта опция позволяет создавать предварительные версии типа альфа, бета, RC и др. перед официальным стабильным выпуском.')}
56
+ --skipPrompts
57
+ ${dim('Пропускает интерактивные запросы пользователя. Команда выполняется автоматически, используя значения по умолчанию или заданные настройки.')}
58
+ --skipGit
59
+ ${dim('Игнорирует действия, связанные с системой контроля версий Git, такие как фиксация изменений, создание меток коммитов или отправка изменений на удалённый репозиторий. Может пригодиться, если вы хотите самостоятельно управлять операциями с Git позже.')}
60
+
61
+ ${yellow$1(`Опции для 'publish':`)}
62
+ --dry
63
+ ${dim('Запускает команду в режиме симуляции ("dry run"), показывая изменения, которые будут произведены, но фактически ничего не меняя. Полезно для предварительного просмотра изменений перед применением.')}
64
+ --skipBuild
65
+ ${dim('Исключает запуск процесса сборки после обновления версий пакетов. Пропускает выполнение заданий, указанных в конвейере сборки, позволяя вам самим решать, необходима ли повторная компиляция после изменения номеров версий.')}
66
+ --skipGit
67
+ ${dim('Игнорирует действия, связанные с системой контроля версий Git, такие как фиксация изменений, создание меток коммитов или отправка изменений на удалённый репозиторий. Может пригодиться, если вы хотите самостоятельно управлять операциями с Git позже.')}
68
+ `;
69
+ const helpMessage = locale === 'ru-RU'
70
+ ? helpMessageRu
71
+ : helpMessageEn;
72
+
73
+ const { yellow } = chalk;
74
+ const messages = await getLocalized();
75
+ const logger = useLogger(messages);
76
+ const rootDir = process.cwd();
77
+ const packagesDir = nodePath.join(rootDir, 'packages');
78
+ const getPackageRoot = (packageName) => nodePath.join(packagesDir, packageName);
79
+ // Перечисляем пакеты, которые должны выйти в релиз.
80
+ const packages = readdirSync(packagesDir)
81
+ .filter((pkgName) => {
82
+ const pkgRoot = getPackageRoot(pkgName);
83
+ if (!statSync(pkgRoot).isDirectory())
84
+ return;
85
+ const pkg = JSON.parse(readFileSync(nodePath.join(pkgRoot, 'package.json'), 'utf-8'));
86
+ return !pkg.private;
87
+ });
88
+ const templatePackages = {};
89
+ /** Имя пользователя или организации, владеющей пакетом. */
90
+ let scope;
91
+ /** Форматированный scope пакета. */
92
+ let scoped;
93
+ /** Использовать scope в качестве префикса в названиях пакетов. */
94
+ let scopeAsPackagePrefix = false;
95
+ const mirtaConfigFilePath = nodePath.join(rootDir, 'mirta.config.json');
96
+ const rootPackage = JSON.parse(readFileSync(nodePath.join(rootDir, 'package.json'), 'utf-8'));
97
+ if (rootPackage.name) {
98
+ const match = /^@([^/]+)\//i.exec(rootPackage.name);
99
+ if (match?.[1]) {
100
+ scope = match[1];
101
+ scoped = `@${match[1]}/`;
102
+ }
103
+ }
104
+ /** Возвращает текущую версию корневого проекта. */
105
+ function getCurrentVersion() {
106
+ return rootPackage.version;
107
+ }
108
+ function hasScript(name) {
109
+ return rootPackage.scripts?.[name] !== void 0;
110
+ }
111
+ if (existsSync(mirtaConfigFilePath)) {
112
+ const config = JSON.parse(readFileSync(mirtaConfigFilePath, 'utf-8'));
113
+ scope = config.scope;
114
+ scopeAsPackagePrefix = config.scopedPackagePrefix === true;
115
+ if (scope?.startsWith('@'))
116
+ scope = scope.slice(1);
117
+ if (scope)
118
+ scoped = `@${scope}/`;
119
+ if (config.templates && Array.isArray(config.templates)) {
120
+ config.templates.forEach((template) => {
121
+ const templatesDir = nodePath.resolve(rootDir, template);
122
+ // Предохранитель от выхода за пределы рабочей директории.
123
+ if (!templatesDir.startsWith(rootDir))
124
+ return;
125
+ // Обрабатываем только существующие директории.
126
+ if (!statSync(templatesDir).isDirectory())
127
+ return;
128
+ const localTemplatePackages = readdirSync(templatesDir, {
129
+ withFileTypes: true,
130
+ recursive: true,
131
+ })
132
+ .reduce((items, nextEntry) => {
133
+ if (nextEntry.name === 'package.json')
134
+ items.push(nextEntry.parentPath);
135
+ return items;
136
+ }, []);
137
+ templatePackages[templatesDir] = localTemplatePackages;
138
+ });
139
+ }
140
+ }
141
+ const isWorkspacePackage = (pkgName) => {
142
+ if (!pkgName)
143
+ return false;
144
+ if (packages.includes(pkgName))
145
+ return true;
146
+ if (!scoped || !pkgName.startsWith(scoped))
147
+ return false;
148
+ pkgName = pkgName.slice(scoped.length);
149
+ if (scopeAsPackagePrefix)
150
+ pkgName = `${scope}-${pkgName}`;
151
+ return packages.includes(pkgName);
152
+ };
153
+ function updateDependencies(pkg, depType, version) {
154
+ const deps = pkg[depType];
155
+ if (!deps)
156
+ return;
157
+ Object.keys(deps).forEach((dep) => {
158
+ if (!isWorkspacePackage(dep))
159
+ return;
160
+ deps[dep] = version;
161
+ });
162
+ }
163
+ function updateTemplateDependencies(templateRoot, version) {
164
+ logger.step(`Template: ${nodePath.relative(rootDir, templateRoot)}`);
165
+ const pkgPath = nodePath.join(templateRoot, 'package.json');
166
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
167
+ updateDependencies(pkg, 'dependencies', version);
168
+ updateDependencies(pkg, 'devDependencies', version);
169
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
170
+ }
171
+ function updatePackageVersion(pkgRoot, version) {
172
+ const pkgPath = nodePath.join(pkgRoot, 'package.json');
173
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
174
+ logger.step(pkgRoot === rootDir
175
+ ? 'Root package'
176
+ : `Package: ${pkg.name ?? nodePath.basename(pkgRoot)}`);
177
+ pkg.version = version;
178
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
179
+ }
180
+ function updateVersion(version) {
181
+ logger.log(`Patching all packages to version ${version}`);
182
+ // Update root package.json
183
+ updatePackageVersion(rootDir, version);
184
+ // Update all packages
185
+ packages.forEach((pkgDirName) => {
186
+ updatePackageVersion(nodePath.join(packagesDir, pkgDirName), version);
187
+ });
188
+ const templateKeys = Object.keys(templatePackages);
189
+ if (templateKeys.length > 0) {
190
+ logger.log(`Patching template packages`);
191
+ Object.keys(templatePackages).forEach((templatesDir) => {
192
+ templatePackages[templatesDir].forEach((templateRoot) => {
193
+ updateTemplateDependencies(templateRoot, version);
194
+ });
195
+ });
196
+ }
197
+ }
198
+ /** Выполняет сборку пакетов стандартной командой `pnpm run build`. */
199
+ async function buildPackagesAsync(skipBuild) {
200
+ if (!skipBuild) {
201
+ logger.log('Building packages...');
202
+ await runCommandAsync('pnpm', ['run', 'build']);
203
+ }
204
+ else {
205
+ logger.log(`${yellow('Skip')} building packages`);
206
+ }
207
+ }
208
+ async function publishSinglePackageAsync(pkgName, version, flags) {
209
+ let releaseTag = void 0;
210
+ if (version.includes('alpha')) {
211
+ releaseTag = 'alpha';
212
+ }
213
+ else if (version.includes('beta')) {
214
+ releaseTag = 'beta';
215
+ }
216
+ else if (version.includes('rc')) {
217
+ releaseTag = 'rc';
218
+ }
219
+ logger.step(`Publishing ${pkgName}`);
220
+ try {
221
+ await runCommandAsync('pnpm', [
222
+ 'publish',
223
+ ...(releaseTag ? ['--tag', releaseTag] : []),
224
+ '--access',
225
+ 'public',
226
+ ...(flags),
227
+ ], {
228
+ cwd: getPackageRoot(pkgName),
229
+ stdio: 'pipe',
230
+ });
231
+ logger.success(`Published ${pkgName}@${version}`);
232
+ }
233
+ catch (e) {
234
+ if (e instanceof Error && /previously published/.exec(e.message)) {
235
+ logger.warn(`Skipping already published ${pkgName}`);
236
+ }
237
+ else {
238
+ throw e;
239
+ }
240
+ }
241
+ }
242
+ /** Выполняет публикацию пакетов монорепозитория в NPM. */
243
+ async function publishPackagesAsync(version, skipGitChecks, isDryRun) {
244
+ logger.log('Publishing packages...');
245
+ const flags = [];
246
+ if (isDryRun)
247
+ flags.push('--dry-run');
248
+ if (isDryRun || skipGitChecks || process.env.CI)
249
+ flags.push('--no-git-checks');
250
+ if (process.env.CI)
251
+ flags.push('--provenance');
252
+ for (const pkgName of packages)
253
+ await publishSinglePackageAsync(pkgName, version, flags);
254
+ }
255
+
256
+ export { hasScript as a, buildPackagesAsync as b, getCurrentVersion as g, helpMessage as h, publishPackagesAsync as p, updateVersion as u };
@@ -0,0 +1,54 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { g as getCurrentVersion, h as helpMessage, b as buildPackagesAsync, p as publishPackagesAsync } from './package.mjs';
3
+ import cliPackage from '../package.json' with { type: 'json' };
4
+ import 'chalk';
5
+ import './shell.mjs';
6
+ import 'node:child_process';
7
+ import 'node:url';
8
+ import 'node:fs';
9
+ import 'node:path';
10
+ import 'lodash.merge';
11
+
12
+ const currentVersion = getCurrentVersion();
13
+ const allOptions = ({
14
+ dry: {
15
+ type: 'boolean',
16
+ default: false,
17
+ },
18
+ skipGit: {
19
+ type: 'boolean',
20
+ default: false,
21
+ },
22
+ skipBuild: {
23
+ type: 'boolean',
24
+ default: false,
25
+ },
26
+ help: {
27
+ type: 'boolean',
28
+ short: 'h',
29
+ default: false,
30
+ },
31
+ version: {
32
+ type: 'boolean',
33
+ short: 'v',
34
+ default: false,
35
+ },
36
+ });
37
+ const args = process.argv.slice(2);
38
+ const { values: argv } = parseArgs({
39
+ args,
40
+ options: allOptions,
41
+ allowPositionals: true,
42
+ });
43
+ if (argv.help) {
44
+ console.log(helpMessage);
45
+ process.exit(0);
46
+ }
47
+ if (argv.version) {
48
+ console.log(`${cliPackage.name} v${cliPackage.version}`);
49
+ process.exit(0);
50
+ }
51
+ const skipGit = argv.skipGit;
52
+ const isDryRun = argv.dry;
53
+ await buildPackagesAsync(argv.skipBuild);
54
+ await publishPackagesAsync(currentVersion, skipGit, isDryRun);
@@ -0,0 +1,195 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { prerelease, inc, valid } from 'semver';
3
+ import { h as helpMessage, g as getCurrentVersion, u as updateVersion, a as hasScript } from './package.mjs';
4
+ import { c as checkIsInWorkTreeAsync, g as getRepositoryDetails, e as ensureIsSyncedWithRemoteAsync, p as prompts, b as ensureWorkflowResultAsync } from './github.mjs';
5
+ import { g as getLocalized, u as useLogger, r as runCommandAsync } from './shell.mjs';
6
+ import chalk from 'chalk';
7
+ import cliPackage from '../package.json' with { type: 'json' };
8
+ import 'node:path';
9
+ import 'node:fs';
10
+ import 'prompts';
11
+ import 'node:child_process';
12
+ import 'node:url';
13
+ import 'lodash.merge';
14
+
15
+ const { yellow } = chalk;
16
+ const allOptions = ({
17
+ dry: {
18
+ type: 'boolean',
19
+ default: false,
20
+ },
21
+ preid: {
22
+ type: 'string',
23
+ },
24
+ skipPrompts: {
25
+ type: 'boolean',
26
+ default: false,
27
+ },
28
+ skipGit: {
29
+ type: 'boolean',
30
+ default: false,
31
+ },
32
+ skipBuild: {
33
+ type: 'boolean',
34
+ default: false,
35
+ },
36
+ help: {
37
+ type: 'boolean',
38
+ short: 'h',
39
+ default: false,
40
+ },
41
+ version: {
42
+ type: 'boolean',
43
+ short: 'v',
44
+ default: false,
45
+ },
46
+ });
47
+ const args = process.argv.slice(2);
48
+ const { values: argv, positionals } = parseArgs({
49
+ args,
50
+ options: allOptions,
51
+ allowPositionals: true,
52
+ });
53
+ if (argv.help) {
54
+ console.log(helpMessage);
55
+ process.exit(0);
56
+ }
57
+ if (argv.version) {
58
+ console.log(`${cliPackage.name} v${cliPackage.version}`);
59
+ process.exit(0);
60
+ }
61
+ const messages = await getLocalized();
62
+ const logger = useLogger(messages);
63
+ const currentVersion = getCurrentVersion();
64
+ const preid = argv.preid ?? prerelease(currentVersion)?.[0];
65
+ /** Возможные типы релиза. */
66
+ const releaseTypes = [
67
+ 'patch',
68
+ 'minor',
69
+ 'major',
70
+ ...(preid
71
+ ? (['prepatch', 'preminor', 'premajor', 'prerelease'])
72
+ : []),
73
+ ];
74
+ const isDryRun = argv.dry;
75
+ const skipGit = argv.skipGit;
76
+ const skipPrompts = argv.skipPrompts;
77
+ // Параметр командной строки:
78
+ // конкретный номер версии, либо тип релиза (см. releaseTypes).
79
+ //
80
+ let targetVersion = positionals[1];
81
+ const getIncremented = (release) => inc(currentVersion, release, void 0, preid);
82
+ const inWorkTree = await checkIsInWorkTreeAsync();
83
+ let repository;
84
+ let connectionType;
85
+ if (inWorkTree) {
86
+ const { name: repoName, connectionType: connType } = await getRepositoryDetails();
87
+ // Репозиторий, в котором выполняется релиз.
88
+ repository = repoName;
89
+ // Тип подключения к удалённому репозиторию.
90
+ connectionType = connType;
91
+ if (repository)
92
+ logger.info(`Repository: ${repository}`);
93
+ await ensureIsSyncedWithRemoteAsync(repository);
94
+ }
95
+ else {
96
+ logger.info('Repository: not in git work tree');
97
+ }
98
+ // Если версия не указана, предоставить возможность выбора.
99
+ if (!targetVersion) {
100
+ const choices = releaseTypes
101
+ .map((item) => {
102
+ const version = getIncremented(item);
103
+ return { title: `${item} (${version})`, value: version };
104
+ })
105
+ .concat([{ title: 'custom', value: 'custom' }]);
106
+ const { release } = await prompts({
107
+ type: 'select',
108
+ name: 'release',
109
+ message: 'Select release type',
110
+ choices,
111
+ });
112
+ if (release === 'custom') {
113
+ const { version } = await prompts({
114
+ type: 'text',
115
+ name: 'version',
116
+ message: 'Type custom version',
117
+ initial: currentVersion,
118
+ });
119
+ // Номер версии вводится вручную.
120
+ targetVersion = version;
121
+ }
122
+ else {
123
+ // Номер версии извлекается из предложенной ранее строки.
124
+ targetVersion = release;
125
+ }
126
+ }
127
+ // Вместо номера версии передан компонент для инкремента?
128
+ //
129
+ if (releaseTypes.includes(targetVersion))
130
+ targetVersion = getIncremented(targetVersion) ?? '';
131
+ if (!valid(targetVersion))
132
+ throw new Error(`Target version is not valid: ${targetVersion}`);
133
+ const runCommandIfNotDryAsync = runCommandAsync.ifNotDry(isDryRun);
134
+ let isVersionUpdated = false;
135
+ async function runAsync() {
136
+ if (skipPrompts) {
137
+ logger.info(`Releasing v${targetVersion}`);
138
+ }
139
+ else {
140
+ const { confirmRelease } = await prompts({
141
+ type: 'confirm',
142
+ name: 'confirmRelease',
143
+ message: `Releasing v${targetVersion}. Continue?`,
144
+ });
145
+ if (!confirmRelease) {
146
+ logger.cancel('No changes was made.');
147
+ return;
148
+ }
149
+ }
150
+ if (inWorkTree) {
151
+ logger.log('Ensuring CI status for HEAD...');
152
+ await ensureWorkflowResultAsync(repository, 'build');
153
+ }
154
+ updateVersion(targetVersion);
155
+ isVersionUpdated = true;
156
+ if (inWorkTree && hasScript('changelog')) {
157
+ logger.log('Generating changelog...');
158
+ await runCommandAsync('pnpm', ['run', 'changelog']);
159
+ if (!skipPrompts) {
160
+ const { isContinue } = await prompts({
161
+ type: 'confirm',
162
+ name: 'isContinue',
163
+ message: 'Changelog generated. Does it look good?',
164
+ });
165
+ if (!isContinue)
166
+ return;
167
+ }
168
+ }
169
+ logger.log('Updating lock-file...');
170
+ await runCommandAsync('pnpm', ['install', '--prefer-offline']);
171
+ if (!skipGit && connectionType === 'ssh') {
172
+ const { stdout } = await runCommandAsync('git', ['diff'], { stdio: 'pipe' });
173
+ if (stdout) {
174
+ logger.step('Committing version changes...');
175
+ await runCommandIfNotDryAsync('git', ['add', '-A']);
176
+ await runCommandIfNotDryAsync('git', ['commit', '-m', `release: v${targetVersion}`]);
177
+ logger.step('Pushing to GitHub');
178
+ await runCommandIfNotDryAsync('git', ['tag', `v${targetVersion}`]);
179
+ await runCommandIfNotDryAsync('git', ['push', 'origin', `refs/tags/v${targetVersion}`]);
180
+ await runCommandIfNotDryAsync('git', ['push']);
181
+ logger.note(yellow('Release will be done via GitHub Actions.')
182
+ + `\nCheck status at https://github.com/${repository}/actions/workflows/release.yml`);
183
+ }
184
+ else {
185
+ logger.info('No changes to commit.');
186
+ }
187
+ }
188
+ }
189
+ await runAsync().catch((e) => {
190
+ if (isVersionUpdated) {
191
+ // Revert version changes on failed release
192
+ updateVersion(currentVersion);
193
+ }
194
+ throw e;
195
+ });
package/dist/shell.mjs ADDED
@@ -0,0 +1,190 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { existsSync, promises } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import merge from 'lodash.merge';
6
+ import chalk from 'chalk';
7
+
8
+ const fallbackLocale = 'en-US';
9
+ const localesPath = './locales';
10
+ let currentLocale = '';
11
+ /**
12
+ *
13
+ * Used to link obtained locale with correct locale file.
14
+ *
15
+ * @param locale Obtained locale
16
+ * @returns locale that linked with correct name
17
+ */
18
+ function linkLocale(locale) {
19
+ if (locale === 'C')
20
+ return fallbackLocale;
21
+ let linkedLocale;
22
+ try {
23
+ linkedLocale = Intl.getCanonicalLocales(locale)[0];
24
+ }
25
+ catch (error) {
26
+ console.warn(`${JSON.stringify(error)}, invalid language tag: "${locale}"`);
27
+ }
28
+ switch (linkedLocale) {
29
+ default:
30
+ linkedLocale = locale;
31
+ }
32
+ return linkedLocale;
33
+ }
34
+ function getLocale() {
35
+ if (currentLocale)
36
+ return currentLocale;
37
+ const shellLocale = process.env.LC_ALL
38
+ ?? process.env.LC_MESSAGES
39
+ ?? process.env.LANG
40
+ ?? Intl.DateTimeFormat().resolvedOptions().locale;
41
+ return currentLocale = linkLocale(shellLocale.split('.')[0].replace('_', '-'));
42
+ }
43
+ async function loadLanguageFile(filePath) {
44
+ return await promises.readFile(filePath, 'utf-8').then((content) => {
45
+ const data = JSON.parse(content);
46
+ return data;
47
+ });
48
+ }
49
+ async function loadLocale(localesRoot) {
50
+ currentLocale = getLocale();
51
+ const fallbackFilePath = resolve(localesRoot, `${fallbackLocale}.json`);
52
+ const targetFilePath = resolve(localesRoot, `${currentLocale}.json`);
53
+ const messages = await loadLanguageFile(fallbackFilePath);
54
+ if (!messages)
55
+ throw Error('Fallback locale file not found.');
56
+ if (existsSync(targetFilePath))
57
+ merge(messages, await loadLanguageFile(targetFilePath));
58
+ return messages;
59
+ }
60
+ let localized;
61
+ async function getLocalized() {
62
+ const path = fileURLToPath(new URL(localesPath, import.meta.url));
63
+ return localized ??= await loadLocale(path);
64
+ }
65
+
66
+ const { dim, red, cyan, green, yellow, bgRed, bgCyan, bgGreen, bgYellow, } = chalk;
67
+ const dot = '•';
68
+ const banner = `Mirta ${dot}`;
69
+ const redBanner = red(banner);
70
+ const cyanBanner = cyan(banner);
71
+ const greenBanner = green(banner);
72
+ const yellowBanner = yellow(banner);
73
+ const dimmedBanner = dim(banner);
74
+ const infoPill = (message) => message ? bgCyan.black(` ${message} `) + (` ${cyan(dot)} `) : '';
75
+ const successPill = (message) => message
76
+ ? bgGreen.black(` ${message} `) + ' '
77
+ : '';
78
+ const warnPill = (message) => message ? bgYellow.black(` ${message} `) + (` ${yellow(dot)} `) : '';
79
+ const errorPill = (message) => message
80
+ ? bgRed.white(` ${message} `) + ' '
81
+ : '';
82
+ const formatMessage = (message) => message ? `${greenBanner} ${message}` : '';
83
+ function useLogger(localized) {
84
+ function log(message) {
85
+ const formatted = formatMessage(message);
86
+ if (formatted)
87
+ console.log(formatted);
88
+ }
89
+ function step(message) {
90
+ if (message)
91
+ console.log(`${dimmedBanner} ${dim(message)}`);
92
+ }
93
+ function info(message, title = localized.status.info) {
94
+ if (message)
95
+ console.log(`${cyanBanner} ${infoPill(title)}${cyan(message)}`);
96
+ }
97
+ function note(message, title = localized.status.note) {
98
+ if (message)
99
+ console.log(`${yellowBanner} ${warnPill(title)}${message}`);
100
+ }
101
+ function success(message, title = localized.status.success) {
102
+ if (message)
103
+ console.log(`${greenBanner} ${successPill(title)}${green(dot, message)}`);
104
+ }
105
+ function warn(message, title = localized.status.warn) {
106
+ if (message)
107
+ console.log(`${yellowBanner} ${warnPill(title)}${yellow(message)}`);
108
+ }
109
+ function error(message, title = localized.status.error) {
110
+ if (message)
111
+ console.log(`${redBanner} ${errorPill(title)}${red(dot, message)}`);
112
+ }
113
+ function cancel(message, title = localized.status.canceled) {
114
+ if (message)
115
+ console.log(`${redBanner} ${errorPill(title)}${red(dot, message)}`);
116
+ }
117
+ return {
118
+ log,
119
+ step,
120
+ info,
121
+ note,
122
+ success,
123
+ warn,
124
+ error,
125
+ cancel,
126
+ };
127
+ }
128
+
129
+ const messages = await getLocalized();
130
+ const logger = useLogger(messages);
131
+ class ShellError extends Error {
132
+ constructor(message) {
133
+ super(message);
134
+ // Убедимся, что экземпляр имеет правильный прототип
135
+ Object.setPrototypeOf(this, ShellError.prototype);
136
+ this.name = 'ShellError';
137
+ this.message = message;
138
+ Error.captureStackTrace(this, ShellError);
139
+ }
140
+ }
141
+ /**
142
+ * @typedef ExecutionResult
143
+ * @property {boolean} isDone
144
+ * @property {number} code
145
+ * @property {string} stderr
146
+ * @property {string} stdout
147
+ */
148
+ async function execAsync(command, args, options) {
149
+ args ??= [];
150
+ return new Promise((resolve, reject) => {
151
+ const runner = spawn(command, args, {
152
+ stdio: [
153
+ 'ignore', // stdin
154
+ 'pipe', // stdout
155
+ 'pipe', // stderr
156
+ ],
157
+ ...options,
158
+ shell: process.platform === 'win32',
159
+ });
160
+ const stdoutChunks = [];
161
+ const stderrChunks = [];
162
+ runner.stdout?.on('data', (chunk) => {
163
+ stdoutChunks.push(chunk);
164
+ });
165
+ runner.stderr?.on('data', (chunk) => {
166
+ stderrChunks.push(chunk);
167
+ });
168
+ runner.on('error', (error) => {
169
+ reject(error);
170
+ });
171
+ runner.on('exit', (code) => {
172
+ const isDone = code === 0;
173
+ const stdout = Buffer.concat(stdoutChunks).toString().trim();
174
+ const stderr = Buffer.concat(stderrChunks).toString().trim();
175
+ if (isDone) {
176
+ resolve({ isDone, code, stdout, stderr });
177
+ }
178
+ else {
179
+ reject(new ShellError(`Failed to execute command ${command} ${args.join(' ')}: ${stderr}`));
180
+ }
181
+ });
182
+ });
183
+ }
184
+ const runCommandAsync = async (command, args, options = {}) => await execAsync(command, args, { ...options });
185
+ const dryRunCommandAsync = (command, args) => {
186
+ logger.info(`${command} ${args?.join(' ')}`, 'Dry');
187
+ };
188
+ runCommandAsync.ifNotDry = (isDryRun) => isDryRun ? dryRunCommandAsync : runCommandAsync;
189
+
190
+ export { ShellError as S, getLocale as a, getLocalized as g, runCommandAsync as r, useLogger as u };