@getxflow/cli 0.0.1 → 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.
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.saveCredential = saveCredential;
4
+ exports.forgetCredential = forgetCredential;
5
+ exports.resolveToken = resolveToken;
6
+ exports.readCredential = readCredential;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = require("node:os");
9
+ const node_path_1 = require("node:path");
10
+ function credentialsDir() {
11
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), '.xflow');
12
+ }
13
+ function credentialsPath() {
14
+ return (0, node_path_1.join)(credentialsDir(), 'credentials.json');
15
+ }
16
+ function readStore() {
17
+ const path = credentialsPath();
18
+ if (!(0, node_fs_1.existsSync)(path))
19
+ return {};
20
+ try {
21
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
22
+ return parsed && typeof parsed === 'object' ? parsed : {};
23
+ }
24
+ catch {
25
+ return {};
26
+ }
27
+ }
28
+ function writeStore(store) {
29
+ const dir = credentialsDir();
30
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
31
+ const path = credentialsPath();
32
+ (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
33
+ // На случай, когда файл уже существовал: writeFileSync права не меняет.
34
+ try {
35
+ (0, node_fs_1.chmodSync)(path, 0o600);
36
+ }
37
+ catch {
38
+ // Windows правами POSIX не управляет — не повод падать.
39
+ }
40
+ }
41
+ function saveCredential(apiUrl, credential) {
42
+ const store = readStore();
43
+ store[apiUrl] = { ...credential, savedAt: new Date().toISOString() };
44
+ writeStore(store);
45
+ }
46
+ function forgetCredential(apiUrl) {
47
+ const store = readStore();
48
+ if (!store[apiUrl])
49
+ return false;
50
+ delete store[apiUrl];
51
+ writeStore(store);
52
+ return true;
53
+ }
54
+ /**
55
+ * Ключ для адреса платформы. Переменная окружения главнее файла: так работает
56
+ * запуск из CI, где домашнего каталога может не быть вовсе.
57
+ */
58
+ function resolveToken(apiUrl) {
59
+ const fromEnv = process.env.XFLOW_TOKEN?.trim();
60
+ if (fromEnv)
61
+ return fromEnv;
62
+ return readStore()[apiUrl]?.token ?? null;
63
+ }
64
+ function readCredential(apiUrl) {
65
+ return readStore()[apiUrl] ?? null;
66
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CliError = void 0;
4
+ /** Ошибка, которую показываем человеку как есть: без стека и без «Unhandled». */
5
+ class CliError extends Error {
6
+ hint;
7
+ constructor(message, hint) {
8
+ super(message);
9
+ this.name = 'CliError';
10
+ this.hint = hint;
11
+ }
12
+ }
13
+ exports.CliError = CliError;
package/dist/help.js ADDED
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.help = help;
4
+ const ui_1 = require("./ui");
5
+ const version_1 = require("./version");
6
+ /**
7
+ * Справка. Заменяет агенту машинную документацию: у CLI её роль играет `--help`,
8
+ * поэтому текст пишем так, чтобы по нему можно было работать, не открывая сайт.
9
+ */
10
+ function help(topic) {
11
+ if (topic && TOPICS[topic]) {
12
+ (0, ui_1.out)(TOPICS[topic]);
13
+ return;
14
+ }
15
+ (0, ui_1.out)(`${(0, ui_1.bold)('xflow')} ${(0, ui_1.dim)(version_1.CLI_VERSION)} — хостинг приложений XFlow
16
+
17
+ ${(0, ui_1.bold)('Начало работы')}
18
+ xflow login вход через браузер
19
+ xflow init [папка] новый проект из шаблона платформы
20
+ xflow templates какие шаблоны доступны
21
+ xflow link <id> связать текущую папку с существующим проектом
22
+ xflow skills [--global] положить инструкцию о платформе для ИИ-агента
23
+
24
+ ${(0, ui_1.bold)('Код')}
25
+ xflow status что на сервере и чем отличается локальная копия
26
+ xflow push [--force] отправить исходники новой ревизией
27
+ xflow pull [--into dir] [--revision N]
28
+ забрать исходники (по умолчанию последнюю ревизию)
29
+
30
+ ${(0, ui_1.bold)('Выкладка')}
31
+ xflow deploy [--no-push] [--skip-build]
32
+ отправить код, собрать локально и выложить на dev
33
+ xflow publish показать dev-версию посетителям
34
+ xflow rollback <номер версии> вернуть dev-адрес на прошлую версию
35
+ xflow deployments история версий
36
+
37
+ ${(0, ui_1.bold)('Функции')}
38
+ xflow functions list что выложено
39
+ xflow functions deploy [имя] собрать и выкатить функцию
40
+
41
+ ${(0, ui_1.bold)('Справочное')}
42
+ xflow projects list проекты организации
43
+ xflow projects get [id] карточка проекта
44
+ xflow open [--live] открыть приложение в браузере
45
+ xflow whoami чей ключ и что он может
46
+ xflow logout забыть ключ
47
+
48
+ ${(0, ui_1.bold)('Окружение')}
49
+ XFLOW_TOKEN ключ доступа (для CI: вместо xflow login)
50
+ XFLOW_API_URL адрес платформы, если он не app.getxflow.com
51
+
52
+ Подробнее о команде: xflow help <команда>`);
53
+ }
54
+ const TOPICS = {
55
+ skills: `${(0, ui_1.bold)('xflow skills')} — инструкция о платформе для ИИ-агента
56
+
57
+ Кладёт файл в формате Agent Skills (agentskills.io): что такое XFlow, как выкладывать
58
+ и публиковать, откуда брать компоненты дизайн-системы, где смотреть ошибки прода.
59
+ Агент подхватывает его сам, когда задача про платформу, и не тратит контекст в
60
+ остальное время.
61
+
62
+ Файл общий для инструментов, а папки у них разные, поэтому кладём в обе:
63
+ ${(0, ui_1.bold)('.claude/skills/xflow/')} читает Claude Code, ${(0, ui_1.bold)('.agents/skills/xflow/')} — Codex и OpenClaw.
64
+
65
+ --global положить в домашнюю папку, тогда скилл виден во всех проектах
66
+
67
+ ${(0, ui_1.bold)('AGENTS.md')} и ${(0, ui_1.bold)('CLAUDE.md')} команда не трогает: это ваши файлы, в них ваши правила.
68
+ Наше знание живёт в своей папке и переустанавливается поверх без спора за файл.
69
+
70
+ Скилл едет вместе с CLI, поэтому после обновления пакета команду стоит повторить.`,
71
+ push: `${(0, ui_1.bold)('xflow push')} — отправить исходники
72
+
73
+ Уходит вся рабочая копия целиком, одной ревизией. Не отправляются: node_modules,
74
+ .git, dist, build, .next, любые .env, а также всё, что перечислено в .xflowignore
75
+ и в поле ignore файла xflow.json.
76
+
77
+ Если на сервере ревизия новее той, от которой вы работали, push отклоняется.
78
+ Это значит, что кто-то запушил раньше: заберите его правки рядом
79
+ (${(0, ui_1.bold)('xflow pull --into ./server-copy')}), сведите их у себя в git и повторите.
80
+
81
+ --force перетирает серверную ревизию. Перед этим CLI показывает, чью работу вы
82
+ затираете, и просит подтверждение вводом имени проекта. В неинтерактивном режиме
83
+ (CI, запуск агентом) подтвердить нельзя: расхождение версий должно ронять сборку,
84
+ а не молча уничтожать чужую работу.`,
85
+ pull: `${(0, ui_1.bold)('xflow pull')} — забрать исходники
86
+
87
+ По умолчанию забирает последнюю ревизию в папку проекта и отказывается писать в
88
+ непустую: слить изменения платформа не умеет, это работа git.
89
+
90
+ --into <папка> выгрузить рядом, чтобы сравнить
91
+ --revision <N> конкретная ревизия (список: xflow deployments)
92
+ --force перезаписать папку целиком`,
93
+ deploy: `${(0, ui_1.bold)('xflow deploy')} — собрать и выложить
94
+
95
+ Три шага: отправка исходников, локальная сборка, заливка результата. Команда
96
+ сборки и каталог результата берутся из xflow.json (по умолчанию npm run build
97
+ и dist).
98
+
99
+ --no-push не отправлять исходники, выложить из последней серверной ревизии
100
+ --skip-build не собирать, взять готовый каталог сборки
101
+ --force разрешить перезапись серверной ревизии при отправке
102
+
103
+ Сборка идёт на вашей машине, поэтому её результат зависит от вашей версии Node.
104
+ Платформа раздаёт статику: сборка без index.html в корне отклоняется.
105
+
106
+ Выложенное видно только по dev-адресу. Посетителям — после ${(0, ui_1.bold)('xflow publish')}.`,
107
+ init: `${(0, ui_1.bold)('xflow init')} [папка] — новый проект
108
+
109
+ Разворачивает шаблон платформы: React на Vite, Tailwind, набор компонентов
110
+ (${(0, ui_1.bold)('src/components/ui')}) и готовые блоки (${(0, ui_1.bold)('src/components/blocks')}) — таблица,
111
+ форма, фильтры, канбан, графики. Тот же шаблон получают проекты, созданные в вебе,
112
+ поэтому приложения выглядят одинаково.
113
+
114
+ Строить интерфейс поверх этих компонентов и токенов из ${(0, ui_1.bold)('src/index.css')} — не
115
+ формальность: своя палитра поверх них выглядит чужеродно внутри платформы.
116
+
117
+ --name <имя> имя проекта (по умолчанию — имя папки)
118
+ --template <id> другой шаблон (список: xflow templates)
119
+ --database <id> привязать логическую базу организации
120
+
121
+ Порядок: проект заводится на платформе первым, потому что без него неоткуда взять
122
+ токен для .env. Если запись файлов сорвётся, проект останется пустым, и CLI скажет,
123
+ как подобрать его командой link.`,
124
+ rollback: `${(0, ui_1.bold)('xflow rollback')} <номер версии> — вернуть прошлую сборку
125
+
126
+ Переключает dev-адрес на выбранную версию. Возвращается только сборка: исходники
127
+ остаются на своей ревизии, и вернуть их — отдельное решение
128
+ (${(0, ui_1.bold)('xflow pull --revision N --into ./старая-версия')}).
129
+
130
+ Посетители продолжают видеть опубликованную версию, пока не выполнен
131
+ ${(0, ui_1.bold)('xflow publish')}.`,
132
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openUrl = openUrl;
4
+ const node_child_process_1 = require("node:child_process");
5
+ /**
6
+ * Открыть адрес в браузере пользователя.
7
+ *
8
+ * Best-effort: на сервере без графики открывать нечего, и это не ошибка —
9
+ * вызывающий в любом случае печатает ссылку рядом.
10
+ */
11
+ function openUrl(url) {
12
+ try {
13
+ const command = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
14
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
15
+ const child = (0, node_child_process_1.spawn)(command, args, { stdio: 'ignore', detached: true });
16
+ child.on('error', () => {
17
+ // Браузера нет — молча, ссылка уже показана.
18
+ });
19
+ child.unref();
20
+ }
21
+ catch {
22
+ // То же самое: открыть не удалось, но команда не про это.
23
+ }
24
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.connect = connect;
4
+ exports.anonymous = anonymous;
5
+ const config_1 = require("./config");
6
+ const credentials_1 = require("./credentials");
7
+ const errors_1 = require("./errors");
8
+ /**
9
+ * Соединение с платформой: адрес плюс ключ.
10
+ *
11
+ * Адрес берётся из XFLOW_API_URL, потом из xflow.json, потом умолчание —
12
+ * порядок такой, чтобы разработчик мог указать локальную платформу одной
13
+ * переменной, не правя конфигурацию проекта.
14
+ */
15
+ function connect(config) {
16
+ const apiUrl = (0, config_1.apiUrlFor)(config);
17
+ const token = (0, credentials_1.resolveToken)(apiUrl);
18
+ if (!token) {
19
+ throw new errors_1.CliError(`Нет ключа доступа для ${apiUrl}`, 'Войдите: xflow login. В CI передайте ключ переменной XFLOW_TOKEN');
20
+ }
21
+ return { apiUrl, token };
22
+ }
23
+ /** Соединение без ключа: только для входа. */
24
+ function anonymous(config) {
25
+ return { apiUrl: (0, config_1.apiUrlFor)(config), token: '' };
26
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Файлы, которые CLI добавляет поверх шаблона платформы.
4
+ *
5
+ * Сам шаблон приезжает архивом с платформы (`GET /api/v1/templates/{id}/archive`)
6
+ * и здесь не дублируется: своя копия неизбежно разъехалась бы с той, из которой
7
+ * создаются проекты в вебе, и приложения перестали бы выглядеть одинаково.
8
+ *
9
+ * Здесь только то, чего в шаблоне нет и быть не может: `.env` с токеном
10
+ * конкретного проекта и правила для git.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.envFile = envFile;
14
+ exports.scaffoldFiles = scaffoldFiles;
15
+ const GITIGNORE = `node_modules/
16
+ dist/
17
+ .xflow/
18
+ .env
19
+ .env.*
20
+ `;
21
+ const README = `# %NAME%
22
+
23
+ Приложение на XFlow.
24
+
25
+ \`\`\`bash
26
+ npm install # зависимости
27
+ npm run dev # разработка на localhost:3000
28
+ xflow deploy # отправить код, собрать и выложить на dev-адрес
29
+ xflow publish # показать dev-версию посетителям
30
+ \`\`\`
31
+
32
+ Интерфейс собран на компонентах шаблона: \`src/components/ui\` (кнопки, поля, диалоги)
33
+ и \`src/components/blocks\` (таблица, форма, фильтры, канбан, графики). Своя вёрстка
34
+ поверх них выглядит как остальная платформа, а собственные цвета вместо токенов из
35
+ \`src/index.css\` эту связность ломают.
36
+
37
+ Полезное: \`xflow status\` — что на сервере, \`xflow deployments\` — история версий.
38
+ `;
39
+ /**
40
+ * `.env` в исходники не уходит (его пропускают и push, и сервер), поэтому в свежем
41
+ * клоне его восстанавливает `xflow link`.
42
+ */
43
+ function envFile(projectToken, apiUrl) {
44
+ return `VITE_XFLOW_PROJECT_TOKEN=${projectToken}\nVITE_XFLOW_API_URL=${apiUrl.replace(/^https?:\/\//, '')}\n`;
45
+ }
46
+ function scaffoldFiles(projectName) {
47
+ return [
48
+ { path: '.gitignore', content: GITIGNORE },
49
+ { path: 'README.md', content: README.replace('%NAME%', projectName.replace(/"/g, '\\"')) },
50
+ ];
51
+ }
package/dist/tree.js ADDED
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadIgnoreRules = loadIgnoreRules;
4
+ exports.collectFiles = collectFiles;
5
+ exports.treeHash = treeHash;
6
+ exports.heaviest = heaviest;
7
+ exports.collectArtifact = collectArtifact;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const node_fs_1 = require("node:fs");
10
+ const node_path_1 = require("node:path");
11
+ /**
12
+ * Не отправляется никогда. Первые пять сервер отклоняет и сам, но узнать об
13
+ * этом лучше до заливки 200 МБ node_modules. Секреты (`.env*`) шире серверного
14
+ * списка сознательно: значения переменных живут в настройках организации,
15
+ * а не в репозитории.
16
+ */
17
+ const DEFAULT_IGNORE = [
18
+ 'node_modules/',
19
+ '.git/',
20
+ 'dist/',
21
+ 'build/',
22
+ '.next/',
23
+ '.xflow/',
24
+ '.env',
25
+ '.env.*',
26
+ '.DS_Store',
27
+ 'Thumbs.db',
28
+ ];
29
+ const IGNORE_FILE = '.xflowignore';
30
+ function globToRegExpSource(pattern) {
31
+ let source = '';
32
+ for (let i = 0; i < pattern.length; i++) {
33
+ const char = pattern[i];
34
+ if (char === '*') {
35
+ if (pattern[i + 1] === '*') {
36
+ source += '.*';
37
+ i++;
38
+ }
39
+ else {
40
+ source += '[^/]*';
41
+ }
42
+ }
43
+ else if (char === '?') {
44
+ source += '[^/]';
45
+ }
46
+ else {
47
+ source += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
48
+ }
49
+ }
50
+ return source;
51
+ }
52
+ function compileRule(raw) {
53
+ const pattern = raw.trim();
54
+ if (!pattern || pattern.startsWith('#'))
55
+ return null;
56
+ const dirOnly = pattern.endsWith('/');
57
+ const body = dirOnly ? pattern.slice(0, -1) : pattern;
58
+ const anchored = body.includes('/');
59
+ const source = globToRegExpSource(anchored ? body.replace(/^\//, '') : body);
60
+ const prefix = anchored ? '^' : '(^|/)';
61
+ const suffix = dirOnly ? '/' : '($|/)';
62
+ return { re: new RegExp(`${prefix}${source}${suffix}`) };
63
+ }
64
+ /** Правила исключения: умолчания, файл .xflowignore и поле ignore в xflow.json. */
65
+ function loadIgnoreRules(root, extra = []) {
66
+ const lines = [...DEFAULT_IGNORE, ...extra];
67
+ const ignoreFile = (0, node_path_1.join)(root, IGNORE_FILE);
68
+ if ((0, node_fs_1.existsSync)(ignoreFile)) {
69
+ lines.push(...(0, node_fs_1.readFileSync)(ignoreFile, 'utf-8').split(/\r?\n/));
70
+ }
71
+ return lines.map(compileRule).filter((rule) => rule !== null);
72
+ }
73
+ function isIgnored(relativePath, rules, isDirectory) {
74
+ const probe = isDirectory ? `${relativePath}/` : relativePath;
75
+ return rules.some((rule) => rule.re.test(probe));
76
+ }
77
+ /** Двоичный файл: ищем нулевой байт в начале, как это делает git. */
78
+ function isBinary(content) {
79
+ const limit = Math.min(content.length, 8000);
80
+ for (let i = 0; i < limit; i++) {
81
+ if (content[i] === 0)
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ /**
87
+ * Приводим переводы строк к LF.
88
+ *
89
+ * Иначе одно и то же дерево на Windows и на Linux даёт разные хеши, и статус
90
+ * вечно показывает изменения, которых никто не делал. Двоичные файлы не трогаем.
91
+ */
92
+ function normalizeEol(content) {
93
+ if (isBinary(content) || !content.includes(0x0d))
94
+ return content;
95
+ return Buffer.from(content.toString('utf-8').replace(/\r\n/g, '\n'), 'utf-8');
96
+ }
97
+ function collectFiles(root, rules) {
98
+ const files = [];
99
+ const skippedLinks = [];
100
+ const walk = (dir) => {
101
+ for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
102
+ const full = (0, node_path_1.join)(dir, entry.name);
103
+ const rel = (0, node_path_1.relative)(root, full).split(node_path_1.sep).join('/');
104
+ // Ссылки не отправляем: в архиве их не представить, а пойти по ним значит
105
+ // утащить чужое дерево целиком.
106
+ if (entry.isSymbolicLink()) {
107
+ skippedLinks.push(rel);
108
+ continue;
109
+ }
110
+ if (entry.isDirectory()) {
111
+ if (isIgnored(rel, rules, true))
112
+ continue;
113
+ walk(full);
114
+ continue;
115
+ }
116
+ if (!entry.isFile())
117
+ continue;
118
+ if (isIgnored(rel, rules, false))
119
+ continue;
120
+ files.push({ path: rel, content: normalizeEol((0, node_fs_1.readFileSync)(full)) });
121
+ }
122
+ };
123
+ walk(root);
124
+ files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
125
+ return { files, skippedLinks };
126
+ }
127
+ /** Хеш дерева. Считается ровно так же на сервере — иначе push «менялось/не менялось» врал бы. */
128
+ function treeHash(files) {
129
+ const parts = files.map((file) => `${file.path}\0${(0, node_crypto_1.createHash)('sha256').update(file.content).digest('hex')}`);
130
+ parts.sort();
131
+ return (0, node_crypto_1.createHash)('sha256').update(parts.join('\n')).digest('hex');
132
+ }
133
+ /** Самые тяжёлые файлы: показываем их, когда архив не влез в лимит. */
134
+ function heaviest(files, count = 5) {
135
+ return [...files].sort((a, b) => b.content.length - a.content.length).slice(0, count);
136
+ }
137
+ /** Собрать дерево из каталога сборки: пути внутри артефакта считаются от него. */
138
+ function collectArtifact(dir) {
139
+ const files = [];
140
+ const walk = (current) => {
141
+ for (const entry of (0, node_fs_1.readdirSync)(current, { withFileTypes: true })) {
142
+ const full = (0, node_path_1.join)(current, entry.name);
143
+ if (entry.isSymbolicLink())
144
+ continue;
145
+ if (entry.isDirectory()) {
146
+ walk(full);
147
+ continue;
148
+ }
149
+ if (!entry.isFile())
150
+ continue;
151
+ files.push({
152
+ path: (0, node_path_1.relative)(dir, full).split(node_path_1.sep).join('/'),
153
+ content: (0, node_fs_1.readFileSync)(full),
154
+ });
155
+ }
156
+ };
157
+ if (!(0, node_fs_1.existsSync)(dir) || !(0, node_fs_1.statSync)(dir).isDirectory())
158
+ return files;
159
+ walk(dir);
160
+ return files;
161
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ /** Ответы публичного API, которые разбирает CLI. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/ui.js ADDED
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dim = exports.bold = void 0;
4
+ exports.out = out;
5
+ exports.note = note;
6
+ exports.step = step;
7
+ exports.ok = ok;
8
+ exports.warn = warn;
9
+ exports.fail = fail;
10
+ exports.table = table;
11
+ exports.formatBytes = formatBytes;
12
+ exports.formatAge = formatAge;
13
+ exports.confirmWord = confirmWord;
14
+ const node_readline_1 = require("node:readline");
15
+ /**
16
+ * Вывод CLI.
17
+ *
18
+ * Всё пишется через один модуль, а не через console: человек читает stdout,
19
+ * машина парсит его же, а служебные сообщения (шаги, предупреждения) обязаны
20
+ * уходить в stderr, иначе `xflow pull > file` сложит их в файл вместе с данными.
21
+ */
22
+ const color = process.stdout.isTTY === true && !process.env.NO_COLOR;
23
+ function paint(code, text) {
24
+ return color ? `\u001b[${code}m${text}\u001b[0m` : text;
25
+ }
26
+ const bold = (t) => paint('1', t);
27
+ exports.bold = bold;
28
+ const dim = (t) => paint('2', t);
29
+ exports.dim = dim;
30
+ const red = (t) => paint('31', t);
31
+ const green = (t) => paint('32', t);
32
+ const yellow = (t) => paint('33', t);
33
+ /** Данные: то, ради чего команду запускали. */
34
+ function out(line = '') {
35
+ process.stdout.write(`${line}\n`);
36
+ }
37
+ /** Ход работы: шаги, предупреждения, ошибки. */
38
+ function note(line = '') {
39
+ process.stderr.write(`${line}\n`);
40
+ }
41
+ function step(message) {
42
+ note((0, exports.dim)(`→ ${message}`));
43
+ }
44
+ function ok(message) {
45
+ note(green(`✓ ${message}`));
46
+ }
47
+ function warn(message) {
48
+ note(yellow(`! ${message}`));
49
+ }
50
+ function fail(message) {
51
+ note(red(`✗ ${message}`));
52
+ }
53
+ /** Таблица с выравниванием по колонкам. Заголовок не рисуем: он не нужен на 3 строки. */
54
+ function table(rows) {
55
+ if (rows.length === 0)
56
+ return;
57
+ const widths = [];
58
+ for (const row of rows) {
59
+ row.forEach((cell, i) => {
60
+ widths[i] = Math.max(widths[i] ?? 0, cell.length);
61
+ });
62
+ }
63
+ for (const row of rows) {
64
+ out(row.map((cell, i) => (i === row.length - 1 ? cell : cell.padEnd(widths[i]))).join(' '));
65
+ }
66
+ }
67
+ function formatBytes(bytes) {
68
+ if (bytes < 1024)
69
+ return `${bytes} Б`;
70
+ if (bytes < 1024 * 1024)
71
+ return `${(bytes / 1024).toFixed(1)} КБ`;
72
+ return `${(bytes / 1024 / 1024).toFixed(1)} МБ`;
73
+ }
74
+ function formatAge(iso) {
75
+ if (!iso)
76
+ return '—';
77
+ const ms = Date.now() - new Date(iso).getTime();
78
+ if (!Number.isFinite(ms) || ms < 0)
79
+ return '—';
80
+ const minutes = Math.floor(ms / 60000);
81
+ if (minutes < 1)
82
+ return 'только что';
83
+ if (minutes < 60)
84
+ return `${minutes} мин назад`;
85
+ const hours = Math.floor(minutes / 60);
86
+ if (hours < 24)
87
+ return `${hours} ч назад`;
88
+ return `${Math.floor(hours / 24)} дн назад`;
89
+ }
90
+ /**
91
+ * Подтверждение опасного действия.
92
+ *
93
+ * Требуем ввести конкретное слово, а не «y»: `--force` перетирает чужую работу,
94
+ * и подтверждение должно стоить осознанного действия. В неинтерактивном режиме
95
+ * (CI, запуск из агента) подтвердить нельзя вовсе — это сознательно (D23).
96
+ */
97
+ async function confirmWord(question, expected) {
98
+ if (!process.stdin.isTTY) {
99
+ fail('Подтверждение возможно только в интерактивном терминале');
100
+ note((0, exports.dim)(' В CI это правильное поведение: расхождение версий должно ронять сборку, а не перетирать чужую работу'));
101
+ return false;
102
+ }
103
+ const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stderr });
104
+ try {
105
+ const answer = await new Promise((resolve) => {
106
+ rl.question(`${question}\n Введите «${expected}» для подтверждения: `, resolve);
107
+ });
108
+ return answer.trim() === expected;
109
+ }
110
+ finally {
111
+ rl.close();
112
+ }
113
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
4
+ /**
5
+ * Версия CLI. Держать синхронно с cli/package.json: читать package.json в
6
+ * рантайме нельзя, после сборки он лежит на уровень выше dist и в бандл не
7
+ * попадает.
8
+ */
9
+ exports.CLI_VERSION = '0.1.0';
10
+ /** Адрес платформы по умолчанию. Переопределяется XFLOW_API_URL и полем `api` в xflow.json. */
11
+ exports.DEFAULT_API_URL = 'https://app.getxflow.com';