@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/release.mjs CHANGED
@@ -1,195 +1,295 @@
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, b as assertIsSyncedWithRemoteAsync, p as prompts, d as assertWorkflowResultAsync } from './github.mjs';
5
- import { g as getLocalized, u as useLogger, r as runCommandAsync } from './shell.mjs';
1
+ import { u as updateVersion, h as hasScript, g as getCurrentVersion } from './package.mjs';
2
+ import { inc, valid, prerelease } from 'semver';
3
+ import { a as assertNoParseErrors, l as logger, p as prompts, g as getRepositoryDetails, b as assertIsSyncedWithRemoteAsync, c as assertWorkflowResultAsync, r as runCommandAsync, t, d as checkIsInWorkTreeAsync } from './index.mjs';
6
4
  import chalk from 'chalk';
7
- import cliPackage from '../package.json' with { type: 'json' };
5
+ import { r as resolveConfigAsync } from './resolve.mjs';
8
6
  import 'node:path';
9
- import 'node:fs';
7
+ import 'p-map';
8
+ import 'node:fs/promises';
9
+ import '@mirta/workspace';
10
+ import '@mirta/package';
11
+ import './constants.mjs';
10
12
  import 'prompts';
13
+ import '@mirta/staged-args';
11
14
  import 'node:child_process';
12
- import 'node:url';
13
- import 'lodash.merge';
15
+ import '@mirta/i18n';
16
+ import '../package.json' with { type: 'json' };
17
+ import '@mirta/basics/fuzzy';
18
+ import 'node:os';
19
+ import 'node:path/posix';
20
+ import 'jsonc-parser';
21
+ import '@mirta/basics/object';
14
22
 
15
- const { yellow } = chalk;
16
- const allOptions = ({
17
- dry: {
23
+ const options = ({
24
+ 'config': {
25
+ type: 'string',
26
+ short: 'c',
27
+ },
28
+ 'dry-run': {
18
29
  type: 'boolean',
19
- default: false,
20
30
  },
21
- preid: {
31
+ 'preid': {
22
32
  type: 'string',
23
33
  },
24
- skipPrompts: {
34
+ 'skip-git': {
25
35
  type: 'boolean',
26
- default: false,
27
36
  },
28
- skipGit: {
37
+ 'skip-prompts': {
29
38
  type: 'boolean',
30
- default: false,
31
39
  },
32
- skipBuild: {
40
+ // Deprecated. Use 'skip-git' instead
41
+ 'skipGit': {
33
42
  type: 'boolean',
34
- default: false,
35
43
  },
36
- help: {
44
+ // Deprecated. Use 'skip-prompts' instead
45
+ 'skipPrompts': {
37
46
  type: 'boolean',
38
- short: 'h',
39
- default: false,
40
47
  },
41
- version: {
48
+ // Deprecated. Use 'dry-run' instead
49
+ 'dry': {
42
50
  type: 'boolean',
43
- short: 'v',
44
- default: false,
45
51
  },
46
52
  });
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);
53
+ function parseArgs(args) {
54
+ const parseResult = args.parseFinal(options);
55
+ assertNoParseErrors(parseResult);
56
+ const { values, positionals } = parseResult.data;
57
+ if (values.dry) {
58
+ logger.warn('Deprecated flag "--dry" used. Please use "--dry-run" instead');
59
+ values['dry-run'] = values['dry-run'] !== false;
60
+ }
61
+ if (values.skipGit) {
62
+ logger.warn('Deprecated flag "--skipGit" used. Please use "--skip-git" instead');
63
+ values['skip-git'] = values['skip-git'] !== false;
64
+ }
65
+ if (values.skipPrompts) {
66
+ logger.warn('Deprecated flag "--skipPrompts" used. Please use "--skip-prompts" instead');
67
+ values['skip-prompts'] = values['skip-prompts'] !== false;
68
+ }
69
+ return {
70
+ values,
71
+ positionals,
72
+ };
56
73
  }
57
- if (argv.version) {
58
- console.log(`${cliPackage.name} v${cliPackage.version}`);
59
- process.exit(0);
74
+
75
+ function getReleaseTypes(preid) {
76
+ const releaseTypes = [
77
+ 'patch',
78
+ 'minor',
79
+ 'major',
80
+ ...(preid
81
+ ? (['prepatch', 'preminor', 'premajor', 'prerelease'])
82
+ : []),
83
+ ];
84
+ return releaseTypes;
60
85
  }
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 assertIsSyncedWithRemoteAsync(repository);
86
+ async function determineTargetVersion(currentVersion, preid, skipPrompts, preferredVersion) {
87
+ let targetVersion = preferredVersion;
88
+ const releaseTypes = getReleaseTypes(preid);
89
+ if (!targetVersion) {
90
+ if (skipPrompts) {
91
+ logger.info('Skipping prompts. Default to "patch" release.');
92
+ targetVersion = inc(currentVersion, 'patch', undefined, preid) ?? '';
93
+ if (!targetVersion)
94
+ throw new Error(`Failed to compute target version from ${currentVersion}`);
95
+ if (!valid(targetVersion))
96
+ throw new Error(`Invalid target version: ${targetVersion}`);
97
+ return targetVersion;
98
+ }
99
+ const choices = releaseTypes
100
+ .map((releaseType) => {
101
+ const version = inc(currentVersion, releaseType, undefined, preid);
102
+ return {
103
+ title: `${releaseType} (${version})`,
104
+ value: version,
105
+ };
106
+ })
107
+ .filter(choice => choice.value !== null)
108
+ .concat({
109
+ title: 'custom',
110
+ value: 'custom',
111
+ });
112
+ const { release } = await prompts({
113
+ type: 'select',
114
+ name: 'release',
115
+ message: 'Select release type',
116
+ choices,
117
+ });
118
+ if (release === 'custom') {
119
+ const { version } = await prompts({
120
+ type: 'text',
121
+ name: 'version',
122
+ message: 'Type custom version',
123
+ initial: currentVersion,
124
+ });
125
+ targetVersion = version;
126
+ }
127
+ else {
128
+ targetVersion = release;
129
+ }
130
+ }
131
+ // Если вместо номера версии передан тип релиза — инкрементируем
132
+ //
133
+ if (releaseTypes.includes(targetVersion))
134
+ targetVersion = inc(currentVersion, targetVersion, undefined, preid) ?? '';
135
+ if (!targetVersion)
136
+ throw new Error(`Failed to compute target version from ${currentVersion}`);
137
+ if (!valid(targetVersion))
138
+ throw new Error(`Invalid target version: ${targetVersion}`);
139
+ return targetVersion;
94
140
  }
95
- else {
96
- logger.info('Repository: not in git work tree');
141
+
142
+ const { yellow: yellow$2 } = chalk;
143
+ async function runGitChecksAsync(context) {
144
+ if (!context.inWorkTree || context.skipGit)
145
+ return {};
146
+ const repoDetails = await getRepositoryDetails();
147
+ const { name: repository, connectionType } = repoDetails;
148
+ logger.log(`Repository: ${yellow$2(repository)}`);
149
+ await assertIsSyncedWithRemoteAsync(repository);
150
+ logger.step('Ensuring CI status for HEAD...');
151
+ await assertWorkflowResultAsync(repository, 'build');
152
+ return { repository, connectionType };
97
153
  }
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;
154
+
155
+ const { yellow: yellow$1 } = chalk;
156
+ async function executeReleaseAsync(context, config) {
157
+ const runAsync = runCommandAsync.dry(context.isDryRun);
158
+ let isCommitted = false;
159
+ try {
160
+ await updateVersion(context.targetVersion, config);
161
+ logger.info(t('release.versionUpdated', {
162
+ newVersion: yellow$1(`v${context.targetVersion}`),
163
+ }));
164
+ if (context.inWorkTree && !context.skipGit && hasScript('changelog')) {
165
+ logger.step(t('release.changelogGenerating'));
166
+ await runCommandAsync('pnpm', ['run', 'changelog'], { shell: true });
167
+ if (!context.skipPrompts) {
168
+ const { isContinue } = await prompts({
169
+ type: 'confirm',
170
+ name: 'isContinue',
171
+ message: t('release.changelogConfirm'),
172
+ });
173
+ if (!isContinue) {
174
+ logger.cancel(t('release.canceled'));
175
+ logger.step(t('release.versionReverting'));
176
+ await updateVersion(context.currentVersion, config);
177
+ logger.step(t('release.versionReverted', {
178
+ oldVersion: yellow$1(`v${context.currentVersion}`),
179
+ }));
180
+ return;
181
+ }
182
+ }
183
+ }
184
+ logger.step(t('release.lockfileUpdating'));
185
+ await runAsync('pnpm', ['install', '--prefer-offline'], { shell: true });
186
+ if (!context.inWorkTree || !context.repository) {
187
+ logger.note(t('release.final.noGit'));
188
+ return;
189
+ }
190
+ if (!context.skipGit && context.connectionType === 'ssh') {
191
+ const { stdout } = await runCommandAsync('git', ['diff'], { stdio: 'pipe' });
192
+ if (stdout) {
193
+ logger.step(t('release.committing'));
194
+ await runAsync('git', ['add', '-A']);
195
+ await runAsync('git', ['commit', '-m', `release: v${context.targetVersion}`]);
196
+ if (!context.isDryRun)
197
+ isCommitted = true;
198
+ logger.step(t('release.pushing'));
199
+ const tagName = `v${context.targetVersion}`;
200
+ const { stdout: existingTag } = await runCommandAsync('git', ['tag', '-l', tagName], { stdio: 'pipe' });
201
+ if (!existingTag) {
202
+ await runAsync('git', ['tag', tagName]);
203
+ }
204
+ else {
205
+ logger.warn(t('release.tagAlreadyExists', {
206
+ tag: yellow$1(tagName),
207
+ }));
208
+ }
209
+ await runAsync('git', ['push']);
210
+ await runAsync('git', ['push', 'origin', `refs/tags/v${context.targetVersion}`]);
211
+ logger.note(yellow$1(t('release.final.gitRemote')));
212
+ logger.note(t('release.final.gitRemoteStatus', {
213
+ workflowLink: `https://github.com/${context.repository}/actions/workflows/release.yml`,
214
+ }));
215
+ }
216
+ else {
217
+ logger.step(t('release.final.gitNoChanges'));
218
+ }
219
+ }
220
+ else {
221
+ logger.note(t('release.final.gitManual', {
222
+ version: yellow$1(`v${context.targetVersion}`),
223
+ }));
224
+ }
121
225
  }
122
- else {
123
- // Номер версии извлекается из предложенной ранее строки.
124
- targetVersion = release;
226
+ catch (e) {
227
+ if (!isCommitted) {
228
+ logger.step(t('release.versionReverting'));
229
+ try {
230
+ await updateVersion(context.currentVersion, config);
231
+ logger.step(t('release.versionReverted', {
232
+ oldVersion: yellow$1(`v${context.currentVersion}`),
233
+ }));
234
+ }
235
+ catch (rollbackError) {
236
+ logger.error(t('release.error.versionRevertingFailed')
237
+ + '\n'
238
+ + (rollbackError instanceof Error ? rollbackError.message : String(rollbackError)));
239
+ }
240
+ }
241
+ else {
242
+ logger.warn(t('release.error.versionAlreadyCommitted'));
243
+ }
244
+ throw e;
125
245
  }
126
246
  }
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 {
247
+
248
+ const { yellow } = chalk;
249
+ async function runAsync(args) {
250
+ // === 1. Парсинг аргументов ===
251
+ const { values: argv, positionals } = parseArgs(args);
252
+ const currentVersion = getCurrentVersion();
253
+ const preid = argv.preid ?? prerelease(currentVersion)?.[0];
254
+ const isDryRun = argv['dry-run'] ?? false;
255
+ const skipGit = argv['skip-git'] ?? false;
256
+ const skipPrompts = argv['skip-prompts'] ?? false;
257
+ // === 2. Проверка окружения ===
258
+ const inWorkTree = await checkIsInWorkTreeAsync();
259
+ // === 3. Git-проверки ===
260
+ const gitContext = await runGitChecksAsync({ inWorkTree, skipGit });
261
+ // === 4. Определение версии ===
262
+ const targetVersion = await determineTargetVersion(currentVersion, preid, skipPrompts, positionals[1]);
263
+ // === 5. Формирование контекста ===
264
+ const context = {
265
+ currentVersion,
266
+ targetVersion,
267
+ preid,
268
+ isDryRun,
269
+ skipGit,
270
+ skipPrompts,
271
+ inWorkTree,
272
+ ...gitContext,
273
+ };
274
+ // === 6. Подтверждение (если нужно) ===
275
+ if (!skipPrompts) {
140
276
  const { confirmRelease } = await prompts({
141
277
  type: 'confirm',
142
278
  name: 'confirmRelease',
143
- message: `Releasing v${targetVersion}. Continue?`,
279
+ message: `Releasing ${yellow(`v${targetVersion}`)} Continue?`,
144
280
  });
145
281
  if (!confirmRelease) {
146
- logger.cancel('No changes was made.');
282
+ logger.cancel('Release canceled. No changes made');
147
283
  return;
148
284
  }
149
285
  }
150
- if (inWorkTree) {
151
- logger.log('Ensuring CI status for HEAD...');
152
- await assertWorkflowResultAsync(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
- }
286
+ else {
287
+ logger.log(`Releasing ${yellow(`v${targetVersion}`)}`);
187
288
  }
289
+ // === 7. Загружаем конфиг ===
290
+ const { config: mirtaConfig } = await resolveConfigAsync(process.cwd(), argv.config);
291
+ // === 8. Выполнение релиза ===
292
+ await executeReleaseAsync(context, mirtaConfig);
188
293
  }
189
- await runAsync().catch((e) => {
190
- if (isVersionUpdated) {
191
- // Revert version changes on failed release
192
- updateVersion(currentVersion);
193
- }
194
- throw e;
195
- });
294
+
295
+ export { runAsync };