@getxflow/cli 0.1.9 → 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.
package/README.md CHANGED
@@ -1,45 +1,45 @@
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.
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
@@ -10,9 +10,9 @@ class ApiError extends Error {
10
10
  code;
11
11
  status;
12
12
  hint;
13
- /** Разбор отказа, если он есть: например, нарушения, из-за которых не пошла сборка. */
13
+ /** Check violations, e.g. why a build was rejected. */
14
14
  issues;
15
- /** Заполнен, когда упёрлись в тариф: команда верна, повторять её бесполезно. */
15
+ /** Set when a plan limit was hit. */
16
16
  limit;
17
17
  constructor(message, code, status, hint, issues, limit) {
18
18
  super(message);
@@ -26,7 +26,7 @@ class ApiError extends Error {
26
26
  }
27
27
  exports.ApiError = ApiError;
28
28
  const DEFAULT_TIMEOUT_MS = 30_000;
29
- /** Заливка и скачивание архивов: десятки мегабайт по обычному домашнему каналу. */
29
+ /** Archive uploads and downloads. */
30
30
  const TRANSFER_TIMEOUT_MS = 5 * 60_000;
31
31
  function parseVersion(value) {
32
32
  return value
@@ -58,13 +58,11 @@ async function send(client, path, options = {}) {
58
58
  'X-Xflow-Cli': version_1.CLI_VERSION,
59
59
  ...headers,
60
60
  };
61
- // Вход по device flow идёт без ключа: пустой Bearer выглядел бы как испорченный.
62
61
  if (client.token)
63
62
  requestHeaders.Authorization = `Bearer ${client.token}`;
64
63
  let payload;
65
64
  if (Buffer.isBuffer(body)) {
66
- // Именно ArrayBuffer: Buffer живёт в общем пуле Node, и его срез это чужие
67
- // байты вокруг наших. Плюс тип понятен и DOM-, и node-типизации fetch.
65
+ // Copy out of Node's shared Buffer pool before handing the bytes to fetch.
68
66
  payload = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
69
67
  requestHeaders['Content-Type'] = requestHeaders['Content-Type'] ?? 'application/zip';
70
68
  }
@@ -104,18 +102,16 @@ async function apiJson(client, path, options = {}) {
104
102
  throw await readError(response);
105
103
  return (await response.json());
106
104
  }
107
- /** Ответ-файл: архив шаблона отдаётся телом, а не ссылкой (он лежит в базе, а не в S3). */
108
105
  async function apiBinary(client, path) {
109
106
  const response = await send(client, path, { timeoutMs: TRANSFER_TIMEOUT_MS });
110
107
  if (!response.ok)
111
108
  throw await readError(response);
112
109
  return Buffer.from(await response.arrayBuffer());
113
110
  }
114
- /** Загрузка архива на сервер: тот же разбор ошибок, но с длинным таймаутом. */
115
111
  async function apiUpload(client, path, body, headers = {}) {
116
112
  return apiJson(client, path, { method: 'POST', body, headers, timeoutMs: TRANSFER_TIMEOUT_MS });
117
113
  }
118
- /** Скачивание по подписанной ссылке. Ключ сюда не подставляем: ссылка сама и есть пропуск. */
114
+ /** Signed URL download: no Authorization header attached. */
119
115
  async function downloadUrl(url) {
120
116
  let response;
121
117
  try {
package/dist/args.js CHANGED
@@ -1,19 +1,11 @@
1
1
  "use strict";
2
- /**
3
- * Разбор аргументов.
4
- *
5
- * Своими руками, без зависимостей: пакет с ключом доступа внутри не должен
6
- * тянуть за собой чужой код ради двадцати строк логики.
7
- */
2
+ /** Argument parsing, dependency-free. */
8
3
  Object.defineProperty(exports, "__esModule", { value: true });
9
4
  exports.parseArgs = parseArgs;
10
5
  exports.flagString = flagString;
11
6
  exports.flagBool = flagBool;
12
7
  exports.flagNumber = flagNumber;
13
- /**
14
- * Флаги без значения. Список явный, потому что иначе `--force ./dir` съедает
15
- * путь как значение флага, и разработчик получает загадочную ошибку.
16
- */
8
+ /** Flags that never take a value, so `--force ./dir` keeps the path as a word. */
17
9
  const BOOLEAN_FLAGS = new Set([
18
10
  'force',
19
11
  'yes',
package/dist/bin.js CHANGED
@@ -21,7 +21,6 @@ const schedules_1 = require("./commands/schedules");
21
21
  const skills_1 = require("./commands/skills");
22
22
  const sources_1 = require("./commands/sources");
23
23
  const deploy_1 = require("./commands/deploy");
24
- /** Одно место, где команда превращается в действие. Слова, а не флаги: `projects list`. */
25
24
  async function run(args) {
26
25
  const [first, second] = args.words;
27
26
  const rest = { ...args, words: args.words.slice(1) };
@@ -174,7 +173,6 @@ async function main() {
174
173
  catch (e) {
175
174
  if (e instanceof api_1.ApiError) {
176
175
  (0, ui_1.fail)(e.message);
177
- // Отказ по тарифу помечаем явно: иначе агент правит команду, которая верна.
178
176
  if (e.limit)
179
177
  (0, ui_1.note)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
180
178
  if (e.hint)
@@ -189,7 +187,6 @@ async function main() {
189
187
  return 1;
190
188
  }
191
189
  (0, ui_1.fail)(e instanceof Error ? e.message : String(e));
192
- // Стек прячем: он полезен только нам, и то по запросу.
193
190
  if (process.env.XFLOW_DEBUG && e instanceof Error && e.stack)
194
191
  (0, ui_1.note)((0, ui_1.dim)(e.stack));
195
192
  return 1;
@@ -23,17 +23,11 @@ function localConfig() {
23
23
  }
24
24
  }
25
25
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
26
- /**
27
- * Вход человека: device flow.
28
- *
29
- * Ключ не проходит ни через копипаст, ни через контекст агента — он выпускается
30
- * на сервере после подтверждения в браузере и приезжает сюда ровно один раз.
31
- */
26
+ /** Device flow: the key is issued after confirmation in the browser, never typed. */
32
27
  async function login() {
33
28
  const config = localConfig();
34
29
  const client = (0, session_1.anonymous)(config);
35
- // Имя машины уходит на сервер и становится названием ключа: иначе в списке
36
- // доступов лежат одинаковые строки и непонятно, какой компьютер отзывать.
30
+ // The machine name becomes the key label on the platform.
37
31
  const start = await (0, api_1.apiJson)(client, '/api/v1/auth/device', {
38
32
  method: 'POST',
39
33
  body: { client_name: (0, node_os_1.hostname)() },
@@ -43,9 +37,19 @@ async function login() {
43
37
  (0, ui_1.note)(` Computer: ${(0, node_os_1.hostname)()}`);
44
38
  (0, ui_1.note)(` Sign-in link: ${start.verification_uri_complete}`);
45
39
  (0, ui_1.note)('');
46
- // Браузер не открываем сами: команду часто запускает агент, и всплывшее окно
47
- // забирает у человека внимание, не объяснив, что он подтверждает.
48
40
  (0, ui_1.step)('Open the link and check the code on the page against the one above');
41
+ const stopOffer = (0, ui_1.offerBrowser)(start.verification_uri_complete);
42
+ if (process.stdin.isTTY)
43
+ (0, ui_1.note)((0, ui_1.dim)(' Or press ENTER to open it in the browser'));
44
+ try {
45
+ await awaitConfirmation(client, start);
46
+ }
47
+ finally {
48
+ stopOffer();
49
+ }
50
+ }
51
+ /** Poll until the code is confirmed or expires. */
52
+ async function awaitConfirmation(client, start) {
49
53
  const deadline = Date.now() + start.expires_in * 1000;
50
54
  let intervalMs = Math.max(start.interval, 1) * 1000;
51
55
  for (;;) {
@@ -61,7 +65,7 @@ async function login() {
61
65
  });
62
66
  }
63
67
  catch (e) {
64
- // Слишком частый опрос не повод прерывать вход, замедляемся.
68
+ // Slow down on rate limits instead of failing the sign-in.
65
69
  if (e instanceof api_1.ApiError && e.code === 'rate_limited') {
66
70
  intervalMs = Math.min(intervalMs * 2, 30_000);
67
71
  continue;
@@ -88,7 +92,7 @@ function logout() {
88
92
  (0, ui_1.warn)(`There is no stored key for ${apiUrl}`);
89
93
  }
90
94
  }
91
- /** Приписка к тарифу. У живой подписки её нет: молчание и значит «всё в порядке». */
95
+ /** Suffix for a non-active subscription. */
92
96
  const SUBSCRIPTION_STATE = {
93
97
  past_due: ', the period has ended',
94
98
  suspended: ', work is suspended',
@@ -101,7 +105,8 @@ async function whoami() {
101
105
  (0, ui_1.out)(`Organization: ${me.organization.name ?? me.organization.id}`);
102
106
  (0, ui_1.out)(`Key: xfk_${me.key.prefix}… (${me.key.scopes.join(', ')})`);
103
107
  (0, ui_1.out)(`Platform: ${client.apiUrl}`);
104
- if (process.env.XFLOW_TOKEN) {
108
+ // Same condition as in connect: for an address out of xflow.json the env key is ignored.
109
+ if (process.env.XFLOW_TOKEN?.trim() && client.apiUrl === (0, config_1.apiUrlFor)()) {
105
110
  (0, ui_1.note)((0, ui_1.dim)(' The key comes from XFLOW_TOKEN, the one stored in ~/.xflow is not used'));
106
111
  }
107
112
  else if (!(0, credentials_1.readCredential)(client.apiUrl)) {
@@ -11,18 +11,12 @@ const config_1 = require("../config");
11
11
  const errors_1 = require("../errors");
12
12
  const session_1 = require("../session");
13
13
  const ui_1 = require("../ui");
14
- /**
15
- * Миграции базы проекта.
16
- *
17
- * Файлы лежат в репозитории (`migrations/0001_init.sql`), историю применённых
18
- * помнит сама база. Отсюда правило: локально мы только читаем и сортируем, а что
19
- * из этого новое, решает сервер.
20
- */
14
+ /** Migration files live in the repo; the applied history lives in the database. */
21
15
  const MIGRATIONS_DIR = 'migrations';
22
16
  function checksum(sql) {
23
17
  return (0, node_crypto_1.createHash)('sha256').update(sql).digest('hex').slice(0, 16);
24
18
  }
25
- /** Порядок по имени файла: `0001_` идёт раньше `0002_`, это и есть очередь. */
19
+ /** File-name order is the queue. */
26
20
  function readMigrations(root) {
27
21
  const dir = (0, node_path_1.join)(root, MIGRATIONS_DIR);
28
22
  if (!(0, node_fs_1.existsSync)(dir))
@@ -68,8 +62,7 @@ async function dbStatus() {
68
62
  (0, ui_1.formatAge)(row.applied_at),
69
63
  ]);
70
64
  }
71
- // Применённое, чего нет локально: чаще всего чужая миграция из другого проекта
72
- // на той же логической базе.
65
+ // Applied but missing locally: usually another project on the same logical database.
73
66
  const localNames = new Set(local.map((migration) => migration.name));
74
67
  for (const row of history.applied) {
75
68
  if (!localNames.has(row.name))
@@ -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 = '';
@@ -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();
@@ -73,12 +73,7 @@ async function envList() {
73
73
  (0, ui_1.table)(rows.map((row) => [row.name, row.scope === 'project' ? 'this project only' : 'the whole organization']));
74
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);
@@ -86,9 +81,6 @@ async function envCheck() {
86
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
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(', ')]));
@@ -131,8 +123,7 @@ async function envSet(args) {
131
123
  body: { name, value, scope: (0, args_1.flagString)(args, 'scope') === 'project' ? 'project' : 'organization' },
132
124
  });
133
125
  (0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} stored (${result.scope === 'project' ? 'this project only' : 'the whole organization'})`);
134
- // Значение попадает в функцию на выкатке, а не в момент записи: пока функцию
135
- // не передеплоили, в её окружении лежит прежнее.
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
129
  (0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, redeploy: xflow functions deploy ${users.join(' && xflow functions deploy ')}`));
@@ -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;
@@ -80,18 +73,10 @@ async function functionsList() {
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,12 +93,7 @@ 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);
@@ -141,8 +120,7 @@ async function functionsInvoke(args) {
141
120
  'X-Project-Token': card.project_token ?? '',
142
121
  },
143
122
  body: sendsBody ? (data ?? '{}') : undefined,
144
- // У функции свой потолок в 90 секунд: ждём чуть дольше, чтобы увидеть её
145
- // собственный таймаут, а не свой.
123
+ // Wait past the function's own 90 s cap to see its timeout, not ours.
146
124
  signal: AbortSignal.timeout(100_000),
147
125
  });
148
126
  }
@@ -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,7 +23,7 @@ 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
29
  const label = row.source === 'function' ? (row.function ?? 'function') : 'browser';
@@ -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,7 +47,7 @@ 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`;
@@ -87,7 +81,6 @@ async function mcpInstall(args) {
87
81
  (0, ui_1.out)(` address: ${url}`);
88
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
86
  (0, ui_1.out)(`${(0, ui_1.bold)('Key')} ${client.token}`);
@@ -16,7 +16,7 @@ const skills_1 = require("./skills");
16
16
  const template_1 = require("../template");
17
17
  const zip_1 = require("../zip");
18
18
  const ui_1 = require("../ui");
19
- /** Пустой ли каталог. `.git` не считается: связывать свежий клон обычное дело. */
19
+ /** .git alone does not count: linking a fresh clone is normal. */
20
20
  function isEmptyEnough(dir) {
21
21
  if (!(0, node_fs_1.existsSync)(dir))
22
22
  return true;
@@ -30,7 +30,7 @@ function write(root, path, content) {
30
30
  function escapeHtml(value) {
31
31
  return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
32
32
  }
33
- /** Заголовок вкладки в шаблоне общий на всех: у проекта он должен быть свой. */
33
+ /** Give the project its own tab title. */
34
34
  function titleFromProject(path, content, name) {
35
35
  if (path !== 'index.html')
36
36
  return content;
@@ -46,17 +46,7 @@ async function templates() {
46
46
  }
47
47
  (0, ui_1.table)(list.map((t) => [t.id, t.name, t.is_default ? 'default' : '', t.description ?? '']));
48
48
  }
49
- /**
50
- * Создать проект: шаблон платформы, локальная папка, проект на платформе.
51
- *
52
- * Шаблон приезжает с платформы, а не лежит в этом пакете. Иначе появились бы две
53
- * стартовые точки — веб и CLI, — и приложения из них выглядели бы по-разному, хотя
54
- * платформа у них одна.
55
- *
56
- * Отсюда и порядок: проект заводится первым, потому что без него неоткуда взять
57
- * токен для `.env`. Если запись файлов сорвётся, проект останется пустым, и CLI
58
- * скажет, как его подобрать (`xflow link`), — молча оставлять сироту нельзя.
59
- */
49
+ /** Create a project from the platform template. */
60
50
  async function init(args) {
61
51
  const target = (0, node_path_1.resolve)(args.words[0] ?? '.');
62
52
  const name = (0, args_1.flagString)(args, 'name') ?? (0, node_path_1.basename)(target);
@@ -107,7 +97,6 @@ async function init(args) {
107
97
  (0, ui_1.out)(' npm run dev # develop');
108
98
  (0, ui_1.out)(' xflow deploy # send the code, build and release');
109
99
  }
110
- /** Связать текущую папку с уже существующим проектом. */
111
100
  async function link(args) {
112
101
  const root = (0, config_1.findProjectRoot)() ?? process.cwd();
113
102
  const existing = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, config_1.CONFIG_FILE)) ? (0, config_1.readConfig)(root) : undefined;
@@ -124,8 +113,7 @@ async function link(args) {
124
113
  build: existing?.build ?? { command: 'npm run build', dir: 'dist' },
125
114
  });
126
115
  (0, config_1.ignoreStateInGit)(dir);
127
- // В исходники .env не уходит, поэтому в свежем клоне его нет вовсе, и приложение
128
- // молча теряет доступ к облачным функциям. Восстанавливаем, но чужой не трогаем.
116
+ // A fresh clone has no .env: recreate it, never overwrite an existing one.
129
117
  if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, '.env')) && card.project_token) {
130
118
  write(dir, '.env', (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions));
131
119
  (0, ui_1.note)((0, ui_1.dim)(' Created .env with the project token and the function addresses'));