@getxflow/cli 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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` | cloud functions of the project from `functions/<name>/index.ts`, shipped by `deploy` |
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) };
@@ -70,9 +69,9 @@ async function run(args) {
70
69
  }
71
70
  throw new errors_1.CliError(`Unknown command: mcp ${second}`, 'Available: install');
72
71
  case 'functions':
72
+ // The command is gone, but silence would leave the habit unexplained.
73
73
  if (second === 'deploy') {
74
- await (0, functions_1.functionsDeploy)(rest);
75
- return;
74
+ throw new errors_1.CliError('Functions are deployed by the build now', 'Run xflow deploy: it ships the functions and then builds the application with their addresses');
76
75
  }
77
76
  if (second === 'invoke') {
78
77
  await (0, functions_1.functionsInvoke)(rest);
@@ -86,7 +85,7 @@ async function run(args) {
86
85
  await (0, functions_1.functionsList)();
87
86
  return;
88
87
  }
89
- throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list, deploy, invoke and logs');
88
+ throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list, invoke and logs');
90
89
  case 'logs':
91
90
  await (0, logs_1.logs)(rest);
92
91
  return;
@@ -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))
@@ -4,26 +4,20 @@ exports.deploy = deploy;
4
4
  exports.publish = publish;
5
5
  exports.rollback = rollback;
6
6
  exports.deployments = deployments;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
7
9
  const api_1 = require("../api");
8
10
  const args_1 = require("../args");
9
11
  const config_1 = require("../config");
10
12
  const errors_1 = require("../errors");
11
13
  const session_1 = require("../session");
14
+ const template_1 = require("../template");
12
15
  const ui_1 = require("../ui");
13
16
  const sources_1 = require("./sources");
14
17
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
- /**
16
- * Потолок ожидания сборки в терминале. Чуть больше, чем срок, после которого
17
- * платформа закрывает молчащую сборку: так разработчик увидит внятный отказ, а
18
- * не «идёт дольше ожидаемого» на сборке, которую уже похоронили.
19
- */
18
+ /** Slightly longer than the platform's own build timeout. */
20
19
  const WAIT_LIMIT_MS = 16 * 60_000;
21
- /**
22
- * Показать, чем проект не подошёл платформе.
23
- *
24
- * Печатаем весь список: правила известны заранее (скилл xflow), а узнавать их по
25
- * одному через повторные запуски значит терять круг на каждое нарушение.
26
- */
20
+ /** Print every check violation at once. */
27
21
  function reportIssues(e) {
28
22
  const issues = e.issues ?? [];
29
23
  for (const issue of issues) {
@@ -33,13 +27,7 @@ function reportIssues(e) {
33
27
  }
34
28
  throw new errors_1.CliError(e.message, e.hint);
35
29
  }
36
- /**
37
- * Дождаться конца сборки, печатая фазы.
38
- *
39
- * Платформа отвечает на запуск сразу и не держит соединение: сборка живёт в
40
- * песочнице и переживает перезапуск платформы, поэтому её состояние забирается
41
- * опросом, а не потоком.
42
- */
30
+ /** Poll the build until it finishes, printing phases. */
43
31
  async function waitForBuild(client, projectId, started) {
44
32
  const deadline = Date.now() + WAIT_LIMIT_MS;
45
33
  let shown = '';
@@ -55,6 +43,24 @@ async function waitForBuild(client, projectId, started) {
55
43
  }
56
44
  throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Version ${started.deploy_id}`);
57
45
  }
46
+ /**
47
+ * Write the function addresses into the local .env.
48
+ *
49
+ * The build bakes its own copy of the map into the bundle, this one is for
50
+ * `npm run dev`: without it the local run calls a function that it has no
51
+ * address for.
52
+ */
53
+ async function refreshFunctionsEnv(root, client, projectId) {
54
+ const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
55
+ // No .env at all (fresh clone): recreate it fully, like link does.
56
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
57
+ (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');
58
+ }
59
+ else {
60
+ (0, template_1.writeEnvValue)(root, template_1.FUNCTIONS_ENV_KEY, (0, template_1.functionsEnvValue)(card.functions));
61
+ }
62
+ return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
63
+ }
58
64
  async function deploy(args) {
59
65
  const { root, config } = (0, config_1.requireProject)();
60
66
  const client = (0, session_1.connect)(config);
@@ -83,6 +89,11 @@ async function deploy(args) {
83
89
  reportIssues(e);
84
90
  throw e;
85
91
  }
92
+ // Removing a function cannot be undone: a new one gets a new address. Say it
93
+ // out loud rather than leaving it to be discovered in functions list.
94
+ if (started.removed_functions?.length) {
95
+ (0, ui_1.warn)(`Removed from the cloud, gone from the sources: ${started.removed_functions.join(', ')}`);
96
+ }
86
97
  const build = await waitForBuild(client, config.projectId, started);
87
98
  if (build.phase === 'failed') {
88
99
  if (build.log_tail) {
@@ -93,6 +104,17 @@ async function deploy(args) {
93
104
  }
94
105
  (0, ui_1.ok)(`Version ${build.deploy_id} built from revision ${build.revision} in ${build.elapsed_s} s`);
95
106
  (0, ui_1.out)(build.project_url);
107
+ // The functions were shipped by the build itself, so their addresses are known
108
+ // only now. A failure here does not undo a finished build: warn and stop there.
109
+ try {
110
+ const functions = await refreshFunctionsEnv(root, client, config.projectId);
111
+ if (functions.length > 0) {
112
+ (0, ui_1.note)((0, ui_1.dim)(` Functions in .env: ${functions.join(', ')}`));
113
+ }
114
+ }
115
+ catch {
116
+ (0, ui_1.warn)('Could not refresh the function addresses in .env, the build itself is fine');
117
+ }
96
118
  (0, ui_1.note)((0, ui_1.dim)(' Show it to visitors: xflow publish'));
97
119
  }
98
120
  async function publish() {
@@ -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,11 +123,10 @@ 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
- (0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, redeploy: xflow functions deploy ${users.join(' && xflow functions deploy ')}`));
129
+ (0, ui_1.note)((0, ui_1.dim)(` For the value to arrive, run xflow deploy: it ships ${users.join(', ')} again`));
139
130
  }
140
131
  }
141
132
  async function envRemove(args) {
@@ -2,75 +2,19 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.functionsList = functionsList;
4
4
  exports.functionsInvoke = functionsInvoke;
5
- exports.functionsDeploy = functionsDeploy;
6
- const node_fs_1 = require("node:fs");
7
- const node_path_1 = require("node:path");
8
5
  const api_1 = require("../api");
9
6
  const args_1 = require("../args");
10
7
  const config_1 = require("../config");
11
8
  const errors_1 = require("../errors");
12
9
  const session_1 = require("../session");
13
- const template_1 = require("../template");
14
10
  const ui_1 = require("../ui");
15
- const ENTRY_NAMES = ['index.ts', 'index.js', 'index.mjs'];
16
- /** Папка с функциями внутри проекта. Та же, что была до пивота: менять её незачем. */
17
11
  const FUNCTIONS_DIR = 'functions';
18
- function entryFor(root, name) {
19
- for (const entry of ENTRY_NAMES) {
20
- const candidate = (0, node_path_1.join)(root, FUNCTIONS_DIR, name, entry);
21
- if ((0, node_fs_1.existsSync)(candidate))
22
- return candidate;
23
- }
24
- return null;
25
- }
26
- function discover(root) {
27
- const dir = (0, node_path_1.join)(root, FUNCTIONS_DIR);
28
- if (!(0, node_fs_1.existsSync)(dir))
29
- return [];
30
- return (0, node_fs_1.readdirSync)(dir)
31
- .filter((name) => (0, node_fs_1.statSync)((0, node_path_1.join)(dir, name)).isDirectory())
32
- .filter((name) => entryFor(root, name) !== null)
33
- .sort();
34
- }
35
- /**
36
- * Собрать функцию в один файл.
37
- *
38
- * esbuild берём из node_modules проекта, а не тащим зависимостью в CLI: он и так
39
- * есть в каждом приложении на платформе (внутри Vite), а пакет без зависимостей
40
- * ставится быстрее и не требует доверия к нашему списку.
41
- *
42
- * `pg` оставляем снаружи бандла сознательно: серверная обёртка подставляет схему
43
- * проекта в каждое соединение, а сделать это можно только с общим драйвером.
44
- */
45
- function bundle(root, entry) {
46
- let esbuild;
47
- try {
48
- esbuild = require(require.resolve('esbuild', { paths: [root] }));
49
- }
50
- catch {
51
- 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
- }
53
- const result = esbuild.buildSync({
54
- entryPoints: [entry],
55
- bundle: true,
56
- platform: 'node',
57
- target: 'node20',
58
- format: 'cjs',
59
- external: ['pg'],
60
- write: false,
61
- logLevel: 'silent',
62
- });
63
- const text = result.outputFiles[0]?.text;
64
- if (!text)
65
- throw new errors_1.CliError(`Building ${entry} produced nothing`);
66
- return text;
67
- }
68
12
  async function functionsList() {
69
13
  const { config } = (0, config_1.requireProject)();
70
14
  const client = (0, session_1.connect)(config);
71
15
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/functions`);
72
16
  if (data.functions.length === 0) {
73
- (0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow functions deploy`);
17
+ (0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow deploy`);
74
18
  return;
75
19
  }
76
20
  (0, ui_1.table)(data.functions.map((fn) => [
@@ -80,27 +24,6 @@ async function functionsList() {
80
24
  fn.error_message ?? '',
81
25
  ]));
82
26
  }
83
- /**
84
- * Записать адреса функций в `.env`, откуда их заберёт сборщик.
85
- *
86
- * Подставляем на сборке, а не спрашиваем у платформы в рантайме: иначе каждый
87
- * запуск приложения начинался бы с похода к нам, и мы стали бы обязательным
88
- * участником работы чужого приложения.
89
- */
90
- async function refreshFunctionsEnv(root, client, projectId) {
91
- const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
92
- // Файла может не быть вовсе: в исходники он не уходит, а клон делают из git.
93
- // Дописывать в такой один адрес функции бессмысленно — без токена вызов всё
94
- // равно получит 401, поэтому восстанавливаем целиком, как это делает link.
95
- if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, '.env')) && card.project_token) {
96
- (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
- }
98
- else {
99
- (0, template_1.writeEnvValue)(root, template_1.FUNCTIONS_ENV_KEY, (0, template_1.functionsEnvValue)(card.functions));
100
- }
101
- return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
102
- }
103
- /** Ответ функции: JSON разворачиваем, прочее отдаём как есть. */
104
27
  function prettyBody(text) {
105
28
  try {
106
29
  return JSON.stringify(JSON.parse(text), null, 2);
@@ -109,12 +32,7 @@ function prettyBody(text) {
109
32
  return text;
110
33
  }
111
34
  }
112
- /**
113
- * Вызвать функцию так же, как её вызывает приложение.
114
- *
115
- * Токен проекта берём из карточки на платформе, а не из локального `.env`: в
116
- * свежем клоне файла нет вовсе, а команда должна работать сразу после link.
117
- */
35
+ /** Call a function the same way the application does. */
118
36
  async function functionsInvoke(args) {
119
37
  const { config } = (0, config_1.requireProject)();
120
38
  const client = (0, session_1.connect)(config);
@@ -126,7 +44,7 @@ async function functionsInvoke(args) {
126
44
  if (!fn || !fn.invoke_url) {
127
45
  throw new errors_1.CliError(`Function ${name} is not deployed`, card.functions.length > 0
128
46
  ? `Deployed: ${card.functions.map((item) => item.name).join(', ')}`
129
- : `To deploy it: xflow functions deploy ${name}`);
47
+ : 'Functions ship with the build: xflow deploy');
130
48
  }
131
49
  const data = (0, args_1.flagString)(args, 'data');
132
50
  const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
@@ -141,8 +59,7 @@ async function functionsInvoke(args) {
141
59
  'X-Project-Token': card.project_token ?? '',
142
60
  },
143
61
  body: sendsBody ? (data ?? '{}') : undefined,
144
- // У функции свой потолок в 90 секунд: ждём чуть дольше, чтобы увидеть её
145
- // собственный таймаут, а не свой.
62
+ // Wait past the function's own 90 s cap to see its timeout, not ours.
146
63
  signal: AbortSignal.timeout(100_000),
147
64
  });
148
65
  }
@@ -158,29 +75,3 @@ async function functionsInvoke(args) {
158
75
  throw new errors_1.CliError(`The function answered ${response.status}`, `The stack and console output: xflow functions logs ${name}`);
159
76
  }
160
77
  }
161
- async function functionsDeploy(args) {
162
- const { root, config } = (0, config_1.requireProject)();
163
- const client = (0, session_1.connect)(config);
164
- const wanted = args.words[1];
165
- const names = wanted ? [wanted] : discover(root);
166
- if (names.length === 0) {
167
- throw new errors_1.CliError(`The project has no functions`, `Create ${FUNCTIONS_DIR}/<name>/index.ts exporting handler and try again`);
168
- }
169
- for (const name of names) {
170
- const entry = entryFor(root, name);
171
- if (!entry) {
172
- throw new errors_1.CliError(`Could not find ${FUNCTIONS_DIR}/${name}/index.ts`, `Available functions: ${discover(root).join(', ') || 'none at all'}`);
173
- }
174
- (0, ui_1.step)(`Building ${name}`);
175
- const code = bundle(root, entry);
176
- (0, ui_1.step)(`Deploying ${name}`);
177
- 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)} deployed`);
179
- (0, ui_1.out)(result.url);
180
- if (result.secrets.length > 0) {
181
- (0, ui_1.note)((0, ui_1.dim)(` Organization secrets in the environment: ${result.secrets.join(', ')}`));
182
- }
183
- }
184
- const available = await refreshFunctionsEnv(root, client, config.projectId);
185
- (0, ui_1.note)((0, ui_1.dim)(` Addresses in .env updated (${available.join(', ')}). In the frontend: xflow.functions.invoke('${names[0]}')`));
186
- }