@mirta/cli 0.3.4 → 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/README.md +295 -78
- package/README.ru.md +303 -79
- package/dist/constants.mjs +9 -0
- package/dist/deploy.mjs +902 -0
- package/dist/index.mjs +759 -20
- package/dist/package.mjs +128 -172
- package/dist/publish.mjs +58 -38
- package/dist/release.mjs +258 -158
- package/dist/resolve.mjs +457 -0
- package/locales/en-US.json +58 -0
- package/locales/ru-RU.json +58 -0
- package/package.json +21 -9
- package/dist/github.mjs +0 -135
- package/dist/locales/en-US.json +0 -21
- package/dist/locales/ru-RU.json +0 -21
- package/dist/shell.mjs +0 -189
package/dist/resolve.mjs
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { T as THIS_PACKAGE_NAME } from './constants.mjs';
|
|
4
|
+
import { toPosix } from '@mirta/package';
|
|
5
|
+
import { resolve, relative, sep } from 'node:path';
|
|
6
|
+
import { join } from 'node:path/posix';
|
|
7
|
+
import jsonc from 'jsonc-parser';
|
|
8
|
+
import { deepMerge } from '@mirta/basics/object';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Специализированный класс для обработки ошибок, связанных с ресурсами проекта.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* throw SourceError.get('file.accessDenied', '/path/to/file')
|
|
16
|
+
* ```
|
|
17
|
+
* @since 0.4.0
|
|
18
|
+
*
|
|
19
|
+
**/
|
|
20
|
+
class SourceError extends Error {
|
|
21
|
+
/**
|
|
22
|
+
* Код ошибки для программной идентификации.
|
|
23
|
+
*
|
|
24
|
+
* Позволяет точно определить причину ошибки в обработчиках `try/catch`.
|
|
25
|
+
*
|
|
26
|
+
**/
|
|
27
|
+
code;
|
|
28
|
+
/**
|
|
29
|
+
* Приватный конструктор, используемый только внутри
|
|
30
|
+
* класса для создания экземпляров ошибки.
|
|
31
|
+
*
|
|
32
|
+
* @param message - Полное сообщение об ошибке.
|
|
33
|
+
* @param code - Код ошибки для идентификации.
|
|
34
|
+
* @param scope - Пространство имён или модуль, в котором возникла ошибка.
|
|
35
|
+
* По умолчанию — {@link THIS_PACKAGE_NAME}.
|
|
36
|
+
*
|
|
37
|
+
**/
|
|
38
|
+
constructor(message, code, scope) {
|
|
39
|
+
super(`[${scope ?? THIS_PACKAGE_NAME}] ${message}`);
|
|
40
|
+
this.name = 'SourceError';
|
|
41
|
+
this.code = code;
|
|
42
|
+
// Захватываем стек вызовов, исключая фабричный метод `get`,
|
|
43
|
+
// чтобы улучшить читаемость трассировки.
|
|
44
|
+
//
|
|
45
|
+
if ('captureStackTrace' in Error)
|
|
46
|
+
Error.captureStackTrace(this, scope
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
48
|
+
? SourceError.getScoped
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
50
|
+
: SourceError.get);
|
|
51
|
+
}
|
|
52
|
+
/** Карта кодов ошибок с соответствующими сообщениями. */
|
|
53
|
+
static codeMappings = {
|
|
54
|
+
'path.outsideRoot': (path) => `Path "${path}" is outside the root directory`,
|
|
55
|
+
'file.notFound': (filePath) => `File not found: "${filePath}"`,
|
|
56
|
+
'file.accessDenied': (filePath) => `Access denied to "${filePath}"`,
|
|
57
|
+
'file.failedToRead': (filePath, reason) => `Failed to read "${filePath}": ${reason ?? 'unknown reason'}`,
|
|
58
|
+
/**
|
|
59
|
+
* Ошибка парсинга JSON.
|
|
60
|
+
*
|
|
61
|
+
* @param filePath - Путь к файлу с некорректным JSON.
|
|
62
|
+
* @param message - Сообщение об ошибке от парсера (например, `Unexpected token }`).
|
|
63
|
+
*
|
|
64
|
+
**/
|
|
65
|
+
'parse.invalidJson': (filePath, message) => `Invalid JSON in file "${filePath}": ${message}`,
|
|
66
|
+
/**
|
|
67
|
+
* Ошибка, когда корневой элемент JSON не является объектом (`{}`).
|
|
68
|
+
*
|
|
69
|
+
**/
|
|
70
|
+
'parse.invalidJsonRoot': () => 'Invalid JSON: root must be an object, not an array or primitive value',
|
|
71
|
+
};
|
|
72
|
+
static isFileError(error) {
|
|
73
|
+
return error instanceof SourceError && error.code.startsWith('file.');
|
|
74
|
+
}
|
|
75
|
+
static isParseError(error) {
|
|
76
|
+
return error instanceof SourceError && error.code.startsWith('parse.');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Фабричный метод для создания экземпляра ошибки по её коду.
|
|
80
|
+
*
|
|
81
|
+
* Автоматически подставляет сообщение из `codeMappings` и формирует ошибку с заданными параметрами.
|
|
82
|
+
*
|
|
83
|
+
* @template T - Ограниченный ключами `codeMappings` тип, гарантирующий корректность кода.
|
|
84
|
+
* @param code - Код ошибки (например, `'alreadyDefined'`).
|
|
85
|
+
* @param args - Аргументы, соответствующие параметрам функции сообщения из `codeMappings`.
|
|
86
|
+
* @returns Новый экземпляр {@link SourceError} с шаблонным сообщением.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const error = SourceError.get('file.notFound', '/path/to/file.json')
|
|
91
|
+
* ```
|
|
92
|
+
**/
|
|
93
|
+
static get(code, ...args) {
|
|
94
|
+
const messageFn = this.codeMappings[code];
|
|
95
|
+
const message = messageFn(...args);
|
|
96
|
+
return new SourceError(message, code);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Фабричный метод, аналогичный `get`, но с возможностью указать
|
|
100
|
+
* пользовательское пространство имён (scope).
|
|
101
|
+
*
|
|
102
|
+
* Полезно при использовании в других модулях, где нужно указать
|
|
103
|
+
* иной контекст ошибки.
|
|
104
|
+
*
|
|
105
|
+
* @template TKey - Тип кода ошибки, аналогично `get`.
|
|
106
|
+
*
|
|
107
|
+
* @param scope - Пространство имён ошибки (например, `'@mirta/bots-remote'`).
|
|
108
|
+
* @param code - Код ошибки.
|
|
109
|
+
* @param args - Аргументы для формирования сообщения.
|
|
110
|
+
*
|
|
111
|
+
* @returns Новый экземпляр {@link SourceError} с пользовательским
|
|
112
|
+
* префиксом и шаблонным сообщением.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
*
|
|
116
|
+
* ```ts
|
|
117
|
+
* const error = SourceError.getScoped('@mirta/bots-remote', 'file.accessDenied', '/path/to/file')
|
|
118
|
+
* ```
|
|
119
|
+
**/
|
|
120
|
+
static getScoped(scope, code, ...args) {
|
|
121
|
+
const messageFn = this.codeMappings[code];
|
|
122
|
+
const message = messageFn(...args);
|
|
123
|
+
return new SourceError(message, code, scope);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Асинхронно проверяет, существует ли файл или директория по указанному пути.
|
|
129
|
+
*
|
|
130
|
+
* Использует `fs.access`, чтобы обойти ограничения `fs.existsSync` в асинхронной среде.
|
|
131
|
+
*
|
|
132
|
+
* @param path - Путь к файлу или директории.
|
|
133
|
+
* @returns `true`, если путь существует и доступен, иначе `false`.
|
|
134
|
+
*
|
|
135
|
+
* @since 0.4.0
|
|
136
|
+
*
|
|
137
|
+
**/
|
|
138
|
+
async function isExistsAsync(path) {
|
|
139
|
+
try {
|
|
140
|
+
await access(path);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT')
|
|
145
|
+
return false;
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Разрешает относительный путь внутри заданной корневой директории.
|
|
151
|
+
*
|
|
152
|
+
* Проверяет, что итоговый путь не выходит за пределы `rootDir` (защита от `../../../` атак).
|
|
153
|
+
* Возвращает путь в POSIX-формате (с `/`), независимо от ОС.
|
|
154
|
+
*
|
|
155
|
+
* @param rootDir - Корневая директория, внутри которой должно происходить разрешение.
|
|
156
|
+
* @param targetPath - Целевой путь (может быть относительным или абсолютным).
|
|
157
|
+
* @returns Относительный путь от `rootDir` в формате POSIX.
|
|
158
|
+
* @throws {SourceError} Если результирующий путь находится вне `rootDir`.
|
|
159
|
+
*
|
|
160
|
+
* @since 0.4.0
|
|
161
|
+
*
|
|
162
|
+
**/
|
|
163
|
+
function resolveSubpath(rootDir, targetPath) {
|
|
164
|
+
const resolvedRoot = resolve(rootDir);
|
|
165
|
+
const resolvedTarget = resolve(resolvedRoot, targetPath);
|
|
166
|
+
const relativePath = relative(resolvedRoot, resolvedTarget);
|
|
167
|
+
if (relativePath.startsWith('..') || relativePath.includes(`${sep}..`))
|
|
168
|
+
throw SourceError.get('path.outsideRoot', targetPath);
|
|
169
|
+
return toPosix(relativePath);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Заменяет `~` в начале пути на домашнюю директорию пользователя.
|
|
173
|
+
*
|
|
174
|
+
* На Unix-подобных системах (Linux, macOS):
|
|
175
|
+
* - `~/dir` → `/home/username/dir`
|
|
176
|
+
* - `~/.ssh` → `/Users/username/.ssh`
|
|
177
|
+
*
|
|
178
|
+
* На Windows:
|
|
179
|
+
* - Путь остаётся без изменений (`~` не раскрывается).
|
|
180
|
+
* - Это сделано для совместимости с WSL2 и shell-средами, где `~` обрабатывается отдельно.
|
|
181
|
+
*
|
|
182
|
+
* @param path - Входной путь, возможно, начинающийся с `~`.
|
|
183
|
+
* @returns Путь с раскрытой домашней директорией (на Unix) или исходный путь (на Windows).
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* expandHomeDir('~/projects') // → '/home/user/projects' (Linux)
|
|
187
|
+
* expandHomeDir('~/config') // → '/Users/user/config' (macOS)
|
|
188
|
+
* expandHomeDir('~/data') // → '~/data' (Windows — без изменений)
|
|
189
|
+
*
|
|
190
|
+
* @since 0.4.0
|
|
191
|
+
*
|
|
192
|
+
**/
|
|
193
|
+
function expandHomeDir(path) {
|
|
194
|
+
if (!path.startsWith('~') || process.platform === 'win32')
|
|
195
|
+
return path;
|
|
196
|
+
return path.replace(/^~($|\/|\\)/, homedir() + '$1');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Специализированный класс для обработки ошибок, связанных с разбором JSONC.
|
|
201
|
+
*
|
|
202
|
+
* @since 0.4.0
|
|
203
|
+
*
|
|
204
|
+
**/
|
|
205
|
+
class JsoncSyntaxError extends Error {
|
|
206
|
+
/**
|
|
207
|
+
* Конструктор для создания экземпляра ошибки синтаксиса JSONC.
|
|
208
|
+
*
|
|
209
|
+
* @param message - Полное сообщение об ошибке.
|
|
210
|
+
* @param offset - Позиция начала ошибки в исходной строке.
|
|
211
|
+
* @param length - Длина фрагмента с ошибкой.
|
|
212
|
+
*
|
|
213
|
+
**/
|
|
214
|
+
constructor(message, offset, length) {
|
|
215
|
+
super(`[${THIS_PACKAGE_NAME}] ${message} at offset ${offset}, length ${length}`);
|
|
216
|
+
this.name = 'JsoncSyntaxError';
|
|
217
|
+
Error.captureStackTrace(this, this.constructor);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const errorMessages = new Map([
|
|
222
|
+
[1 /* jsonc.ParseErrorCode.InvalidSymbol */, 'Invalid symbol encountered'],
|
|
223
|
+
[2 /* jsonc.ParseErrorCode.InvalidNumberFormat */, 'Invalid number format'],
|
|
224
|
+
[3 /* jsonc.ParseErrorCode.PropertyNameExpected */, 'Property name expected'],
|
|
225
|
+
[4 /* jsonc.ParseErrorCode.ValueExpected */, 'Value expected'],
|
|
226
|
+
[5 /* jsonc.ParseErrorCode.ColonExpected */, 'Colon expected'],
|
|
227
|
+
[6 /* jsonc.ParseErrorCode.CommaExpected */, 'Comma expected'],
|
|
228
|
+
[7 /* jsonc.ParseErrorCode.CloseBraceExpected */, 'Closing brace expected'],
|
|
229
|
+
[8 /* jsonc.ParseErrorCode.CloseBracketExpected */, 'Closing bracket expected'],
|
|
230
|
+
[9 /* jsonc.ParseErrorCode.EndOfFileExpected */, 'Unexpected end of file'],
|
|
231
|
+
[10 /* jsonc.ParseErrorCode.InvalidCommentToken */, 'Invalid comment token'],
|
|
232
|
+
[11 /* jsonc.ParseErrorCode.UnexpectedEndOfComment */, 'Unexpected end of comment'],
|
|
233
|
+
[12 /* jsonc.ParseErrorCode.UnexpectedEndOfString */, 'Unexpected end of string'],
|
|
234
|
+
[13 /* jsonc.ParseErrorCode.UnexpectedEndOfNumber */, 'Unexpected end of number'],
|
|
235
|
+
[14 /* jsonc.ParseErrorCode.InvalidUnicode */, 'Invalid Unicode escape'],
|
|
236
|
+
[15 /* jsonc.ParseErrorCode.InvalidEscapeCharacter */, 'Invalid escape character'],
|
|
237
|
+
[16 /* jsonc.ParseErrorCode.InvalidCharacter */, 'Invalid character'],
|
|
238
|
+
]);
|
|
239
|
+
function getErrorMessage(errorCode) {
|
|
240
|
+
return errorMessages.get(errorCode) ?? 'Unknown parsing error';
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Обёртка для определения конфигурации. Позволяет использовать подсказки типов TypeScript.
|
|
244
|
+
*
|
|
245
|
+
* На этапе выполнения просто возвращает переданный объект без изменений.
|
|
246
|
+
* Предназначена для улучшения DX (developer experience).
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* export default defineConfig({
|
|
251
|
+
* deploy: {
|
|
252
|
+
* profiles: { ... }
|
|
253
|
+
* }
|
|
254
|
+
* })
|
|
255
|
+
* ```
|
|
256
|
+
* @param config - Объект конфигурации Mirta.
|
|
257
|
+
* @returns Тот же объект, что и входе.
|
|
258
|
+
*
|
|
259
|
+
* @since 0.4.0
|
|
260
|
+
*
|
|
261
|
+
**/
|
|
262
|
+
function defineConfig(config) {
|
|
263
|
+
return config;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Парсит строку JSON и проверяет, что корневой элемент — это объект (не массив и не примитив).
|
|
267
|
+
*
|
|
268
|
+
* Используется для валидации содержимого конфигурационного файла перед приведением к типу `MirtaConfig`.
|
|
269
|
+
*
|
|
270
|
+
* @param content - Строка с содержимым JSON-файла.
|
|
271
|
+
* @returns Распарсенный объект.
|
|
272
|
+
* @throws {SourceError} Если JSON имеет неверный формат или корень не является объектом.
|
|
273
|
+
*
|
|
274
|
+
* @since 0.4.0
|
|
275
|
+
*
|
|
276
|
+
**/
|
|
277
|
+
function parseConfigJson(content) {
|
|
278
|
+
const errors = [];
|
|
279
|
+
const parsed = jsonc.parse(content, errors, {
|
|
280
|
+
allowTrailingComma: true,
|
|
281
|
+
});
|
|
282
|
+
// Проверяем, есть ли ошибки парсинга
|
|
283
|
+
if (errors.length > 0) {
|
|
284
|
+
const firstError = errors[0];
|
|
285
|
+
throw new JsoncSyntaxError(getErrorMessage(firstError.error), firstError.offset, firstError.length);
|
|
286
|
+
}
|
|
287
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
288
|
+
throw SourceError.get('parse.invalidJsonRoot');
|
|
289
|
+
}
|
|
290
|
+
return parsed;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Асинхронно читает и парсит конфигурационный файл.
|
|
294
|
+
*
|
|
295
|
+
* @param rootDir - Корневая директория проекта.
|
|
296
|
+
* @param pathInput - Относительный путь к конфигурационному файлу (например, 'mirta.config.json').
|
|
297
|
+
* @returns Объект конфигурации или `undefined`, если файл не существует.
|
|
298
|
+
* @throws {SourceError} С различными кодами ошибок в зависимости от типа проблемы:
|
|
299
|
+
* - `parse.invalidJson` — невалидный JSON
|
|
300
|
+
* - `file.notFound` — файл не найден
|
|
301
|
+
* - `file.accessDenied` — нет прав на чтение
|
|
302
|
+
* - `file.failedToRead` — другие ошибки чтения
|
|
303
|
+
*
|
|
304
|
+
* @since 0.4.0
|
|
305
|
+
*
|
|
306
|
+
**/
|
|
307
|
+
async function readConfigAsync(rootDir, pathInput) {
|
|
308
|
+
const configPath = join(rootDir, resolveSubpath(rootDir, pathInput));
|
|
309
|
+
if (!await isExistsAsync(configPath))
|
|
310
|
+
return;
|
|
311
|
+
try {
|
|
312
|
+
const content = await readFile(configPath, 'utf-8');
|
|
313
|
+
return parseConfigJson(content);
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
if (e instanceof SourceError)
|
|
317
|
+
throw e;
|
|
318
|
+
if (e instanceof JsoncSyntaxError)
|
|
319
|
+
throw e;
|
|
320
|
+
if (e && typeof e === 'object' && 'code' in e) {
|
|
321
|
+
switch (e.code) {
|
|
322
|
+
case 'ENOENT':
|
|
323
|
+
throw SourceError.get('file.notFound', configPath);
|
|
324
|
+
case 'EACCES':
|
|
325
|
+
case 'EPERM':
|
|
326
|
+
throw SourceError.get('file.accessDenied', configPath);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const message = e instanceof Error
|
|
330
|
+
? e.message
|
|
331
|
+
: String(e);
|
|
332
|
+
throw SourceError.get('file.failedToRead', configPath, message);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Имя файла конфигурации по умолчанию.
|
|
338
|
+
*
|
|
339
|
+
* @since 0.4.0
|
|
340
|
+
*
|
|
341
|
+
**/
|
|
342
|
+
const DEFAULT_CONFIG_FILE = 'mirta.config.json';
|
|
343
|
+
/**
|
|
344
|
+
* Имя пользователя по умолчанию для SSH-подключения.
|
|
345
|
+
*
|
|
346
|
+
* Используется, когда значение отсутствует в строке подключения.
|
|
347
|
+
*
|
|
348
|
+
* @since 0.4.0
|
|
349
|
+
*
|
|
350
|
+
**/
|
|
351
|
+
const DEFAULT_SSH_USERNAME = 'root';
|
|
352
|
+
/**
|
|
353
|
+
* Адрес хоста по умолчанию для подключения к контроллеру.
|
|
354
|
+
*
|
|
355
|
+
* Используется, когда значение отсутствует в строке подключения.
|
|
356
|
+
*
|
|
357
|
+
* @since 0.4.0
|
|
358
|
+
*
|
|
359
|
+
**/
|
|
360
|
+
const DEFAULT_SSH_HOSTNAME = '192.168.42.1';
|
|
361
|
+
/**
|
|
362
|
+
* Стандартный порт SSH.
|
|
363
|
+
*
|
|
364
|
+
* @since 0.4.0
|
|
365
|
+
*
|
|
366
|
+
**/
|
|
367
|
+
const KNOWN_SSH_PORT = 22;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Экспортируемая конфигурация по умолчанию (zero-config).
|
|
371
|
+
*
|
|
372
|
+
* Содержит:
|
|
373
|
+
* - Подключение `default` по SSH к стандартному адресу
|
|
374
|
+
* - Профиль деплоя `default`, использующий это подключение
|
|
375
|
+
* - Маппинг `wb-rules-es5` для синхронизации скомпилированных модулей и правил wb-rules
|
|
376
|
+
*
|
|
377
|
+
* @since 0.4.0
|
|
378
|
+
*
|
|
379
|
+
**/
|
|
380
|
+
var defaultConfig = defineConfig({
|
|
381
|
+
/**
|
|
382
|
+
* Список подключений по умолчанию.
|
|
383
|
+
*
|
|
384
|
+
* Содержит одно соединение `default`,
|
|
385
|
+
* соответствующее контроллеру Wiren Board в Debug-режиме (подключение USB-кабелем).
|
|
386
|
+
*
|
|
387
|
+
**/
|
|
388
|
+
connections: {
|
|
389
|
+
'default': `ssh://${DEFAULT_SSH_USERNAME}@${DEFAULT_SSH_HOSTNAME}`,
|
|
390
|
+
},
|
|
391
|
+
deploy: {
|
|
392
|
+
/**
|
|
393
|
+
* Предустановленные маппинги файлов.
|
|
394
|
+
*
|
|
395
|
+
* Маппинг 'wb-rules-es5' включает:
|
|
396
|
+
* - Синхронизацию модулей wb-rules
|
|
397
|
+
* - Синхронизацию скриптов wb-rules
|
|
398
|
+
* - Защиту файла alarms.conf от удаления
|
|
399
|
+
* - Очистку лишних файлов на контроллере (cleanup: true)
|
|
400
|
+
*
|
|
401
|
+
**/
|
|
402
|
+
mappings: {
|
|
403
|
+
'wb-rules-es5': [
|
|
404
|
+
{
|
|
405
|
+
from: 'dist/es5/wb-rules-modules',
|
|
406
|
+
to: '/mnt/data/etc/wb-rules-modules',
|
|
407
|
+
cleanup: true,
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
from: 'dist/es5/wb-rules',
|
|
411
|
+
to: '/mnt/data/etc/wb-rules',
|
|
412
|
+
cleanup: true,
|
|
413
|
+
protect: ['alarms.conf'],
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
/**
|
|
418
|
+
* Профили деплоя по умолчанию.
|
|
419
|
+
*
|
|
420
|
+
* Профиль 'default':
|
|
421
|
+
* - Использует подключение 'default'
|
|
422
|
+
* - Применяет маппинг 'wb-rules-es5'
|
|
423
|
+
*
|
|
424
|
+
**/
|
|
425
|
+
profiles: {
|
|
426
|
+
default: {
|
|
427
|
+
connection: 'default',
|
|
428
|
+
mappings: ['wb-rules-es5'],
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Асинхронно разрешает итоговую конфигурацию для проекта.
|
|
436
|
+
*
|
|
437
|
+
* 1. Читает пользовательский конфиг из указанного пути или из `mirta.config.json` по умолчанию.
|
|
438
|
+
* 2. Если путь указан явно, но файл не найден — выбрасывает ошибку.
|
|
439
|
+
* 3. Сливает пользовательскую конфигурацию с конфигурацией по умолчанию (глубокое слияние).
|
|
440
|
+
*
|
|
441
|
+
* @param rootDir - Корневая директория проекта.
|
|
442
|
+
* @param path - Путь к пользовательскому конфигурационному файлу (опционально).
|
|
443
|
+
* @returns Объект `ResolvedConfig` с итоговой и пользовательской конфигурацией.
|
|
444
|
+
* @throws {SourceError} Если файл не найден при явном указании пути.
|
|
445
|
+
*
|
|
446
|
+
**/
|
|
447
|
+
async function resolveConfigAsync(rootDir, path) {
|
|
448
|
+
const userConfig = await readConfigAsync(rootDir, path ?? DEFAULT_CONFIG_FILE);
|
|
449
|
+
if (!userConfig && path)
|
|
450
|
+
throw SourceError.get('file.notFound', path);
|
|
451
|
+
return {
|
|
452
|
+
config: deepMerge(defaultConfig, userConfig),
|
|
453
|
+
userConfig,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export { DEFAULT_SSH_USERNAME as D, KNOWN_SSH_PORT as K, resolveSubpath as a, expandHomeDir as e, isExistsAsync as i, resolveConfigAsync as r };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"command.suggest": "{input} → did you mean {suggestion}?",
|
|
3
|
+
"command.notFound": "Unknown command: {input}",
|
|
4
|
+
"args.errorHeader": "Invalid CLI {count, plural, one{argument} other{arguments}}",
|
|
5
|
+
"args.unknownOption": "{option} → unknown option",
|
|
6
|
+
"args.unknownOptionSuggest": "{option} → did you mean {suggestion}?",
|
|
7
|
+
"args.missingValue": "{option} → missing value",
|
|
8
|
+
"label.debug": "Debug",
|
|
9
|
+
"label.info": "Info",
|
|
10
|
+
"label.warning": "Warning",
|
|
11
|
+
"label.error": "Error",
|
|
12
|
+
"label.success": "Success",
|
|
13
|
+
"label.canceled": "Canceled",
|
|
14
|
+
"step.canceled": "Operation canceled",
|
|
15
|
+
"connection.emptyParameterSkipped": "Empty connection parameter '{key}' skipped",
|
|
16
|
+
"connection.ttlSkipped": "TTL parameter skipped — no pkcs11 or key specified",
|
|
17
|
+
"package.templateOutsideRoot": "Template '{template}' is outside the root directory",
|
|
18
|
+
"release.versionUpdated": "Version updated to {newVersion}",
|
|
19
|
+
"release.versionReverting": "Release canceled. Reverting version...",
|
|
20
|
+
"release.versionReverted": "Version reverted to {oldVersion}",
|
|
21
|
+
"release.error.versionRevertingFailed": "Failed to revert version to {oldVersion}. Please manually resolve the issue",
|
|
22
|
+
"release.error.versionAlreadyCommitted": "Version was already committed. Please manually resolve the issue.",
|
|
23
|
+
"release.changelogGenerating": "Generating changelog...",
|
|
24
|
+
"release.changelogConfirm": "Changelog generated. Does it look good?",
|
|
25
|
+
"release.canceled": "Release canceled",
|
|
26
|
+
"release.lockfileUpdating": "Updating lockfile...",
|
|
27
|
+
"release.committing": "Committing changes...",
|
|
28
|
+
"release.pushing": "Pushing to remote...",
|
|
29
|
+
"release.tagAlreadyExists": "Tag {tag} already exists, skipping tag creation",
|
|
30
|
+
"release.final.gitNoChanges": "No changes to commit",
|
|
31
|
+
"release.final.gitManual": "Commit the changes with message 'release: {version}' and tag {version}",
|
|
32
|
+
"release.final.gitRemote": "Release initiated. Continuing in GitHub Actions",
|
|
33
|
+
"release.final.gitRemoteStatus": "Check status at {workflowLink}",
|
|
34
|
+
"release.final.noGit": "Version update complete. Git operations skipped",
|
|
35
|
+
"publish.begin": "Publishing packages...",
|
|
36
|
+
"publish.newPackages": "New packages: {packages}",
|
|
37
|
+
"publish.initialPublishRequired": "Trusted Publisher requires manual initial publish",
|
|
38
|
+
"publish.skippingPrivate": "Skipping private {name}",
|
|
39
|
+
"publish.skippingPublished": "Skipping already published {name}",
|
|
40
|
+
"publish.packagePublishing": "Publishing {name}",
|
|
41
|
+
"publish.packagePublished": "Published {name}",
|
|
42
|
+
"wsl.notInstalled": "WSL2 is not installed",
|
|
43
|
+
"wsl.noDistros": "No WSL2 distributions found",
|
|
44
|
+
"wsl.noDefault": "No default WSL distribution set",
|
|
45
|
+
"wsl.distroNotFound": "WSL distribution {name} not found",
|
|
46
|
+
"wsl.distroNotWsl2": "Distribution {name} is not WSL2",
|
|
47
|
+
"wsl.error": "WSL error: {error}",
|
|
48
|
+
"deploy.useDedicatedGroup": "Use dedicated group (e.g. '{group}') to prevent system file loss",
|
|
49
|
+
"deploy.deploying": "Deploying to {target} ({mode}), {profile} profile",
|
|
50
|
+
"deploy.profileNotFound": "Deploy profile '{name}' not found",
|
|
51
|
+
"deploy.profileImplicit": "{name} (implicit)",
|
|
52
|
+
"deploy.mappingsNotFound": "Mappings not found: {key}",
|
|
53
|
+
"deploy.mappingDisabled": "Skipping disabled: {from} → {to}",
|
|
54
|
+
"deploy.sourceNotExists": "Skipping non-existent: {source}",
|
|
55
|
+
"deploy.transmitting": "Transmitting {from} → {to}",
|
|
56
|
+
"deploy.simulationComplete": "Simulation complete. No changes applied.",
|
|
57
|
+
"deploy.successful": "🎉 Deployment successful!"
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"command.suggest": "{input} → возможно, {suggestion}?",
|
|
3
|
+
"command.notFound": "Неизвестная команда: {input}",
|
|
4
|
+
"args.errorHeader": "{count, plural, one{Недопустимый аргумент} other{Недопустимые аргументы}} CLI",
|
|
5
|
+
"args.unknownOption": "{option} → неизвестный параметр",
|
|
6
|
+
"args.unknownOptionSuggest": "{option} → возможно, {suggestion}?",
|
|
7
|
+
"args.missingValue": "{option} → отсутствует значение",
|
|
8
|
+
"label.debug": "Отладка",
|
|
9
|
+
"label.info": "Инфо",
|
|
10
|
+
"label.warning": "Предупреждение",
|
|
11
|
+
"label.error": "Ошибка",
|
|
12
|
+
"label.success": "Успешно",
|
|
13
|
+
"label.canceled": "Отмена",
|
|
14
|
+
"step.canceled": "Операция отменена",
|
|
15
|
+
"connection.emptyParameterSkipped": "Пустой параметр подключения '{key}' пропущен",
|
|
16
|
+
"connection.ttlSkipped": "Параметр TTL пропущен — не указаны PKCS#11 или ключ",
|
|
17
|
+
"package.templateOutsideRoot": "Пропуск шаблона {template} — вне корневой директории",
|
|
18
|
+
"release.versionUpdated": "Версия обновлена до {newVersion}",
|
|
19
|
+
"release.versionReverting": "Релиз отменён. Откат версии...",
|
|
20
|
+
"release.versionReverted": "Версия возвращена к {oldVersion}",
|
|
21
|
+
"release.error.versionRevertingFailed": "Не удалось вернуть версию к {oldVersion}, устраните проблему вручную",
|
|
22
|
+
"release.error.versionAlreadyCommitted": "Версия уже закоммичена. Устраните проблему вручную",
|
|
23
|
+
"release.changelogGenerating": "Генерация changelog...",
|
|
24
|
+
"release.changelogConfirm": "Changelog сгенерирован. Всё выглядит хорошо?",
|
|
25
|
+
"release.canceled": "Релиз отменён",
|
|
26
|
+
"release.lockfileUpdating": "Обновление lockfile...",
|
|
27
|
+
"release.committing": "Коммит изменений...",
|
|
28
|
+
"release.pushing": "Отправка в удалённый репозиторий...",
|
|
29
|
+
"release.tagAlreadyExists": "Тег {tag} уже существует — создание не требуется",
|
|
30
|
+
"release.final.gitNoChanges": "Нет изменений для коммита",
|
|
31
|
+
"release.final.gitManual": "Выполните коммит с сообщением 'release: {version}' и тегом {version}",
|
|
32
|
+
"release.final.gitRemote": "Релиз инициирован. Дальнейшие шаги — в GitHub Actions",
|
|
33
|
+
"release.final.gitRemoteStatus": "Статус: {workflowLink}",
|
|
34
|
+
"release.final.noGit": "Версии обновлены. Операции с Git пропущены",
|
|
35
|
+
"publish.begin": "Публикация пакетов...",
|
|
36
|
+
"publish.newPackages": "Новые пакеты: {packages}",
|
|
37
|
+
"publish.initialPublishRequired": "Trusted Publisher требует ручную первичную публикацию",
|
|
38
|
+
"publish.skippingPrivate": "Пропуск приватного пакета {name}",
|
|
39
|
+
"publish.skippingPublished": "Пропуск уже опубликованного пакета {name}",
|
|
40
|
+
"publish.packagePublishing": "Публикация пакета {name}",
|
|
41
|
+
"publish.packagePublished": "Пакет {name} опубликован",
|
|
42
|
+
"wsl.notInstalled": "WSL не установлен",
|
|
43
|
+
"wsl.noDistros": "WSL не содержит ни одного дистрибутива",
|
|
44
|
+
"wsl.noDefault": "Не найден дистрибутив WSL по умолчанию",
|
|
45
|
+
"wsl.distroNotFound": "Дистрибутив WSL {name} не найден",
|
|
46
|
+
"wsl.distroNotWsl2": "Дистрибутив {name} не поддерживает WSL2",
|
|
47
|
+
"wsl.error": "Ошибка WSL: {error}",
|
|
48
|
+
"deploy.useDedicatedGroup": "Используйте группы (например, '{group}') для защиты системных файлов",
|
|
49
|
+
"deploy.deploying": "Деплой по адресу {target} ({mode}), профиль {profile}",
|
|
50
|
+
"deploy.profileNotFound": "Профиль деплоя '{name}' не найден",
|
|
51
|
+
"deploy.profileImplicit": "{name} (встроенный)",
|
|
52
|
+
"deploy.mappingsNotFound": "Набор маппингов не найден: {key}",
|
|
53
|
+
"deploy.mappingDisabled": "Выключено: {from} → {to}",
|
|
54
|
+
"deploy.sourceNotExists": "Не найдено: {source}",
|
|
55
|
+
"deploy.transmitting": "Передача {from} → {to}",
|
|
56
|
+
"deploy.simulationComplete": "Симуляция завершена. Изменения не применены.",
|
|
57
|
+
"deploy.successful": "🎉 Деплой успешно выполнен!"
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirta/cli",
|
|
3
3
|
"description": "🛠️ Mirta Framework - the CLI",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"license": "Unlicense",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"cli",
|
|
8
8
|
"mirta",
|
|
9
9
|
"wb-rules"
|
|
10
10
|
],
|
|
11
|
-
"engines": {
|
|
12
|
-
"node": ">=20.6.0"
|
|
13
|
-
},
|
|
14
11
|
"type": "module",
|
|
15
12
|
"files": [
|
|
16
13
|
"bin",
|
|
17
14
|
"dist",
|
|
15
|
+
"locales",
|
|
18
16
|
"LICENSE",
|
|
19
17
|
"README.md"
|
|
20
18
|
],
|
|
@@ -23,6 +21,11 @@
|
|
|
23
21
|
},
|
|
24
22
|
"imports": {
|
|
25
23
|
"#src/*": "./src/*.js",
|
|
24
|
+
"#src/i18n": "./src/i18n/index.js",
|
|
25
|
+
"#src/auth": "./src/auth/index.js",
|
|
26
|
+
"#src/staged-args": "./src/staged-args/index.js",
|
|
27
|
+
"#src/staged-args/*": "./src/staged-args/*.js",
|
|
28
|
+
"#src/config/connection": "./src/config/connection/index.js",
|
|
26
29
|
"#utils/*": "./src/utils/*.js"
|
|
27
30
|
},
|
|
28
31
|
"homepage": "https://github.com/wb-mirta/core/tree/latest/packages/mirta-cli#readme",
|
|
@@ -40,17 +43,26 @@
|
|
|
40
43
|
},
|
|
41
44
|
"dependencies": {
|
|
42
45
|
"@clack/prompts": "^0.11.0",
|
|
46
|
+
"p-map": "^7.0.4",
|
|
43
47
|
"chalk": "^5.6.2",
|
|
44
48
|
"prompts": "^2.4.2",
|
|
45
49
|
"semver": "^7.7.3",
|
|
46
|
-
"
|
|
50
|
+
"jsonc-parser": "^3.3.1",
|
|
51
|
+
"@mirta/basics": "0.4.0",
|
|
52
|
+
"@mirta/env-loader": "0.4.0",
|
|
53
|
+
"@mirta/package": "0.4.0",
|
|
54
|
+
"@mirta/staged-args": "0.4.0",
|
|
55
|
+
"@mirta/i18n": "0.4.0",
|
|
56
|
+
"@mirta/workspace": "0.4.0"
|
|
47
57
|
},
|
|
48
58
|
"devDependencies": {
|
|
49
|
-
"@types/semver": "^7.7.1"
|
|
50
|
-
|
|
59
|
+
"@types/semver": "^7.7.1"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=24.12.0"
|
|
51
63
|
},
|
|
52
64
|
"scripts": {
|
|
53
|
-
"
|
|
54
|
-
"
|
|
65
|
+
"build:mono": "rollup -c node:@mirta/rollup/config-package --config-skip-exports",
|
|
66
|
+
"i18n:shape-gen": "node ../../scripts/i18n/shape-gen.mjs"
|
|
55
67
|
}
|
|
56
68
|
}
|