@deploya-app/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # Deploya CLI
2
+
3
+ Deploy static HTML/CSS/JavaScript sites to Deploya from a terminal, CI pipeline, or AI coding agent.
4
+
5
+ ```bash
6
+ npx @deploya-app/cli login
7
+ npx @deploya-app/cli deploy --dir dist --prod
8
+ ```
9
+
10
+ The first production deploy creates a project and writes its non-secret ID to `.deploya/project.json`. Later deploys from the same directory update that project. Authentication tokens are stored in the user's configuration directory, never inside the project.
11
+
12
+ For CI:
13
+
14
+ ```bash
15
+ DEPLOYA_TOKEN=dpl_live_... npx @deploya-app/cli deploy --dir dist --prod
16
+ ```
17
+
18
+ Full documentation: <https://docs.deploya.ru/cli>
package/bin/deploya.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { run } = require('../src/cli');
5
+
6
+ run(process.argv.slice(2)).catch((error) => {
7
+ const message = error?.message || String(error);
8
+ process.stderr.write(`Ошибка: ${message}\n`);
9
+ if (error?.hint) process.stderr.write(`${error.hint}\n`);
10
+ process.exitCode = Number(error?.exitCode || 1);
11
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@deploya-app/cli",
3
+ "version": "0.1.0",
4
+ "description": "Deploy static sites to Deploya from a terminal or AI agent",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "deploya": "bin/deploya.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "keywords": [
18
+ "deploya",
19
+ "deploy",
20
+ "static-site",
21
+ "hosting",
22
+ "cli",
23
+ "ai"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Bukhalov1/deploya.git",
28
+ "directory": "cli"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "adm-zip": "^0.5.16"
35
+ }
36
+ }
package/src/api.js ADDED
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ class ApiError extends Error {
4
+ constructor(message, status, code, details) {
5
+ super(message);
6
+ this.name = 'ApiError';
7
+ this.status = status;
8
+ this.code = code;
9
+ this.details = details;
10
+ }
11
+ }
12
+
13
+ async function parseResponse(response) {
14
+ const contentType = response.headers.get('content-type') || '';
15
+ const body = contentType.includes('json') ? await response.json().catch(() => ({})) : { message: await response.text().catch(() => '') };
16
+ if (!response.ok) {
17
+ const apiError = body?.error;
18
+ const message = typeof apiError === 'object' ? apiError.message : apiError || body.message || `HTTP ${response.status}`;
19
+ throw new ApiError(message, response.status, typeof apiError === 'object' ? apiError.code : 'HTTP_ERROR', apiError?.details);
20
+ }
21
+ return body;
22
+ }
23
+
24
+ async function request(apiBase, pathname, options = {}) {
25
+ const headers = { Accept: 'application/json', ...(options.headers || {}) };
26
+ if (options.token) headers.Authorization = `Bearer ${options.token}`;
27
+ let body = options.body;
28
+ if (body && !(body instanceof FormData)) {
29
+ headers['Content-Type'] = 'application/json';
30
+ body = JSON.stringify(body);
31
+ }
32
+ let response;
33
+ try {
34
+ response = await fetch(`${apiBase}${pathname}`, {
35
+ method: options.method || 'GET',
36
+ headers,
37
+ body,
38
+ signal: options.signal || AbortSignal.timeout(options.timeout || 30_000),
39
+ });
40
+ } catch (error) {
41
+ if (error?.name === 'TimeoutError' || error?.name === 'AbortError') throw new ApiError('Сервер Deploya не ответил вовремя.', 0, 'TIMEOUT');
42
+ throw new ApiError(`Не удалось подключиться к Deploya: ${error.message}`, 0, 'NETWORK_ERROR');
43
+ }
44
+ return parseResponse(response);
45
+ }
46
+
47
+ module.exports = { ApiError, request };
package/src/archive.js ADDED
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const AdmZip = require('adm-zip');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const HARD_DIRECTORIES = new Set(['.git', '.deploya', 'node_modules']);
8
+
9
+ function loadIgnore(root) {
10
+ const rules = [];
11
+ for (const name of ['.gitignore', '.deployaignore']) {
12
+ const file = path.join(root, name);
13
+ if (!fs.existsSync(file)) continue;
14
+ for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
15
+ const value = line.trim();
16
+ if (!value || value.startsWith('#')) continue;
17
+ const negative = value.startsWith('!');
18
+ const pattern = (negative ? value.slice(1) : value).replace(/^\//, '');
19
+ if (pattern) rules.push({ negative, regex: globRegex(pattern) });
20
+ }
21
+ }
22
+ return {
23
+ ignores(relative, isDirectory) {
24
+ const normalized = relative.replace(/\\/g, '/').replace(/\/$/, '');
25
+ const segments = normalized.split('/');
26
+ const basename = segments.at(-1) || '';
27
+ if (segments.some((segment) => HARD_DIRECTORIES.has(segment))) return true;
28
+ if (basename === '.env' || (basename.startsWith('.env.') && basename !== '.env.example')) return true;
29
+ if (/\.(?:pem|key)$/i.test(basename)) return true;
30
+ let ignored = false;
31
+ for (const rule of rules) if (rule.regex.test(normalized) || (isDirectory && rule.regex.test(`${normalized}/`))) ignored = !rule.negative;
32
+ return ignored;
33
+ },
34
+ };
35
+ }
36
+
37
+ function globRegex(pattern) {
38
+ const directory = pattern.endsWith('/');
39
+ let source = pattern.replace(/\/$/, '').replace(/[.+^${}()|[\]\\]/g, '\\$&');
40
+ source = source.replace(/\*\*/g, '§§').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]').replace(/§§/g, '.*');
41
+ const prefix = pattern.includes('/') ? '^' : '(^|.*/)';
42
+ return new RegExp(`${prefix}${source}${directory ? '(/.*)?' : ''}$`);
43
+ }
44
+
45
+ function collectFiles(root) {
46
+ const matcher = loadIgnore(root);
47
+ const files = [];
48
+ let bytes = 0;
49
+ function walk(directory) {
50
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
51
+ const absolute = path.join(directory, entry.name);
52
+ const relative = path.relative(root, absolute).split(path.sep).join('/');
53
+ if (entry.isSymbolicLink()) continue;
54
+ if (matcher.ignores(relative, entry.isDirectory())) continue;
55
+ if (entry.isDirectory()) walk(absolute);
56
+ else if (entry.isFile()) {
57
+ const stat = fs.statSync(absolute);
58
+ files.push({ absolute, relative, bytes: stat.size });
59
+ bytes += stat.size;
60
+ }
61
+ }
62
+ }
63
+ walk(root);
64
+ if (!files.length) throw new Error(`В папке ${root} нет файлов для деплоя.`);
65
+ if (files.length > 2000) throw new Error(`В проекте ${files.length} файлов. Лимит Deploya — 2000.`);
66
+ return { files, bytes };
67
+ }
68
+
69
+ function createArchive(root) {
70
+ const absoluteRoot = path.resolve(root);
71
+ if (!fs.existsSync(absoluteRoot) || !fs.statSync(absoluteRoot).isDirectory()) {
72
+ throw new Error(`Папка для деплоя не найдена: ${absoluteRoot}`);
73
+ }
74
+ const inventory = collectFiles(absoluteRoot);
75
+ const zip = new AdmZip();
76
+ for (const file of inventory.files) {
77
+ const zipDirectory = path.posix.dirname(file.relative);
78
+ zip.addLocalFile(file.absolute, zipDirectory === '.' ? '' : zipDirectory, path.posix.basename(file.relative));
79
+ }
80
+ const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'deploya-'));
81
+ const archivePath = path.join(temporaryDirectory, 'site.zip');
82
+ zip.writeZip(archivePath);
83
+ return {
84
+ archivePath,
85
+ bytes: inventory.bytes,
86
+ fileCount: inventory.files.length,
87
+ cleanup() { fs.rmSync(temporaryDirectory, { recursive: true, force: true }); },
88
+ };
89
+ }
90
+
91
+ module.exports = { collectFiles, createArchive };
package/src/auth.js ADDED
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+ const { request } = require('./api');
5
+ const { saveSession } = require('./config');
6
+
7
+ function openBrowser(url) {
8
+ let command;
9
+ let args;
10
+ if (process.platform === 'win32') { command = 'rundll32.exe'; args = ['url.dll,FileProtocolHandler', url]; }
11
+ else if (process.platform === 'darwin') { command = 'open'; args = [url]; }
12
+ else { command = 'xdg-open'; args = [url]; }
13
+ try {
14
+ const child = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true });
15
+ child.unref();
16
+ return true;
17
+ } catch { return false; }
18
+ }
19
+
20
+ function delay(ms) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+
24
+ async function loginWithBrowser(apiBase, options = {}) {
25
+ const authorization = await request(apiBase, '/device/authorize', {
26
+ method: 'POST',
27
+ body: { clientName: options.clientName || `Deploya CLI on ${process.platform}` },
28
+ });
29
+ const output = options.output || process.stdout;
30
+ output.write(`Код подтверждения: ${authorization.userCode}\n`);
31
+ output.write(`Откройте: ${authorization.verificationUriComplete}\n`);
32
+ if (!options.noBrowser && openBrowser(authorization.verificationUriComplete)) output.write('Браузер открыт. Подтвердите доступ к аккаунту Deploya.\n');
33
+
34
+ const deadline = Date.now() + authorization.expiresIn * 1000;
35
+ while (Date.now() < deadline) {
36
+ await delay(Math.max(1, authorization.interval) * 1000);
37
+ try {
38
+ const result = await request(apiBase, '/device/token', { method: 'POST', body: { deviceCode: authorization.deviceCode } });
39
+ saveSession({ token: result.token, apiBase });
40
+ return result.user;
41
+ } catch (error) {
42
+ if (error.code === 'AUTHORIZATION_PENDING') continue;
43
+ throw error;
44
+ }
45
+ }
46
+ throw new Error('Время подтверждения истекло. Запустите deploya login ещё раз.');
47
+ }
48
+
49
+ module.exports = { loginWithBrowser, openBrowser };
package/src/cli.js ADDED
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { request } = require('./api');
5
+ const { loginWithBrowser } = require('./auth');
6
+ const { clearSession, loadProject, resolveCredentials, saveProject, saveSession } = require('./config');
7
+ const { deployDirectory } = require('./deploy');
8
+
9
+ const VERSION = '0.1.0';
10
+
11
+ function parseArguments(argv) {
12
+ const positionals = [];
13
+ const options = {};
14
+ for (let index = 0; index < argv.length; index += 1) {
15
+ const value = argv[index];
16
+ if (!value.startsWith('--')) { positionals.push(value); continue; }
17
+ const [rawName, inline] = value.slice(2).split(/=(.*)/s, 2);
18
+ const name = rawName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
19
+ if (inline !== undefined) options[name] = inline;
20
+ else if (argv[index + 1] && !argv[index + 1].startsWith('-')) options[name] = argv[++index];
21
+ else options[name] = true;
22
+ }
23
+ return { command: positionals[0] || 'help', positionals: positionals.slice(1), options };
24
+ }
25
+
26
+ function requireToken(credentials) {
27
+ if (credentials.token) return credentials.token;
28
+ const error = new Error('Вы не вошли в Deploya. Выполните: npx @deploya-app/cli login');
29
+ error.exitCode = 2;
30
+ throw error;
31
+ }
32
+
33
+ function printJsonOr(value, json, human) {
34
+ if (json) process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
35
+ else process.stdout.write(`${human}\n`);
36
+ }
37
+
38
+ async function run(argv) {
39
+ const { command, options } = parseArguments(argv);
40
+ if (options.version || command === 'version') return process.stdout.write(`${VERSION}\n`);
41
+ if (options.help || command === 'help') return process.stdout.write(helpText());
42
+ const credentials = resolveCredentials(options);
43
+
44
+ if (command === 'login') {
45
+ if (options.token && options.token !== true) {
46
+ const me = await request(credentials.apiBase, '/me', { token: options.token });
47
+ saveSession({ token: options.token, apiBase: credentials.apiBase });
48
+ return printJsonOr(me, options.json, `Вход выполнен: ${me.user.email}`);
49
+ }
50
+ const user = await loginWithBrowser(credentials.apiBase, { noBrowser: Boolean(options.noBrowser), output: options.json ? process.stderr : process.stdout });
51
+ return printJsonOr({ user }, options.json, `Готово. CLI подключён к ${user.email}`);
52
+ }
53
+
54
+ if (command === 'logout') {
55
+ clearSession();
56
+ return process.stdout.write('Локальная сессия Deploya удалена.\n');
57
+ }
58
+
59
+ const token = requireToken(credentials);
60
+
61
+ if (command === 'whoami') {
62
+ const result = await request(credentials.apiBase, '/me', { token });
63
+ return printJsonOr(result, options.json, `${result.user.email} · тариф ${result.user.plan}`);
64
+ }
65
+
66
+ if (command === 'projects') {
67
+ const result = await request(credentials.apiBase, '/projects', { token });
68
+ if (options.json) return printJsonOr(result, true, '');
69
+ if (!result.projects.length) return process.stdout.write('Проектов пока нет. Первый создаст deploya deploy --prod.\n');
70
+ process.stdout.write(`${'ID'.padEnd(8)} ${'Название'.padEnd(28)} Адрес\n`);
71
+ for (const project of result.projects) process.stdout.write(`${String(project.id).padEnd(8)} ${String(project.name).slice(0, 27).padEnd(28)} ${project.url}\n`);
72
+ return;
73
+ }
74
+
75
+ if (command === 'link') {
76
+ const projectId = Number(options.project || 0);
77
+ if (!projectId) throw new Error('Укажите проект: deploya link --project ID');
78
+ const result = await request(credentials.apiBase, `/projects/${projectId}`, { token });
79
+ saveProject(process.cwd(), { projectId: result.project.id, teamId: result.project.teamId, name: result.project.name, url: result.project.url });
80
+ return printJsonOr(result, options.json, `Папка связана с «${result.project.name}» (${result.project.url}).`);
81
+ }
82
+
83
+ if (command === 'deploy') {
84
+ const projectRoot = process.cwd();
85
+ const linked = loadProject(projectRoot);
86
+ const projectId = Number(options.project || linked.projectId || 0) || null;
87
+ const target = options.prod ? 'production' : 'preview';
88
+ if (!projectId && target === 'preview') {
89
+ const error = new Error('Это первый деплой проекта. Добавьте --prod, чтобы создать его и получить постоянный адрес.');
90
+ error.hint = 'Пример: npx @deploya-app/cli deploy --dir dist --prod';
91
+ throw error;
92
+ }
93
+ const directory = path.resolve(projectRoot, options.dir && options.dir !== true ? options.dir : '.');
94
+ const name = options.name && options.name !== true ? options.name : path.basename(projectRoot);
95
+ const result = await deployDirectory({
96
+ apiBase: credentials.apiBase,
97
+ token,
98
+ directory,
99
+ projectId,
100
+ name,
101
+ subdomain: options.subdomain === true ? '' : options.subdomain,
102
+ teamId: options.team === true ? '' : options.team,
103
+ entrypoint: options.entrypoint === true ? '' : options.entrypoint,
104
+ target,
105
+ onProgress: options.json ? (message) => process.stderr.write(`${message}\n`) : (message) => process.stdout.write(`${message}\n`),
106
+ });
107
+ saveProject(projectRoot, {
108
+ projectId: result.project.id,
109
+ teamId: result.project.teamId,
110
+ name: result.project.name,
111
+ url: result.project.url,
112
+ });
113
+ if (options.json) return printJsonOr(result, true, '');
114
+ process.stdout.write(`Готово: ${result.deployment.url}\n`);
115
+ if (result.deployment.url !== result.deployment.productionUrl) process.stdout.write(`Production: ${result.deployment.productionUrl}\n`);
116
+ return;
117
+ }
118
+
119
+ const error = new Error(`Неизвестная команда: ${command}`);
120
+ error.hint = 'Выполните deploya --help, чтобы увидеть список команд.';
121
+ throw error;
122
+ }
123
+
124
+ function helpText() {
125
+ return `Deploya CLI ${VERSION}
126
+
127
+ Публикация статических сайтов из терминала и AI-агентов.
128
+
129
+ Быстрый старт:
130
+ npx @deploya-app/cli login
131
+ npx @deploya-app/cli deploy --dir dist --prod
132
+
133
+ Команды:
134
+ login [--no-browser] Войти через подтверждение в браузере
135
+ logout Удалить локальную сессию
136
+ whoami Показать текущий аккаунт
137
+ projects Показать проекты
138
+ link --project ID Связать текущую папку с проектом
139
+ deploy [--dir PATH] Создать preview связанного проекта
140
+ deploy --dir PATH --prod Опубликовать production-версию
141
+
142
+ Параметры deploy:
143
+ --name TEXT Название нового проекта
144
+ --subdomain NAME Желаемый поддомен
145
+ --entrypoint FILE Главный HTML-файл
146
+ --project ID Проект без локальной привязки
147
+ --team ID Команда для нового проекта
148
+ --json Машиночитаемый JSON-результат
149
+
150
+ Общие параметры:
151
+ --api URL Другой сервер Deploya
152
+ --token TOKEN API-токен (предпочтительно DEPLOYA_TOKEN)
153
+ --help, --version
154
+ `;
155
+ }
156
+
157
+ module.exports = { parseArguments, run };
package/src/config.js ADDED
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const DEFAULT_APP_URL = 'https://app.deploya.ru';
8
+
9
+ function configDirectory() {
10
+ if (process.env.DEPLOYA_CONFIG_DIR) return path.resolve(process.env.DEPLOYA_CONFIG_DIR);
11
+ if (process.platform === 'win32' && process.env.APPDATA) return path.join(process.env.APPDATA, 'Deploya');
12
+ return path.join(os.homedir(), '.config', 'deploya');
13
+ }
14
+
15
+ function userConfigPath() {
16
+ return path.join(configDirectory(), 'config.json');
17
+ }
18
+
19
+ function readJson(file) {
20
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
21
+ catch { return {}; }
22
+ }
23
+
24
+ function writeJsonAtomic(file, value, secret = false) {
25
+ fs.mkdirSync(path.dirname(file), { recursive: true });
26
+ const temporary = `${file}.${process.pid}.tmp`;
27
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: secret ? 0o600 : 0o644 });
28
+ fs.renameSync(temporary, file);
29
+ if (secret && process.platform !== 'win32') fs.chmodSync(file, 0o600);
30
+ }
31
+
32
+ function normalizeApiBase(value) {
33
+ const source = String(value || DEFAULT_APP_URL).trim().replace(/\/+$/, '');
34
+ return source.endsWith('/api/v1') ? source : `${source}/api/v1`;
35
+ }
36
+
37
+ function loadUserConfig() {
38
+ return readJson(userConfigPath());
39
+ }
40
+
41
+ function saveSession({ token, apiBase }) {
42
+ const current = loadUserConfig();
43
+ writeJsonAtomic(userConfigPath(), { ...current, token, apiBase: normalizeApiBase(apiBase), updatedAt: new Date().toISOString() }, true);
44
+ }
45
+
46
+ function clearSession() {
47
+ const current = loadUserConfig();
48
+ delete current.token;
49
+ writeJsonAtomic(userConfigPath(), current, true);
50
+ }
51
+
52
+ function resolveCredentials(options = {}) {
53
+ const stored = loadUserConfig();
54
+ const token = options.token || process.env.DEPLOYA_TOKEN || stored.token || '';
55
+ const apiBase = normalizeApiBase(options.api || process.env.DEPLOYA_API_URL || stored.apiBase || DEFAULT_APP_URL);
56
+ return { token, apiBase };
57
+ }
58
+
59
+ function projectConfigPath(root = process.cwd()) {
60
+ return path.join(path.resolve(root), '.deploya', 'project.json');
61
+ }
62
+
63
+ function loadProject(root = process.cwd()) {
64
+ return readJson(projectConfigPath(root));
65
+ }
66
+
67
+ function saveProject(root, project) {
68
+ writeJsonAtomic(projectConfigPath(root), {
69
+ projectId: Number(project.projectId),
70
+ teamId: project.teamId ? Number(project.teamId) : null,
71
+ name: project.name || '',
72
+ url: project.url || '',
73
+ });
74
+ }
75
+
76
+ module.exports = { clearSession, loadProject, loadUserConfig, normalizeApiBase, resolveCredentials, saveProject, saveSession, userConfigPath };
package/src/deploy.js ADDED
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const { createArchive } = require('./archive');
7
+ const { request } = require('./api');
8
+
9
+ async function archiveBlob(archivePath) {
10
+ if (typeof fs.openAsBlob === 'function') return fs.openAsBlob(archivePath, { type: 'application/zip' });
11
+ return new Blob([fs.readFileSync(archivePath)], { type: 'application/zip' });
12
+ }
13
+
14
+ async function deployDirectory({ apiBase, token, directory, projectId, name, subdomain, teamId, entrypoint, target, onProgress }) {
15
+ onProgress?.('Собираем безопасный ZIP-архив…');
16
+ const archive = createArchive(directory);
17
+ try {
18
+ onProgress?.(`Загружаем ${archive.fileCount} файлов (${formatBytes(archive.bytes)})…`);
19
+ const form = new FormData();
20
+ form.append('archive', await archiveBlob(archive.archivePath), 'site.zip');
21
+ if (projectId) form.append('projectId', String(projectId));
22
+ if (name) form.append('name', name);
23
+ if (subdomain) form.append('subdomain', subdomain);
24
+ if (teamId) form.append('teamId', String(teamId));
25
+ if (entrypoint) form.append('entrypoint', entrypoint);
26
+ form.append('target', target);
27
+ return await request(apiBase, '/deployments', {
28
+ method: 'POST',
29
+ token,
30
+ body: form,
31
+ timeout: 10 * 60 * 1000,
32
+ headers: { 'Idempotency-Key': crypto.randomUUID(), 'User-Agent': 'deploya-cli/0.1.0' },
33
+ });
34
+ } finally {
35
+ archive.cleanup();
36
+ }
37
+ }
38
+
39
+ function formatBytes(value) {
40
+ if (value < 1024) return `${value} Б`;
41
+ if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} КБ`;
42
+ return `${(value / 1024 ** 2).toFixed(1)} МБ`;
43
+ }
44
+
45
+ module.exports = { deployDirectory, formatBytes };