@getxflow/cli 0.1.8 → 0.1.10

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.
@@ -12,18 +12,9 @@ const session_1 = require("../session");
12
12
  const ui_1 = require("../ui");
13
13
  const sources_1 = require("./sources");
14
14
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
- /**
16
- * Потолок ожидания сборки в терминале. Чуть больше, чем срок, после которого
17
- * платформа закрывает молчащую сборку: так разработчик увидит внятный отказ, а
18
- * не «идёт дольше ожидаемого» на сборке, которую уже похоронили.
19
- */
15
+ /** Slightly longer than the platform's own build timeout. */
20
16
  const WAIT_LIMIT_MS = 16 * 60_000;
21
- /**
22
- * Показать, чем проект не подошёл платформе.
23
- *
24
- * Печатаем весь список: правила известны заранее (скилл xflow), а узнавать их по
25
- * одному через повторные запуски значит терять круг на каждое нарушение.
26
- */
17
+ /** Print every check violation at once. */
27
18
  function reportIssues(e) {
28
19
  const issues = e.issues ?? [];
29
20
  for (const issue of issues) {
@@ -33,13 +24,7 @@ function reportIssues(e) {
33
24
  }
34
25
  throw new errors_1.CliError(e.message, e.hint);
35
26
  }
36
- /**
37
- * Дождаться конца сборки, печатая фазы.
38
- *
39
- * Платформа отвечает на запуск сразу и не держит соединение: сборка живёт в
40
- * песочнице и переживает перезапуск платформы, поэтому её состояние забирается
41
- * опросом, а не потоком.
42
- */
27
+ /** Poll the build until it finishes, printing phases. */
43
28
  async function waitForBuild(client, projectId, started) {
44
29
  const deadline = Date.now() + WAIT_LIMIT_MS;
45
30
  let shown = '';
@@ -53,7 +38,7 @@ async function waitForBuild(client, projectId, started) {
53
38
  if (build.phase === 'done' || build.phase === 'failed')
54
39
  return build;
55
40
  }
56
- throw new errors_1.CliError('Сборка идёт дольше ожидаемого', `Посмотреть состояние: xflow deployments. Версия ${started.deploy_id}`);
41
+ throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Version ${started.deploy_id}`);
57
42
  }
58
43
  async function deploy(args) {
59
44
  const { root, config } = (0, config_1.requireProject)();
@@ -62,15 +47,15 @@ async function deploy(args) {
62
47
  if ((0, args_1.flagBool)(args, 'no-push')) {
63
48
  const server = await (0, sources_1.latestRevision)(client, config.projectId);
64
49
  if (!server) {
65
- throw new errors_1.CliError('На сервере нет исходников', 'Отправьте их: xflow push');
50
+ throw new errors_1.CliError('The server holds no sources', 'Send them: xflow push');
66
51
  }
67
52
  revision = server.revision;
68
- (0, ui_1.warn)(`Исходники не отправлены (--no-push), сборка пойдёт от ревизии ${revision}`);
53
+ (0, ui_1.warn)(`Sources not sent (--no-push), the build runs from revision ${revision}`);
69
54
  }
70
55
  else {
71
56
  revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
72
57
  }
73
- (0, ui_1.step)(`Собираю на платформе из ревизии ${revision}`);
58
+ (0, ui_1.step)(`Building on the platform from revision ${revision}`);
74
59
  let started;
75
60
  try {
76
61
  started = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/builds`, {
@@ -89,22 +74,22 @@ async function deploy(args) {
89
74
  (0, ui_1.out)();
90
75
  (0, ui_1.out)(build.log_tail);
91
76
  }
92
- throw new errors_1.CliError(build.error || 'Сборка не удалась', `Исправьте код и повторите: xflow deploy. Версия ${build.deploy_id}`);
77
+ throw new errors_1.CliError(build.error || 'The build failed', `Fix the code and try again: xflow deploy. Version ${build.deploy_id}`);
93
78
  }
94
- (0, ui_1.ok)(`Версия ${build.deploy_id} собрана из ревизии ${build.revision} за ${build.elapsed_s} с`);
79
+ (0, ui_1.ok)(`Version ${build.deploy_id} built from revision ${build.revision} in ${build.elapsed_s} s`);
95
80
  (0, ui_1.out)(build.project_url);
96
- (0, ui_1.note)((0, ui_1.dim)(' Показать посетителям: xflow publish'));
81
+ (0, ui_1.note)((0, ui_1.dim)(' Show it to visitors: xflow publish'));
97
82
  }
98
83
  async function publish() {
99
84
  const { config } = (0, config_1.requireProject)();
100
85
  const client = (0, session_1.connect)(config);
101
- (0, ui_1.step)('Публикую текущую dev-версию');
86
+ (0, ui_1.step)('Publishing the current dev version');
102
87
  const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/publish`, { method: 'POST', timeoutMs: 120_000 });
103
88
  if (result.already_published) {
104
- (0, ui_1.ok)('Эта версия уже опубликована');
89
+ (0, ui_1.ok)('This version is already published');
105
90
  }
106
91
  else {
107
- (0, ui_1.ok)(`Опубликована версия ${result.deploy_id}: её видят посетители`);
92
+ (0, ui_1.ok)(`Version ${result.deploy_id} published: visitors see it now`);
108
93
  }
109
94
  (0, ui_1.out)(result.project_url);
110
95
  }
@@ -112,29 +97,29 @@ async function rollback(args) {
112
97
  const { config } = (0, config_1.requireProject)();
113
98
  const deployId = args.words[0];
114
99
  if (!deployId) {
115
- throw new errors_1.CliError('Нужен номер версии', 'Посмотреть версии: xflow deployments. Пример: xflow rollback 481203');
100
+ throw new errors_1.CliError('A version number is required', 'To see the versions: xflow deployments. For example: xflow rollback 481203');
116
101
  }
117
102
  const client = (0, session_1.connect)(config);
118
103
  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}`);
104
+ (0, ui_1.ok)(`The dev version of the project is switched to ${result.deploy_id}`);
120
105
  (0, ui_1.out)(result.project_url);
121
106
  if (result.revision !== null) {
122
- (0, ui_1.note)((0, ui_1.dim)(` Код этой версии: xflow pull --revision ${result.revision} --into ./v${result.deploy_id}`));
107
+ (0, ui_1.note)((0, ui_1.dim)(` The code of this version: xflow pull --revision ${result.revision} --into ./v${result.deploy_id}`));
123
108
  }
124
- (0, ui_1.note)((0, ui_1.dim)(' Посетители увидят её только после xflow publish'));
109
+ (0, ui_1.note)((0, ui_1.dim)(' Visitors see it only after xflow publish'));
125
110
  }
126
111
  async function deployments() {
127
112
  const { config } = (0, config_1.requireProject)();
128
113
  const client = (0, session_1.connect)(config);
129
114
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/deployments`);
130
115
  if (data.deployments.length === 0) {
131
- (0, ui_1.note)('Версий пока нет. Собрать и выложить: xflow deploy');
116
+ (0, ui_1.note)('No versions yet. To build and release: xflow deploy');
132
117
  return;
133
118
  }
134
119
  (0, ui_1.table)(data.deployments.map((d) => [
135
- d.deploy_id ?? '',
136
- d.revision !== null ? `ревизия ${d.revision}` : 'без ревизии',
137
- d.status === 'deployed' ? 'готова' : d.status,
120
+ d.deploy_id ?? '-',
121
+ d.revision !== null ? `revision ${d.revision}` : 'no revision',
122
+ d.status === 'deployed' ? 'ready' : d.status,
138
123
  (0, ui_1.formatAge)(d.deployed_at ?? d.created_at),
139
124
  [d.is_dev ? (0, ui_1.bold)('dev') : '', d.is_live ? (0, ui_1.bold)('live') : ''].filter(Boolean).join(' '),
140
125
  ]));
@@ -13,9 +13,9 @@ const errors_1 = require("../errors");
13
13
  const session_1 = require("../session");
14
14
  const ui_1 = require("../ui");
15
15
  const FUNCTIONS_DIR = 'functions';
16
- /** Что функции просят у окружения: `process.env.NAME` и `process.env['NAME']`. */
16
+ /** Matches `process.env.NAME` and `process.env['NAME']`. */
17
17
  const ENV_REFERENCE = /process\.env(?:\.([A-Z0-9_]+)|\[['"]([A-Z0-9_]+)['"]\])/g;
18
- /** Свои переменные платформа кладёт сама, спрашивать их у пользователя незачем. */
18
+ /** Set by the platform itself. */
19
19
  const PROVIDED = new Set([
20
20
  'XFLOW_PROJECT_ID',
21
21
  'XFLOW_PROJECT_TOKEN',
@@ -37,7 +37,7 @@ function sourceFiles(dir, found = []) {
37
37
  }
38
38
  return found;
39
39
  }
40
- /** Какие переменные упоминают функции проекта, и в какой из них. */
40
+ /** Which variables the project's functions read, and in which of them. */
41
41
  function referencedByFunctions(root) {
42
42
  const needed = new Map();
43
43
  const provided = new Map();
@@ -66,31 +66,23 @@ async function fetchVariables() {
66
66
  async function envList() {
67
67
  const { rows } = await fetchVariables();
68
68
  if (rows.length === 0) {
69
- (0, ui_1.note)('Переменных нет');
70
- (0, ui_1.note)((0, ui_1.dim)(' Записать: xflow env set SMTP_PASSWORD=секрет'));
69
+ (0, ui_1.note)('No variables');
70
+ (0, ui_1.note)((0, ui_1.dim)(' To store one: xflow env set SMTP_PASSWORD=secret'));
71
71
  return;
72
72
  }
73
- (0, ui_1.table)(rows.map((row) => [row.name, row.scope === 'project' ? 'только этот проект' : 'вся организация']));
74
- (0, ui_1.note)((0, ui_1.dim)(' Значения платформа не отдаёт: их видно только внутри функции'));
73
+ (0, ui_1.table)(rows.map((row) => [row.name, row.scope === 'project' ? 'this project only' : 'the whole organization']));
74
+ (0, ui_1.note)((0, ui_1.dim)(' The platform never returns values: they are visible only inside the function'));
75
75
  }
76
- /**
77
- * Сверить, хватает ли функциям переменных.
78
- *
79
- * Это ответ на самый частый способ потерять полчаса: функция выкатилась, а падает
80
- * на пустом process.env, потому что переменную забыли записать наверх.
81
- */
76
+ /** Check that every referenced variable is stored. */
82
77
  async function envCheck() {
83
78
  const { names, root } = await fetchVariables();
84
79
  const { needed, provided } = referencedByFunctions(root);
85
80
  if (needed.size === 0 && provided.size === 0) {
86
- (0, ui_1.note)('Функции проекта не читают переменных окружения');
81
+ (0, ui_1.note)('The functions of this project read no environment variables');
87
82
  return;
88
83
  }
89
- // Платформенные показываем отдельной таблицей, а не молчим о них: раньше
90
- // функция, читающая только DATABASE_URL, получала ответ «переменных не
91
- // читают», то есть команда делала ложное утверждение о собственном коде.
92
84
  if (provided.size > 0) {
93
- (0, ui_1.out)((0, ui_1.bold)('Даёт платформа:'));
85
+ (0, ui_1.out)((0, ui_1.bold)('Provided by the platform:'));
94
86
  (0, ui_1.table)([...provided.entries()].sort().map(([name, users]) => [name, users.join(', ')]));
95
87
  if (needed.size > 0)
96
88
  (0, ui_1.out)('');
@@ -105,24 +97,24 @@ async function envCheck() {
105
97
  missing.push(row);
106
98
  }
107
99
  if (present.length > 0) {
108
- (0, ui_1.out)((0, ui_1.bold)('Записаны на платформе:'));
100
+ (0, ui_1.out)((0, ui_1.bold)('Stored on the platform:'));
109
101
  (0, ui_1.table)(present);
110
102
  }
111
103
  if (missing.length === 0) {
112
- (0, ui_1.ok)('Всем функциям хватает переменных');
104
+ (0, ui_1.ok)('Every function has the variables it needs');
113
105
  return;
114
106
  }
115
107
  (0, ui_1.out)('');
116
- (0, ui_1.fail)('Нет на платформе:');
108
+ (0, ui_1.fail)('Missing on the platform:');
117
109
  (0, ui_1.table)(missing);
118
- throw new errors_1.CliError(`Переменных не хватает: ${missing.length}`, 'Записать: xflow env set ИМЯ=значение. Пока их нет, функция получит undefined');
110
+ throw new errors_1.CliError(`Missing variables: ${missing.length}`, 'To store them: xflow env set NAME=value. Until then the function receives undefined');
119
111
  }
120
112
  async function envSet(args) {
121
113
  const { root, config } = (0, config_1.requireProject)();
122
114
  const client = (0, session_1.connect)(config);
123
115
  const pair = args.words[1];
124
116
  if (!pair || !pair.includes('=')) {
125
- throw new errors_1.CliError('Нужна пара ИМЯ=значение', 'Например: xflow env set SMTP_PASSWORD=секрет');
117
+ throw new errors_1.CliError('A NAME=value pair is required', 'For example: xflow env set SMTP_PASSWORD=secret');
126
118
  }
127
119
  const name = pair.slice(0, pair.indexOf('=')).trim();
128
120
  const value = pair.slice(pair.indexOf('=') + 1);
@@ -130,12 +122,11 @@ async function envSet(args) {
130
122
  method: 'POST',
131
123
  body: { name, value, scope: (0, args_1.flagString)(args, 'scope') === 'project' ? 'project' : 'organization' },
132
124
  });
133
- (0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} записана (${result.scope === 'project' ? 'только этот проект' : 'вся организация'})`);
134
- // Значение попадает в функцию на выкатке, а не в момент записи: пока функцию
135
- // не передеплоили, в её окружении лежит прежнее.
125
+ (0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} stored (${result.scope === 'project' ? 'this project only' : 'the whole organization'})`);
126
+ // Values reach a function on its next deploy.
136
127
  const users = referencedByFunctions(root).needed.get(name);
137
128
  if (users && users.length > 0) {
138
- (0, ui_1.note)((0, ui_1.dim)(` Чтобы значение доехало, перевыложите: xflow functions deploy ${users.join(' && xflow functions deploy ')}`));
129
+ (0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, redeploy: xflow functions deploy ${users.join(' && xflow functions deploy ')}`));
139
130
  }
140
131
  }
141
132
  async function envRemove(args) {
@@ -143,12 +134,12 @@ async function envRemove(args) {
143
134
  const client = (0, session_1.connect)(config);
144
135
  const name = args.words[1];
145
136
  if (!name)
146
- throw new errors_1.CliError('Нужно имя переменной', 'Что записано: xflow env');
137
+ throw new errors_1.CliError('A variable name is required', 'What is stored: xflow env');
147
138
  const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
148
139
  if (!result.removed) {
149
- (0, ui_1.note)(`Переменной ${name} нет`);
140
+ (0, ui_1.note)(`There is no variable ${name}`);
150
141
  return;
151
142
  }
152
- (0, ui_1.ok)(`${name} удалена`);
153
- (0, ui_1.note)((0, ui_1.dim)(' В уже выложенных функциях значение останется до следующей выкатки'));
143
+ (0, ui_1.ok)(`${name} deleted`);
144
+ (0, ui_1.note)((0, ui_1.dim)(' Functions already deployed keep the value until their next deploy'));
154
145
  }
@@ -13,7 +13,6 @@ const session_1 = require("../session");
13
13
  const template_1 = require("../template");
14
14
  const ui_1 = require("../ui");
15
15
  const ENTRY_NAMES = ['index.ts', 'index.js', 'index.mjs'];
16
- /** Папка с функциями внутри проекта. Та же, что была до пивота: менять её незачем. */
17
16
  const FUNCTIONS_DIR = 'functions';
18
17
  function entryFor(root, name) {
19
18
  for (const entry of ENTRY_NAMES) {
@@ -33,14 +32,8 @@ function discover(root) {
33
32
  .sort();
34
33
  }
35
34
  /**
36
- * Собрать функцию в один файл.
37
- *
38
- * esbuild берём из node_modules проекта, а не тащим зависимостью в CLI: он и так
39
- * есть в каждом приложении на платформе (внутри Vite), а пакет без зависимостей
40
- * ставится быстрее и не требует доверия к нашему списку.
41
- *
42
- * `pg` оставляем снаружи бандла сознательно: серверная обёртка подставляет схему
43
- * проекта в каждое соединение, а сделать это можно только с общим драйвером.
35
+ * Bundle a function into one file. esbuild comes from the project's node_modules;
36
+ * `pg` stays external so the server wrapper can inject the project schema.
44
37
  */
45
38
  function bundle(root, entry) {
46
39
  let esbuild;
@@ -48,7 +41,7 @@ function bundle(root, entry) {
48
41
  esbuild = require(require.resolve('esbuild', { paths: [root] }));
49
42
  }
50
43
  catch {
51
- throw new errors_1.CliError('Не нашёл esbuild в проекте', 'Установите его: npm i -D esbuild. Обычно он уже стоит вместе с Vite');
44
+ throw new errors_1.CliError('Could not find esbuild in the project', 'Install it: npm i -D esbuild. It usually comes with Vite already');
52
45
  }
53
46
  const result = esbuild.buildSync({
54
47
  entryPoints: [entry],
@@ -62,7 +55,7 @@ function bundle(root, entry) {
62
55
  });
63
56
  const text = result.outputFiles[0]?.text;
64
57
  if (!text)
65
- throw new errors_1.CliError(`Сборка ${entry} не дала результата`);
58
+ throw new errors_1.CliError(`Building ${entry} produced nothing`);
66
59
  return text;
67
60
  }
68
61
  async function functionsList() {
@@ -70,28 +63,20 @@ async function functionsList() {
70
63
  const client = (0, session_1.connect)(config);
71
64
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/functions`);
72
65
  if (data.functions.length === 0) {
73
- (0, ui_1.note)(`Функций нет. Положите код в ${FUNCTIONS_DIR}/<имя>/index.ts и выполните xflow functions deploy`);
66
+ (0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow functions deploy`);
74
67
  return;
75
68
  }
76
69
  (0, ui_1.table)(data.functions.map((fn) => [
77
70
  fn.name,
78
- fn.status === 'deployed' ? 'выложена' : fn.status === 'failed' ? 'ошибка' : fn.status,
79
- fn.last_deployed_at ? (0, ui_1.formatAge)(fn.last_deployed_at) : '',
71
+ fn.status === 'deployed' ? 'deployed' : fn.status === 'failed' ? 'failed' : fn.status,
72
+ fn.last_deployed_at ? (0, ui_1.formatAge)(fn.last_deployed_at) : '-',
80
73
  fn.error_message ?? '',
81
74
  ]));
82
75
  }
83
- /**
84
- * Записать адреса функций в `.env`, откуда их заберёт сборщик.
85
- *
86
- * Подставляем на сборке, а не спрашиваем у платформы в рантайме: иначе каждый
87
- * запуск приложения начинался бы с похода к нам, и мы стали бы обязательным
88
- * участником работы чужого приложения.
89
- */
76
+ /** Write function URLs into .env, where the build picks them up. */
90
77
  async function refreshFunctionsEnv(root, client, projectId) {
91
78
  const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
92
- // Файла может не быть вовсе: в исходники он не уходит, а клон делают из git.
93
- // Дописывать в такой один адрес функции бессмысленно — без токена вызов всё
94
- // равно получит 401, поэтому восстанавливаем целиком, как это делает link.
79
+ // No .env at all (fresh clone): recreate it fully, like link does.
95
80
  if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
96
81
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, '.env'), (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions), 'utf-8');
97
82
  }
@@ -100,7 +85,6 @@ async function refreshFunctionsEnv(root, client, projectId) {
100
85
  }
101
86
  return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
102
87
  }
103
- /** Ответ функции: JSON разворачиваем, прочее отдаём как есть. */
104
88
  function prettyBody(text) {
105
89
  try {
106
90
  return JSON.stringify(JSON.parse(text), null, 2);
@@ -109,24 +93,19 @@ function prettyBody(text) {
109
93
  return text;
110
94
  }
111
95
  }
112
- /**
113
- * Вызвать функцию так же, как её вызывает приложение.
114
- *
115
- * Токен проекта берём из карточки на платформе, а не из локального `.env`: в
116
- * свежем клоне файла нет вовсе, а команда должна работать сразу после link.
117
- */
96
+ /** Call a function the same way the application does. */
118
97
  async function functionsInvoke(args) {
119
98
  const { config } = (0, config_1.requireProject)();
120
99
  const client = (0, session_1.connect)(config);
121
100
  const name = args.words[1];
122
101
  if (!name)
123
- throw new errors_1.CliError('Нужно имя функции', 'Что выложено: xflow functions list');
102
+ throw new errors_1.CliError('A function name is required', 'What is deployed: xflow functions list');
124
103
  const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}`);
125
104
  const fn = card.functions.find((item) => item.name === name);
126
105
  if (!fn || !fn.invoke_url) {
127
- throw new errors_1.CliError(`Функция ${name} не выложена`, card.functions.length > 0
128
- ? `Выложены: ${card.functions.map((item) => item.name).join(', ')}`
129
- : `Выкатить: xflow functions deploy ${name}`);
106
+ throw new errors_1.CliError(`Function ${name} is not deployed`, card.functions.length > 0
107
+ ? `Deployed: ${card.functions.map((item) => item.name).join(', ')}`
108
+ : `To deploy it: xflow functions deploy ${name}`);
130
109
  }
131
110
  const data = (0, args_1.flagString)(args, 'data');
132
111
  const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
@@ -141,21 +120,20 @@ async function functionsInvoke(args) {
141
120
  'X-Project-Token': card.project_token ?? '',
142
121
  },
143
122
  body: sendsBody ? (data ?? '{}') : undefined,
144
- // У функции свой потолок в 30 секунд: ждём чуть дольше, чтобы увидеть её
145
- // собственный таймаут, а не свой.
146
- signal: AbortSignal.timeout(40_000),
123
+ // Wait past the function's own 90 s cap to see its timeout, not ours.
124
+ signal: AbortSignal.timeout(100_000),
147
125
  });
148
126
  }
149
127
  catch (e) {
150
- throw new errors_1.CliError(`Функция не ответила: ${e instanceof Error ? e.message : String(e)}`, 'Проверьте, что она выложена: xflow functions list');
128
+ throw new errors_1.CliError(`The function did not answer: ${e instanceof Error ? e.message : String(e)}`, 'Check that it is deployed: xflow functions list');
151
129
  }
152
130
  const elapsed = Date.now() - started;
153
131
  const text = await response.text();
154
- (0, ui_1.note)((0, ui_1.dim)(`${response.status} ${response.statusText} за ${elapsed} мс`));
132
+ (0, ui_1.note)((0, ui_1.dim)(`${response.status} ${response.statusText} in ${elapsed} ms`));
155
133
  if (text)
156
134
  (0, ui_1.out)(prettyBody(text));
157
135
  if (!response.ok) {
158
- throw new errors_1.CliError(`Функция ответила ${response.status}`, `Стек и вывод консоли: xflow functions logs ${name}`);
136
+ throw new errors_1.CliError(`The function answered ${response.status}`, `The stack and console output: xflow functions logs ${name}`);
159
137
  }
160
138
  }
161
139
  async function functionsDeploy(args) {
@@ -164,23 +142,23 @@ async function functionsDeploy(args) {
164
142
  const wanted = args.words[1];
165
143
  const names = wanted ? [wanted] : discover(root);
166
144
  if (names.length === 0) {
167
- throw new errors_1.CliError( проекте нет функций`, `Создайте ${FUNCTIONS_DIR}/<имя>/index.ts с экспортом handler и повторите`);
145
+ throw new errors_1.CliError(`The project has no functions`, `Create ${FUNCTIONS_DIR}/<name>/index.ts exporting handler and try again`);
168
146
  }
169
147
  for (const name of names) {
170
148
  const entry = entryFor(root, name);
171
149
  if (!entry) {
172
- throw new errors_1.CliError(`Не нашёл ${FUNCTIONS_DIR}/${name}/index.ts`, `Доступные функции: ${discover(root).join(', ') || 'нет ни одной'}`);
150
+ throw new errors_1.CliError(`Could not find ${FUNCTIONS_DIR}/${name}/index.ts`, `Available functions: ${discover(root).join(', ') || 'none at all'}`);
173
151
  }
174
- (0, ui_1.step)(`Собираю ${name}`);
152
+ (0, ui_1.step)(`Building ${name}`);
175
153
  const code = bundle(root, entry);
176
- (0, ui_1.step)(`Выкладываю ${name}`);
154
+ (0, ui_1.step)(`Deploying ${name}`);
177
155
  const result = await (0, api_1.apiUpload)(client, `/api/v1/projects/${config.projectId}/functions?name=${encodeURIComponent(name)}`, Buffer.from(code, 'utf-8'), { 'Content-Type': 'application/javascript' });
178
- (0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} выложена`);
156
+ (0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} deployed`);
179
157
  (0, ui_1.out)(result.url);
180
158
  if (result.secrets.length > 0) {
181
- (0, ui_1.note)((0, ui_1.dim)(` Секреты организации в окружении: ${result.secrets.join(', ')}`));
159
+ (0, ui_1.note)((0, ui_1.dim)(` Organization secrets in the environment: ${result.secrets.join(', ')}`));
182
160
  }
183
161
  }
184
162
  const available = await refreshFunctionsEnv(root, client, config.projectId);
185
- (0, ui_1.note)((0, ui_1.dim)(` Адреса в .env обновлены (${available.join(', ')}). Во фронтенде: xflow.functions.invoke('${names[0]}')`));
163
+ (0, ui_1.note)((0, ui_1.dim)(` Addresses in .env updated (${available.join(', ')}). In the frontend: xflow.functions.invoke('${names[0]}')`));
186
164
  }
@@ -8,7 +8,6 @@ const config_1 = require("../config");
8
8
  const session_1 = require("../session");
9
9
  const ui_1 = require("../ui");
10
10
  const DEFAULT_LIMIT = 10;
11
- /** Стек с хвостом консоли бывает длинным: показываем начало, оно и есть причина. */
12
11
  const STACK_MAX_LINES = 14;
13
12
  function stamp(iso) {
14
13
  const date = new Date(iso);
@@ -24,17 +23,17 @@ async function fetchLogs(source, name, args) {
24
23
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/logs?${query.toString()}`);
25
24
  return data.logs;
26
25
  }
27
- /** Свежее внизу: так последняя ошибка оказывается перед глазами, а не уезжает вверх. */
26
+ /** Newest at the bottom. */
28
27
  function render(rows) {
29
28
  for (const row of [...rows].reverse()) {
30
- const label = row.source === 'function' ? (row.function ?? 'функция') : 'браузер';
29
+ const label = row.source === 'function' ? (row.function ?? 'function') : 'browser';
31
30
  (0, ui_1.out)(`${(0, ui_1.dim)(stamp(row.timestamp))} ${(0, ui_1.bold)(label)} ${row.message}`);
32
31
  if (row.stack) {
33
32
  const lines = row.stack.split('\n');
34
33
  for (const line of lines.slice(0, STACK_MAX_LINES))
35
34
  (0, ui_1.out)((0, ui_1.dim)(` ${line}`));
36
35
  if (lines.length > STACK_MAX_LINES)
37
- (0, ui_1.out)((0, ui_1.dim)(` … ещё строк: ${lines.length - STACK_MAX_LINES}`));
36
+ (0, ui_1.out)((0, ui_1.dim)(` … ${lines.length - STACK_MAX_LINES} more lines`));
38
37
  }
39
38
  (0, ui_1.out)('');
40
39
  }
@@ -42,8 +41,8 @@ function render(rows) {
42
41
  async function logs(args) {
43
42
  const rows = await fetchLogs('client', undefined, args);
44
43
  if (rows.length === 0) {
45
- (0, ui_1.note)('Ошибок из браузера нет');
46
- (0, ui_1.note)((0, ui_1.dim)(' Сюда попадают падения выложенного приложения, а не локального npm run dev'));
44
+ (0, ui_1.note)('No browser errors');
45
+ (0, ui_1.note)((0, ui_1.dim)(' This collects crashes of the released application, not of a local npm run dev'));
47
46
  return;
48
47
  }
49
48
  render(rows);
@@ -52,8 +51,8 @@ async function functionsLogs(args) {
52
51
  const name = args.words[1];
53
52
  const rows = await fetchLogs('function', name, args);
54
53
  if (rows.length === 0) {
55
- (0, ui_1.note)(name ? `Функция ${name} не падала` : 'Функции не падали');
56
- (0, ui_1.note)((0, ui_1.dim)(' Сюда попадают только неудачные вызовы: успешные ничего не пишут'));
54
+ (0, ui_1.note)(name ? `Function ${name} has not crashed` : 'No function has crashed');
55
+ (0, ui_1.note)((0, ui_1.dim)(' Only failed calls land here: successful ones write nothing'));
57
56
  return;
58
57
  }
59
58
  render(rows);
@@ -9,7 +9,7 @@ const args_1 = require("../args");
9
9
  const config_1 = require("../config");
10
10
  const session_1 = require("../session");
11
11
  const ui_1 = require("../ui");
12
- /** Клиенты, у которых есть своя команда для добавления сервера. */
12
+ /** Clients with their own server-add command. */
13
13
  const CLIENTS = [
14
14
  {
15
15
  binary: 'claude',
@@ -24,26 +24,20 @@ const CLIENTS = [
24
24
  '--header',
25
25
  `Authorization: Bearer ${token}`,
26
26
  ],
27
- // Повторный запуск должен обновлять запись, а не спотыкаться о неё: команду
28
- // выполняют и после смены ключа, и после обновления пакета.
27
+ // Re-running must update the entry, not fail on it.
29
28
  reset: ['mcp', 'remove', 'xflow', '-s', 'local'],
30
29
  },
31
30
  ];
32
31
  const WINDOWS = process.platform === 'win32';
33
32
  /**
34
- * Запустить чужую программу.
35
- *
36
- * На Windows без оболочки не найти `.cmd`-обёртки, которыми ставятся все
37
- * консольные пакеты npm. А оболочка не экранирует аргументы, а склеивает их:
38
- * заголовок «Authorization: Bearer …» разваливается по пробелу. Поэтому на
39
- * Windows собираем строку сами и сами же расставляем кавычки.
33
+ * Windows needs a shell to resolve npm's .cmd shims, and the shell joins
34
+ * arguments instead of escaping them: quote by hand.
40
35
  */
41
36
  function quote(value) {
42
37
  return WINDOWS ? `"${value.replace(/"/g, '""')}"` : value;
43
38
  }
44
39
  function execute(binary, args) {
45
- // Вывод чужой команды перехватываем, а не пропускаем на экран: клиенты любят
46
- // напечатать добавленные заголовки целиком, вместе с ключом.
40
+ // Capture output: clients may echo the added header together with the key.
47
41
  const options = { encoding: 'utf-8' };
48
42
  return WINDOWS
49
43
  ? (0, node_child_process_1.spawnSync)([binary, ...args.map(quote)].join(' '), { ...options, shell: true })
@@ -53,47 +47,46 @@ function hasBinary(binary) {
53
47
  return execute(binary, ['--version']).status === 0;
54
48
  }
55
49
  async function mcpInstall(args) {
56
- // Сервер общий для организации, поэтому команда работает и вне папки проекта.
50
+ // The server covers the organization: the command works outside a project folder too.
57
51
  const root = (0, config_1.findProjectRoot)();
58
52
  const client = (0, session_1.connect)(root ? (0, config_1.readConfig)(root) : undefined);
59
53
  const url = `${client.apiUrl.replace(/\/+$/, '')}/api/mcp`;
60
54
  const identity = await (0, api_1.apiJson)(client, '/api/v1/me');
61
- (0, ui_1.out)(`${(0, ui_1.bold)('Сервер MCP')} ${url}`);
62
- (0, ui_1.note)((0, ui_1.dim)(` Организация: ${identity.organization.name ?? 'без названия'}`));
63
- (0, ui_1.note)((0, ui_1.dim)(' Ключ на всю организацию: проект агент называет сам'));
55
+ (0, ui_1.out)(`${(0, ui_1.bold)('MCP server')} ${url}`);
56
+ (0, ui_1.note)((0, ui_1.dim)(` Organization: ${identity.organization.name ?? 'unnamed'}`));
57
+ (0, ui_1.note)((0, ui_1.dim)(' The key covers the whole organization: the agent names the project itself'));
64
58
  let found = 0;
65
59
  let installed = 0;
66
60
  for (const target of CLIENTS) {
67
61
  if (!hasBinary(target.binary))
68
62
  continue;
69
63
  found++;
70
- (0, ui_1.step)(`Прописываю в ${target.label}`);
64
+ (0, ui_1.step)(`Writing into ${target.label}`);
71
65
  execute(target.binary, [...target.reset]);
72
66
  const result = execute(target.binary, target.args(url, client.token));
73
67
  if (result.status === 0) {
74
68
  installed++;
75
- (0, ui_1.ok)(`${target.label}: сервер xflow подключён`);
69
+ (0, ui_1.ok)(`${target.label}: the xflow server is connected`);
76
70
  }
77
71
  else {
78
72
  const reason = `${result.stderr ?? ''}${result.stdout ?? ''}`.trim();
79
- (0, ui_1.note)(`${target.label}: команда завершилась с ошибкой`);
73
+ (0, ui_1.note)(`${target.label}: the command exited with an error`);
80
74
  if (reason)
81
- (0, ui_1.out)((0, ui_1.dim)(` ${reason.split(client.token).join('<ключ>').split('\n').slice(0, 5).join('\n ')}`));
75
+ (0, ui_1.out)((0, ui_1.dim)(` ${reason.split(client.token).join('<key>').split('\n').slice(0, 5).join('\n ')}`));
82
76
  }
83
77
  }
84
78
  if (installed === 0) {
85
- (0, ui_1.note)(found === 0 ? 'Не нашёл клиентов со своей командой подключения.' : 'Добавьте сервер вручную:');
86
- (0, ui_1.out)(` тип: HTTP (streamable)`);
87
- (0, ui_1.out)(` адрес: ${url}`);
88
- (0, ui_1.out)(` заголовок: Authorization: Bearer <ключ>`);
79
+ (0, ui_1.note)(found === 0 ? 'Found no client with its own connect command.' : 'Add the server by hand:');
80
+ (0, ui_1.out)(` type: HTTP (streamable)`);
81
+ (0, ui_1.out)(` address: ${url}`);
82
+ (0, ui_1.out)(` header: Authorization: Bearer <key>`);
89
83
  }
90
- // Ключ печатаем только по явной просьбе: см. комментарий в шапке файла.
91
84
  if ((0, args_1.flagBool)(args, 'show-token')) {
92
85
  (0, ui_1.out)('');
93
- (0, ui_1.out)(`${(0, ui_1.bold)('Ключ')} ${client.token}`);
94
- (0, ui_1.note)((0, ui_1.dim)(' Не оставляйте его в переписке с агентом'));
86
+ (0, ui_1.out)(`${(0, ui_1.bold)('Key')} ${client.token}`);
87
+ (0, ui_1.note)((0, ui_1.dim)(' Do not leave it in the chat with the agent'));
95
88
  }
96
89
  else {
97
- (0, ui_1.note)((0, ui_1.dim)(` Ключ лежит в ${(0, node_path_1.join)((0, node_os_1.homedir)(), '.xflow', 'credentials.json')} и на экран не выводится`));
90
+ (0, ui_1.note)((0, ui_1.dim)(` The key lives in ${(0, node_path_1.join)((0, node_os_1.homedir)(), '.xflow', 'credentials.json')} and is not printed`));
98
91
  }
99
92
  }