@getxflow/cli 0.1.8 → 0.1.9

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 CHANGED
@@ -1,44 +1,45 @@
1
- # @getxflow/cli
2
-
3
- CLI платформы [XFlow](https://getxflow.com): синхронизация кода, сборка и деплой приложений.
4
-
5
- ```bash
6
- npm i -g @getxflow/cli
7
- xflow login
8
- xflow init my-app
9
- cd my-app && npm install
10
- xflow deploy
11
- xflow publish
12
- ```
13
-
14
- Требуется Node 20+. Зависимостей у пакета нет: он держит в себе ключ доступа, и чужого кода
15
- рядом с ним быть не должно.
16
-
17
- | Команда | Что делает |
18
- |---|---|
19
- | `login` / `logout` / `whoami` | вход через браузер, выход, чей ключ и что может |
20
- | `init` / `link` | новый проект, связать папку с существующим |
21
- | `status` / `push` / `pull` | состояние, отправка и получение исходников |
22
- | `deploy` / `publish` / `rollback` / `deployments` | сборка, публикация, откат, история версий |
23
- | `db status` / `db migrate` | миграции из `migrations/*.sql` с гейтом на разрушающие |
24
- | `functions list` / `functions deploy` | облачные функции проекта из `functions/<имя>/index.ts` |
25
- | `functions invoke` / `functions logs` | вызвать функцию, посмотреть её падения со стеком |
26
- | `schedules list` / `set` / `rm` | запуск функций по времени (таймер-триггеры) |
27
- | `env` / `env check` / `env set` | переменные окружения функций, значения наружу не отдаются |
28
- | `logs` | ошибки выложенного приложения в браузере |
29
- | `projects list` / `projects get` / `open` | проекты организации и адреса приложения |
30
- | `skills` | инструкция о платформе для ИИ-агента (формат [Agent Skills](https://agentskills.io)) |
31
- | `mcp install` | подключить агента к платформе напрямую, без терминала |
32
-
33
- Справка по команде: `xflow help <команда>`.
34
-
35
- Приложение вы пишете сами или своим агентом: платформа в код не лезет. `xflow skills` кладёт
36
- рядом описание платформы, которое Claude Code, Codex и OpenClaw подхватывают сами, когда задача
37
- касается XFlow. Ваши `AGENTS.md` и `CLAUDE.md` команда не трогает.
38
-
39
- `xflow mcp install` подключает агента к платформе напрямую: база, миграции, функции, расписания,
40
- переменные и версии становятся его инструментами, без разбора вывода команд. Код, сборка и выкладка
41
- остаются в CLI.
42
-
43
- Ключ хранится в `~/.xflow/credentials.json`, конфигурация проекта — в `xflow.json`.
44
- Для CI передайте ключ переменной `XFLOW_TOKEN`.
1
+ # @getxflow/cli
2
+
3
+ CLI for the [XFlow](https://getxflow.com) platform: source sync, builds and releases.
4
+
5
+ ```bash
6
+ npm i -g @getxflow/cli
7
+ xflow login
8
+ xflow init my-app
9
+ cd my-app && npm install
10
+ xflow deploy
11
+ xflow publish
12
+ ```
13
+
14
+ Requires Node 20+. The package has no dependencies: it holds an access key, and nobody
15
+ else's code belongs next to it.
16
+
17
+ | Command | What it does |
18
+ |---|---|
19
+ | `login` / `logout` / `whoami` | sign in through the browser, sign out, whose key this is and what it can do |
20
+ | `init` / `link` | new project, link a folder to an existing one |
21
+ | `status` / `push` / `pull` | state, sending and fetching sources |
22
+ | `deploy` / `publish` / `rollback` / `deployments` | build, publish, roll back, version history |
23
+ | `db status` / `db migrate` | migrations from `migrations/*.sql` with a gate on destructive ones |
24
+ | `functions list` / `functions deploy` | cloud functions of the project from `functions/<name>/index.ts` |
25
+ | `functions invoke` / `functions logs` | call a function, look at its crashes with the stack |
26
+ | `schedules list` / `set` / `rm` | running functions on a timer (timer triggers) |
27
+ | `env` / `env check` / `env set` | environment variables of the functions, values are never handed back |
28
+ | `logs` | browser errors from the released application |
29
+ | `projects list` / `projects get` / `open` | projects of the organization and the application addresses |
30
+ | `skills` | platform instructions for an AI agent ([Agent Skills](https://agentskills.io) format) |
31
+ | `mcp install` | connect the agent to the platform directly, without a terminal |
32
+
33
+ Help for one command: `xflow help <command>`.
34
+
35
+ You write the application yourself or with your agent: the platform stays out of the code.
36
+ `xflow skills` puts a description of the platform alongside it, which Claude Code, Codex and
37
+ OpenClaw pick up on their own when a task touches XFlow. Your `AGENTS.md` and `CLAUDE.md` are
38
+ left alone.
39
+
40
+ `xflow mcp install` connects the agent to the platform directly: the database, migrations,
41
+ functions, schedules, variables and versions become its tools, with no command output to parse.
42
+ Code, builds and releases stay in the CLI.
43
+
44
+ The key is stored in `~/.xflow/credentials.json`, the project configuration in `xflow.json`.
45
+ For CI, pass the key in the `XFLOW_TOKEN` variable.
package/dist/api.js CHANGED
@@ -6,27 +6,22 @@ exports.apiBinary = apiBinary;
6
6
  exports.apiUpload = apiUpload;
7
7
  exports.downloadUrl = downloadUrl;
8
8
  const version_1 = require("./version");
9
- /**
10
- * Клиент публичного API платформы.
11
- *
12
- * Ошибка сервера приходит в одном формате (`error`, `code`, `hint`) — здесь она
13
- * превращается в исключение, у которого есть что показать человеку. Плюс
14
- * проверка версии протокола: старый CLI обязан сказать «обновись», а не падать
15
- * на разборе неизвестного ответа.
16
- */
17
9
  class ApiError extends Error {
18
10
  code;
19
11
  status;
20
12
  hint;
21
13
  /** Разбор отказа, если он есть: например, нарушения, из-за которых не пошла сборка. */
22
14
  issues;
23
- constructor(message, code, status, hint, issues) {
15
+ /** Заполнен, когда упёрлись в тариф: команда верна, повторять её бесполезно. */
16
+ limit;
17
+ constructor(message, code, status, hint, issues, limit) {
24
18
  super(message);
25
19
  this.name = 'ApiError';
26
20
  this.code = code;
27
21
  this.status = status;
28
22
  this.hint = hint;
29
23
  this.issues = issues;
24
+ this.limit = limit;
30
25
  }
31
26
  }
32
27
  exports.ApiError = ApiError;
@@ -53,7 +48,7 @@ function isOlder(current, required) {
53
48
  function assertProtocol(response) {
54
49
  const required = response.headers.get('x-xflow-cli-min');
55
50
  if (required && isOlder(version_1.CLI_VERSION, required)) {
56
- throw new ApiError(`Платформа требует CLI не ниже ${required}, установлен ${version_1.CLI_VERSION}`, 'outdated_cli', response.status, 'Обновите: npm i -g @getxflow/cli');
51
+ throw new ApiError(`The platform requires CLI ${required} or newer, ${version_1.CLI_VERSION} is installed`, 'outdated_cli', response.status, 'Update: npm i -g @getxflow/cli');
57
52
  }
58
53
  }
59
54
  async function send(client, path, options = {}) {
@@ -88,7 +83,7 @@ async function send(client, path, options = {}) {
88
83
  }
89
84
  catch (e) {
90
85
  const reason = e instanceof Error ? e.message : String(e);
91
- throw new ApiError(`Платформа недоступна: ${reason}`, 'network', 0, `Проверьте соединение и адрес ${client.apiUrl}`);
86
+ throw new ApiError(`The platform is unreachable: ${reason}`, 'network', 0, `Check the connection and the address ${client.apiUrl}`);
92
87
  }
93
88
  assertProtocol(response);
94
89
  return response;
@@ -97,10 +92,10 @@ async function readError(response) {
97
92
  const text = await response.text().catch(() => '');
98
93
  try {
99
94
  const parsed = JSON.parse(text);
100
- return new ApiError(parsed.error || `Запрос отклонён (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed.issues);
95
+ return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed.issues, parsed.limit);
101
96
  }
102
97
  catch {
103
- return new ApiError(`Запрос отклонён (${response.status})`, 'unknown', response.status, text.slice(0, 200) || undefined);
98
+ return new ApiError(`Request rejected (${response.status})`, 'unknown', response.status, text.slice(0, 200) || undefined);
104
99
  }
105
100
  }
106
101
  async function apiJson(client, path, options = {}) {
@@ -127,10 +122,10 @@ async function downloadUrl(url) {
127
122
  response = await fetch(url, { signal: AbortSignal.timeout(TRANSFER_TIMEOUT_MS) });
128
123
  }
129
124
  catch (e) {
130
- throw new ApiError(`Не удалось скачать архив: ${e instanceof Error ? e.message : e}`, 'network', 0);
125
+ throw new ApiError(`Could not download the archive: ${e instanceof Error ? e.message : e}`, 'network', 0);
131
126
  }
132
127
  if (!response.ok) {
133
- throw new ApiError(`Не удалось скачать архив (${response.status})`, 'download_failed', response.status);
128
+ throw new ApiError(`Could not download the archive (${response.status})`, 'download_failed', response.status);
134
129
  }
135
130
  return Buffer.from(await response.arrayBuffer());
136
131
  }
package/dist/args.js CHANGED
@@ -80,7 +80,7 @@ function flagNumber(args, name) {
80
80
  return undefined;
81
81
  const parsed = Number(value);
82
82
  if (!Number.isInteger(parsed) || parsed < 0) {
83
- throw new Error(`Значение --${name} должно быть целым числом, получено «${value}»`);
83
+ throw new Error(`The value of --${name} has to be an integer, got "${value}"`);
84
84
  }
85
85
  return parsed;
86
86
  }
package/dist/bin.js CHANGED
@@ -6,6 +6,7 @@ const args_1 = require("./args");
6
6
  const config_1 = require("./config");
7
7
  const errors_1 = require("./errors");
8
8
  const help_1 = require("./help");
9
+ const limits_1 = require("./limits");
9
10
  const ui_1 = require("./ui");
10
11
  const zip_1 = require("./zip");
11
12
  const version_1 = require("./version");
@@ -58,7 +59,7 @@ async function run(args) {
58
59
  await (0, projects_1.list)();
59
60
  return;
60
61
  }
61
- throw new errors_1.CliError(`Неизвестная команда: projects ${second}`, 'Есть list и get');
62
+ throw new errors_1.CliError(`Unknown command: projects ${second}`, 'Available: list and get');
62
63
  case 'skills':
63
64
  (0, skills_1.skills)(rest);
64
65
  return;
@@ -67,7 +68,7 @@ async function run(args) {
67
68
  await (0, mcp_1.mcpInstall)(rest);
68
69
  return;
69
70
  }
70
- throw new errors_1.CliError(`Неизвестная команда: mcp ${second}`, 'Есть install');
71
+ throw new errors_1.CliError(`Unknown command: mcp ${second}`, 'Available: install');
71
72
  case 'functions':
72
73
  if (second === 'deploy') {
73
74
  await (0, functions_1.functionsDeploy)(rest);
@@ -85,7 +86,7 @@ async function run(args) {
85
86
  await (0, functions_1.functionsList)();
86
87
  return;
87
88
  }
88
- throw new errors_1.CliError(`Неизвестная команда: functions ${second}`, 'Есть list, deploy, invoke и logs');
89
+ throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list, deploy, invoke and logs');
89
90
  case 'logs':
90
91
  await (0, logs_1.logs)(rest);
91
92
  return;
@@ -106,7 +107,7 @@ async function run(args) {
106
107
  await (0, env_1.envList)();
107
108
  return;
108
109
  }
109
- throw new errors_1.CliError(`Неизвестная команда: env ${second}`, 'Есть list, check, set и rm');
110
+ throw new errors_1.CliError(`Unknown command: env ${second}`, 'Available: list, check, set and rm');
110
111
  case 'schedules':
111
112
  if (second === 'set') {
112
113
  await (0, schedules_1.schedulesSet)(rest);
@@ -120,7 +121,7 @@ async function run(args) {
120
121
  await (0, schedules_1.schedulesList)();
121
122
  return;
122
123
  }
123
- throw new errors_1.CliError(`Неизвестная команда: schedules ${second}`, 'Есть list, set и rm');
124
+ throw new errors_1.CliError(`Unknown command: schedules ${second}`, 'Available: list, set and rm');
124
125
  case 'db':
125
126
  if (second === 'migrate') {
126
127
  await (0, db_1.dbMigrate)(rest);
@@ -130,7 +131,7 @@ async function run(args) {
130
131
  await (0, db_1.dbStatus)();
131
132
  return;
132
133
  }
133
- throw new errors_1.CliError(`Неизвестная команда: db ${second}`, 'Есть status и migrate');
134
+ throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status and migrate');
134
135
  case 'status':
135
136
  await (0, sources_1.status)();
136
137
  return;
@@ -153,7 +154,7 @@ async function run(args) {
153
154
  await (0, deploy_1.deployments)();
154
155
  return;
155
156
  default:
156
- throw new errors_1.CliError(`Неизвестная команда: ${first}`, 'Список команд: xflow help');
157
+ throw new errors_1.CliError(`Unknown command: ${first}`, 'The list of commands: xflow help');
157
158
  }
158
159
  }
159
160
  async function main() {
@@ -173,6 +174,9 @@ async function main() {
173
174
  catch (e) {
174
175
  if (e instanceof api_1.ApiError) {
175
176
  (0, ui_1.fail)(e.message);
177
+ // Отказ по тарифу помечаем явно: иначе агент правит команду, которая верна.
178
+ if (e.limit)
179
+ (0, ui_1.note)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
176
180
  if (e.hint)
177
181
  (0, ui_1.note)((0, ui_1.dim)(` ${e.hint}`));
178
182
  return 1;
@@ -8,6 +8,7 @@ const api_1 = require("../api");
8
8
  const config_1 = require("../config");
9
9
  const credentials_1 = require("../credentials");
10
10
  const errors_1 = require("../errors");
11
+ const limits_1 = require("../limits");
11
12
  const session_1 = require("../session");
12
13
  const ui_1 = require("../ui");
13
14
  function localConfig() {
@@ -38,18 +39,18 @@ async function login() {
38
39
  body: { client_name: (0, node_os_1.hostname)() },
39
40
  });
40
41
  (0, ui_1.note)('');
41
- (0, ui_1.note)(` Код подтверждения: ${(0, ui_1.bold)(start.user_code)}`);
42
- (0, ui_1.note)(` Компьютер: ${(0, node_os_1.hostname)()}`);
43
- (0, ui_1.note)(` Ссылка для входа: ${start.verification_uri_complete}`);
42
+ (0, ui_1.note)(` Confirmation code: ${(0, ui_1.bold)(start.user_code)}`);
43
+ (0, ui_1.note)(` Computer: ${(0, node_os_1.hostname)()}`);
44
+ (0, ui_1.note)(` Sign-in link: ${start.verification_uri_complete}`);
44
45
  (0, ui_1.note)('');
45
46
  // Браузер не открываем сами: команду часто запускает агент, и всплывшее окно
46
47
  // забирает у человека внимание, не объяснив, что он подтверждает.
47
- (0, ui_1.step)('Откройте ссылку и сверьте код на странице с показанным выше');
48
+ (0, ui_1.step)('Open the link and check the code on the page against the one above');
48
49
  const deadline = Date.now() + start.expires_in * 1000;
49
50
  let intervalMs = Math.max(start.interval, 1) * 1000;
50
51
  for (;;) {
51
52
  if (Date.now() > deadline) {
52
- throw new errors_1.CliError('Срок кода истёк', 'Повторите: xflow login');
53
+ throw new errors_1.CliError('The code has expired', 'Try again: xflow login');
53
54
  }
54
55
  await sleep(intervalMs);
55
56
  let poll;
@@ -74,54 +75,55 @@ async function login() {
74
75
  organizationId: poll.organization_id,
75
76
  projectId: poll.project_id,
76
77
  });
77
- (0, ui_1.ok)(`Вход выполнен, ключ сохранён для ${client.apiUrl}`);
78
+ (0, ui_1.ok)(`Signed in, the key is stored for ${client.apiUrl}`);
78
79
  return;
79
80
  }
80
81
  }
81
82
  function logout() {
82
83
  const apiUrl = (0, config_1.apiUrlFor)(localConfig());
83
84
  if ((0, credentials_1.forgetCredential)(apiUrl)) {
84
- (0, ui_1.ok)(`Ключ для ${apiUrl} удалён`);
85
+ (0, ui_1.ok)(`The key for ${apiUrl} is deleted`);
85
86
  }
86
87
  else {
87
- (0, ui_1.warn)(`Сохранённого ключа для ${apiUrl} нет`);
88
+ (0, ui_1.warn)(`There is no stored key for ${apiUrl}`);
88
89
  }
89
90
  }
90
91
  /** Приписка к тарифу. У живой подписки её нет: молчание и значит «всё в порядке». */
91
92
  const SUBSCRIPTION_STATE = {
92
- past_due: ', период кончился',
93
- suspended: ', работа приостановлена',
94
- terminated: ', подписка прекращена',
95
- };
96
- const QUOTA_LABELS = {
97
- projects: 'проекты',
98
- functions: 'функции',
99
- developers: 'разработчики',
100
- members: 'участники',
93
+ past_due: ', the period has ended',
94
+ suspended: ', work is suspended',
95
+ terminated: ', the subscription is terminated',
101
96
  };
102
97
  async function whoami() {
103
98
  const config = localConfig();
104
99
  const client = (0, session_1.connect)(config);
105
100
  const me = await (0, api_1.apiJson)(client, '/api/v1/me');
106
- (0, ui_1.out)(`Организация: ${me.organization.name ?? me.organization.id}`);
107
- (0, ui_1.out)(`Ключ: xfk_${me.key.prefix}… (${me.key.scopes.join(', ')})`);
108
- (0, ui_1.out)(`Платформа: ${client.apiUrl}`);
101
+ (0, ui_1.out)(`Organization: ${me.organization.name ?? me.organization.id}`);
102
+ (0, ui_1.out)(`Key: xfk_${me.key.prefix}… (${me.key.scopes.join(', ')})`);
103
+ (0, ui_1.out)(`Platform: ${client.apiUrl}`);
109
104
  if (process.env.XFLOW_TOKEN) {
110
- (0, ui_1.note)((0, ui_1.dim)(' Ключ взят из XFLOW_TOKEN, сохранённый в ~/.xflow не используется'));
105
+ (0, ui_1.note)((0, ui_1.dim)(' The key comes from XFLOW_TOKEN, the one stored in ~/.xflow is not used'));
111
106
  }
112
107
  else if (!(0, credentials_1.readCredential)(client.apiUrl)) {
113
- (0, ui_1.note)((0, ui_1.dim)(' Ключ не сохранён'));
108
+ (0, ui_1.note)((0, ui_1.dim)(' The key is not stored'));
114
109
  }
115
110
  if (me.billing) {
111
+ const pkg = me.billing.package_title ? `, ${me.billing.package_title}` : '';
116
112
  (0, ui_1.out)('');
117
- (0, ui_1.out)(`Тариф: ${me.billing.plan_title}${SUBSCRIPTION_STATE[me.billing.status] ?? ''}`);
118
- (0, ui_1.out)(`Баланс: ${me.billing.balance ?? '—'}`);
113
+ (0, ui_1.out)(`Plan: ${me.billing.plan_title}${pkg}${SUBSCRIPTION_STATE[me.billing.status] ?? ''}`);
119
114
  }
120
115
  if (me.quotas) {
121
116
  (0, ui_1.out)('');
122
- (0, ui_1.out)('Лимиты тарифа:');
123
- for (const [name, value] of Object.entries(me.quotas)) {
124
- (0, ui_1.out)(` ${QUOTA_LABELS[name] ?? name}: ${value.used} из ${value.limit}`);
125
- }
117
+ (0, ui_1.out)('Plan limits:');
118
+ (0, ui_1.table)((0, limits_1.quotaRows)(me.quotas));
119
+ }
120
+ if (me.features) {
121
+ const interval = me.features.schedule_min_interval_min;
122
+ (0, ui_1.out)('');
123
+ (0, ui_1.out)(`Schedules: ${me.features.schedules
124
+ ? interval
125
+ ? `yes, no more often than one run every ${interval} min`
126
+ : 'yes'
127
+ : 'no'}`);
126
128
  }
127
129
  }
@@ -38,7 +38,7 @@ function readMigrations(root) {
38
38
  function requireMigrations(root) {
39
39
  const migrations = readMigrations(root);
40
40
  if (migrations.length === 0) {
41
- throw new errors_1.CliError( проекте нет миграций`, `Положите SQL в ${MIGRATIONS_DIR}/0001_init.sql и повторите`);
41
+ throw new errors_1.CliError(`The project has no migrations`, `Put the SQL in ${MIGRATIONS_DIR}/0001_init.sql and try again`);
42
42
  }
43
43
  return migrations;
44
44
  }
@@ -48,23 +48,23 @@ async function dbStatus() {
48
48
  const local = readMigrations(root);
49
49
  const history = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`);
50
50
  const applied = new Map(history.applied.map((row) => [row.name, row]));
51
- (0, ui_1.out)(`Схема: ${(0, ui_1.bold)(history.schema)}`);
51
+ (0, ui_1.out)(`Schema: ${(0, ui_1.bold)(history.schema)}`);
52
52
  (0, ui_1.out)('');
53
53
  if (local.length === 0 && applied.size === 0) {
54
- (0, ui_1.note)(`Миграций нет. Первая: ${MIGRATIONS_DIR}/0001_init.sql`);
54
+ (0, ui_1.note)(`No migrations. The first one: ${MIGRATIONS_DIR}/0001_init.sql`);
55
55
  return;
56
56
  }
57
57
  const rows = [];
58
58
  for (const migration of local) {
59
59
  const row = applied.get(migration.name);
60
60
  if (!row) {
61
- rows.push([migration.name, 'ждёт применения', '']);
61
+ rows.push([migration.name, 'waiting', '']);
62
62
  continue;
63
63
  }
64
64
  const changed = row.checksum !== checksum(migration.sql);
65
65
  rows.push([
66
66
  migration.name,
67
- changed ? 'применена, файл изменён' : 'применена',
67
+ changed ? 'applied, file changed' : 'applied',
68
68
  (0, ui_1.formatAge)(row.applied_at),
69
69
  ]);
70
70
  }
@@ -73,12 +73,12 @@ async function dbStatus() {
73
73
  const localNames = new Set(local.map((migration) => migration.name));
74
74
  for (const row of history.applied) {
75
75
  if (!localNames.has(row.name))
76
- rows.push([row.name, 'применена, файла нет', (0, ui_1.formatAge)(row.applied_at)]);
76
+ rows.push([row.name, 'applied, file missing', (0, ui_1.formatAge)(row.applied_at)]);
77
77
  }
78
78
  (0, ui_1.table)(rows);
79
- if (rows.some((row) => row[1] === 'применена, файл изменён')) {
79
+ if (rows.some((row) => row[1] === 'applied, file changed')) {
80
80
  (0, ui_1.out)('');
81
- (0, ui_1.warn)('Изменённый файл повторно не применится: заведите новую миграцию');
81
+ (0, ui_1.warn)('A changed file will not be applied again: write a new migration');
82
82
  }
83
83
  }
84
84
  async function dbMigrate(args) {
@@ -86,7 +86,7 @@ async function dbMigrate(args) {
86
86
  const client = (0, session_1.connect)(config);
87
87
  const migrations = requireMigrations(root);
88
88
  const dryRun = (0, args_1.flagBool)(args, 'dry-run');
89
- (0, ui_1.step)(dryRun ? `Проверяю миграции (${migrations.length} в каталоге)` : `Применяю миграции (${migrations.length} в каталоге)`);
89
+ (0, ui_1.step)(dryRun ? `Checking the migrations (${migrations.length} in the directory)` : `Applying the migrations (${migrations.length} in the directory)`);
90
90
  const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`, {
91
91
  method: 'POST',
92
92
  body: {
@@ -97,34 +97,34 @@ async function dbMigrate(args) {
97
97
  timeoutMs: 300_000,
98
98
  });
99
99
  if (result.destructive.length > 0) {
100
- (0, ui_1.warn)('Разрушающие операции:');
100
+ (0, ui_1.warn)('Destructive operations:');
101
101
  for (const hit of result.destructive)
102
102
  (0, ui_1.note)((0, ui_1.dim)(` ${hit}`));
103
103
  }
104
104
  if (result.dry_run) {
105
105
  if (result.pending.length === 0) {
106
- (0, ui_1.ok)('Применять нечего: все миграции уже в базе');
106
+ (0, ui_1.ok)('Nothing to apply: every migration is already in the database');
107
107
  return;
108
108
  }
109
109
  (0, ui_1.out)('');
110
- (0, ui_1.out)(`Будут применены (${result.pending.length}):`);
110
+ (0, ui_1.out)(`Will be applied (${result.pending.length}):`);
111
111
  for (const name of result.pending)
112
112
  (0, ui_1.out)(` ${name}`);
113
- (0, ui_1.note)((0, ui_1.dim)(' Пробный прогон: база не менялась'));
113
+ (0, ui_1.note)((0, ui_1.dim)(' Dry run: the database was not changed'));
114
114
  return;
115
115
  }
116
116
  for (const dump of result.dumps) {
117
- (0, ui_1.note)((0, ui_1.dim)(` Дамп ${dump.table}: строк ${dump.rows}${dump.truncated ? ' (обрезан по потолку)' : ''}, хранится ${result.dump_retention_days} суток`));
117
+ (0, ui_1.note)((0, ui_1.dim)(` Dump of ${dump.table}: ${dump.rows} rows${dump.truncated ? ' (cut at the cap)' : ''}, kept for ${result.dump_retention_days} days`));
118
118
  }
119
119
  if (result.applied.length > 0) {
120
- (0, ui_1.ok)(`Применено: ${result.applied.join(', ')}`);
120
+ (0, ui_1.ok)(`Applied: ${result.applied.join(', ')}`);
121
121
  }
122
122
  else if (!result.failed) {
123
- (0, ui_1.ok)('Применять нечего: все миграции уже в базе');
123
+ (0, ui_1.ok)('Nothing to apply: every migration is already in the database');
124
124
  }
125
125
  if (result.failed) {
126
126
  throw new errors_1.CliError(`${result.failed.name}: ${result.failed.error}`, result.applied.length > 0
127
- ? `Применённое до неё осталось в базе (${result.applied.join(', ')}), остальное не запускалось`
128
- : 'База не изменилась: миграция откатилась целиком');
127
+ ? `What ran before it stays in the database (${result.applied.join(', ')}), the rest did not run`
128
+ : 'The database is unchanged: the migration rolled back as a whole');
129
129
  }
130
130
  }
@@ -53,7 +53,7 @@ async function waitForBuild(client, projectId, started) {
53
53
  if (build.phase === 'done' || build.phase === 'failed')
54
54
  return build;
55
55
  }
56
- throw new errors_1.CliError('Сборка идёт дольше ожидаемого', `Посмотреть состояние: xflow deployments. Версия ${started.deploy_id}`);
56
+ throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Version ${started.deploy_id}`);
57
57
  }
58
58
  async function deploy(args) {
59
59
  const { root, config } = (0, config_1.requireProject)();
@@ -62,15 +62,15 @@ async function deploy(args) {
62
62
  if ((0, args_1.flagBool)(args, 'no-push')) {
63
63
  const server = await (0, sources_1.latestRevision)(client, config.projectId);
64
64
  if (!server) {
65
- throw new errors_1.CliError('На сервере нет исходников', 'Отправьте их: xflow push');
65
+ throw new errors_1.CliError('The server holds no sources', 'Send them: xflow push');
66
66
  }
67
67
  revision = server.revision;
68
- (0, ui_1.warn)(`Исходники не отправлены (--no-push), сборка пойдёт от ревизии ${revision}`);
68
+ (0, ui_1.warn)(`Sources not sent (--no-push), the build runs from revision ${revision}`);
69
69
  }
70
70
  else {
71
71
  revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
72
72
  }
73
- (0, ui_1.step)(`Собираю на платформе из ревизии ${revision}`);
73
+ (0, ui_1.step)(`Building on the platform from revision ${revision}`);
74
74
  let started;
75
75
  try {
76
76
  started = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/builds`, {
@@ -89,22 +89,22 @@ async function deploy(args) {
89
89
  (0, ui_1.out)();
90
90
  (0, ui_1.out)(build.log_tail);
91
91
  }
92
- throw new errors_1.CliError(build.error || 'Сборка не удалась', `Исправьте код и повторите: xflow deploy. Версия ${build.deploy_id}`);
92
+ throw new errors_1.CliError(build.error || 'The build failed', `Fix the code and try again: xflow deploy. Version ${build.deploy_id}`);
93
93
  }
94
- (0, ui_1.ok)(`Версия ${build.deploy_id} собрана из ревизии ${build.revision} за ${build.elapsed_s} с`);
94
+ (0, ui_1.ok)(`Version ${build.deploy_id} built from revision ${build.revision} in ${build.elapsed_s} s`);
95
95
  (0, ui_1.out)(build.project_url);
96
- (0, ui_1.note)((0, ui_1.dim)(' Показать посетителям: xflow publish'));
96
+ (0, ui_1.note)((0, ui_1.dim)(' Show it to visitors: xflow publish'));
97
97
  }
98
98
  async function publish() {
99
99
  const { config } = (0, config_1.requireProject)();
100
100
  const client = (0, session_1.connect)(config);
101
- (0, ui_1.step)('Публикую текущую dev-версию');
101
+ (0, ui_1.step)('Publishing the current dev version');
102
102
  const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/publish`, { method: 'POST', timeoutMs: 120_000 });
103
103
  if (result.already_published) {
104
- (0, ui_1.ok)('Эта версия уже опубликована');
104
+ (0, ui_1.ok)('This version is already published');
105
105
  }
106
106
  else {
107
- (0, ui_1.ok)(`Опубликована версия ${result.deploy_id}: её видят посетители`);
107
+ (0, ui_1.ok)(`Version ${result.deploy_id} published: visitors see it now`);
108
108
  }
109
109
  (0, ui_1.out)(result.project_url);
110
110
  }
@@ -112,29 +112,29 @@ async function rollback(args) {
112
112
  const { config } = (0, config_1.requireProject)();
113
113
  const deployId = args.words[0];
114
114
  if (!deployId) {
115
- throw new errors_1.CliError('Нужен номер версии', 'Посмотреть версии: xflow deployments. Пример: xflow rollback 481203');
115
+ throw new errors_1.CliError('A version number is required', 'To see the versions: xflow deployments. For example: xflow rollback 481203');
116
116
  }
117
117
  const client = (0, session_1.connect)(config);
118
118
  const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/rollback`, { method: 'POST', body: { deploy_id: deployId } });
119
- (0, ui_1.ok)(`dev-версия проекта переключена на ${result.deploy_id}`);
119
+ (0, ui_1.ok)(`The dev version of the project is switched to ${result.deploy_id}`);
120
120
  (0, ui_1.out)(result.project_url);
121
121
  if (result.revision !== null) {
122
- (0, ui_1.note)((0, ui_1.dim)(` Код этой версии: xflow pull --revision ${result.revision} --into ./v${result.deploy_id}`));
122
+ (0, ui_1.note)((0, ui_1.dim)(` The code of this version: xflow pull --revision ${result.revision} --into ./v${result.deploy_id}`));
123
123
  }
124
- (0, ui_1.note)((0, ui_1.dim)(' Посетители увидят её только после xflow publish'));
124
+ (0, ui_1.note)((0, ui_1.dim)(' Visitors see it only after xflow publish'));
125
125
  }
126
126
  async function deployments() {
127
127
  const { config } = (0, config_1.requireProject)();
128
128
  const client = (0, session_1.connect)(config);
129
129
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/deployments`);
130
130
  if (data.deployments.length === 0) {
131
- (0, ui_1.note)('Версий пока нет. Собрать и выложить: xflow deploy');
131
+ (0, ui_1.note)('No versions yet. To build and release: xflow deploy');
132
132
  return;
133
133
  }
134
134
  (0, ui_1.table)(data.deployments.map((d) => [
135
- d.deploy_id ?? '',
136
- d.revision !== null ? `ревизия ${d.revision}` : 'без ревизии',
137
- d.status === 'deployed' ? 'готова' : d.status,
135
+ d.deploy_id ?? '-',
136
+ d.revision !== null ? `revision ${d.revision}` : 'no revision',
137
+ d.status === 'deployed' ? 'ready' : d.status,
138
138
  (0, ui_1.formatAge)(d.deployed_at ?? d.created_at),
139
139
  [d.is_dev ? (0, ui_1.bold)('dev') : '', d.is_live ? (0, ui_1.bold)('live') : ''].filter(Boolean).join(' '),
140
140
  ]));