@getxflow/cli 0.1.4 → 0.1.5

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
@@ -24,6 +24,7 @@ xflow publish
24
24
  | `functions list` / `functions deploy` | облачные функции проекта из `functions/<имя>/index.ts` |
25
25
  | `functions invoke` / `functions logs` | вызвать функцию, посмотреть её падения со стеком |
26
26
  | `schedules list` / `set` / `rm` | запуск функций по времени (таймер-триггеры) |
27
+ | `env` / `env check` / `env set` | переменные окружения функций, значения наружу не отдаются |
27
28
  | `logs` | ошибки выложенного приложения в браузере |
28
29
  | `projects list` / `projects get` / `open` | проекты организации и адреса приложения |
29
30
  | `skills` | инструкция о платформе для ИИ-агента (формат [Agent Skills](https://agentskills.io)) |
package/dist/bin.js CHANGED
@@ -12,6 +12,7 @@ const version_1 = require("./version");
12
12
  const auth_1 = require("./commands/auth");
13
13
  const projects_1 = require("./commands/projects");
14
14
  const db_1 = require("./commands/db");
15
+ const env_1 = require("./commands/env");
15
16
  const functions_1 = require("./commands/functions");
16
17
  const logs_1 = require("./commands/logs");
17
18
  const schedules_1 = require("./commands/schedules");
@@ -84,6 +85,24 @@ async function run(args) {
84
85
  case 'logs':
85
86
  await (0, logs_1.logs)(rest);
86
87
  return;
88
+ case 'env':
89
+ if (second === 'set') {
90
+ await (0, env_1.envSet)(rest);
91
+ return;
92
+ }
93
+ if (second === 'rm' || second === 'remove') {
94
+ await (0, env_1.envRemove)(rest);
95
+ return;
96
+ }
97
+ if (second === 'check') {
98
+ await (0, env_1.envCheck)();
99
+ return;
100
+ }
101
+ if (second === undefined || second === 'list' || second === 'pull') {
102
+ await (0, env_1.envList)();
103
+ return;
104
+ }
105
+ throw new errors_1.CliError(`Неизвестная команда: env ${second}`, 'Есть list, check, set и rm');
87
106
  case 'schedules':
88
107
  if (second === 'set') {
89
108
  await (0, schedules_1.schedulesSet)(rest);
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.envList = envList;
4
+ exports.envCheck = envCheck;
5
+ exports.envSet = envSet;
6
+ exports.envRemove = envRemove;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const api_1 = require("../api");
10
+ const args_1 = require("../args");
11
+ const config_1 = require("../config");
12
+ const errors_1 = require("../errors");
13
+ const session_1 = require("../session");
14
+ const ui_1 = require("../ui");
15
+ const FUNCTIONS_DIR = 'functions';
16
+ /** Что функции просят у окружения: `process.env.NAME` и `process.env['NAME']`. */
17
+ const ENV_REFERENCE = /process\.env(?:\.([A-Z0-9_]+)|\[['"]([A-Z0-9_]+)['"]\])/g;
18
+ /** Свои переменные платформа кладёт сама, спрашивать их у пользователя незачем. */
19
+ const PROVIDED = new Set([
20
+ 'XFLOW_PROJECT_ID',
21
+ 'XFLOW_PROJECT_TOKEN',
22
+ 'XFLOW_PROJECT_SCHEMA',
23
+ 'XFLOW_FUNCTION_NAME',
24
+ 'XFLOW_API_URL',
25
+ 'DATABASE_URL',
26
+ 'NODE_ENV',
27
+ ]);
28
+ function sourceFiles(dir, found = []) {
29
+ if (!(0, node_fs_1.existsSync)(dir))
30
+ return found;
31
+ for (const entry of (0, node_fs_1.readdirSync)(dir)) {
32
+ const full = (0, node_path_1.join)(dir, entry);
33
+ if ((0, node_fs_1.statSync)(full).isDirectory())
34
+ sourceFiles(full, found);
35
+ else if (/\.(ts|js|mjs)$/.test(entry))
36
+ found.push(full);
37
+ }
38
+ return found;
39
+ }
40
+ /** Какие переменные упоминают функции проекта, и в какой из них. */
41
+ function referencedByFunctions(root) {
42
+ const references = new Map();
43
+ for (const file of sourceFiles((0, node_path_1.join)(root, FUNCTIONS_DIR))) {
44
+ const functionName = file.slice((0, node_path_1.join)(root, FUNCTIONS_DIR).length + 1).split(/[\\/]/)[0];
45
+ const code = (0, node_fs_1.readFileSync)(file, 'utf-8');
46
+ for (const match of code.matchAll(ENV_REFERENCE)) {
47
+ const name = match[1] || match[2];
48
+ if (!name || PROVIDED.has(name))
49
+ continue;
50
+ const users = references.get(name) ?? [];
51
+ if (!users.includes(functionName))
52
+ users.push(functionName);
53
+ references.set(name, users);
54
+ }
55
+ }
56
+ return references;
57
+ }
58
+ async function fetchVariables() {
59
+ const { root, config } = (0, config_1.requireProject)();
60
+ const client = (0, session_1.connect)(config);
61
+ const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env`);
62
+ return { names: new Set(data.variables.map((row) => row.name)), rows: data.variables, root };
63
+ }
64
+ async function envList() {
65
+ const { rows } = await fetchVariables();
66
+ if (rows.length === 0) {
67
+ (0, ui_1.note)('Переменных нет');
68
+ (0, ui_1.note)((0, ui_1.dim)(' Записать: xflow env set SMTP_PASSWORD=секрет'));
69
+ return;
70
+ }
71
+ (0, ui_1.table)(rows.map((row) => [row.name, row.scope === 'project' ? 'только этот проект' : 'вся организация']));
72
+ (0, ui_1.note)((0, ui_1.dim)(' Значения платформа не отдаёт: их видно только внутри функции'));
73
+ }
74
+ /**
75
+ * Сверить, хватает ли функциям переменных.
76
+ *
77
+ * Это ответ на самый частый способ потерять полчаса: функция выкатилась, а падает
78
+ * на пустом process.env, потому что переменную забыли записать наверх.
79
+ */
80
+ async function envCheck() {
81
+ const { names, root } = await fetchVariables();
82
+ const referenced = referencedByFunctions(root);
83
+ if (referenced.size === 0) {
84
+ (0, ui_1.note)('Функции проекта не читают переменных окружения');
85
+ return;
86
+ }
87
+ const missing = [];
88
+ const present = [];
89
+ for (const [name, users] of [...referenced.entries()].sort()) {
90
+ const row = [name, users.join(', ')];
91
+ if (names.has(name))
92
+ present.push(row);
93
+ else
94
+ missing.push(row);
95
+ }
96
+ if (present.length > 0) {
97
+ (0, ui_1.out)((0, ui_1.bold)('На платформе:'));
98
+ (0, ui_1.table)(present);
99
+ }
100
+ if (missing.length === 0) {
101
+ (0, ui_1.ok)('Всем функциям хватает переменных');
102
+ return;
103
+ }
104
+ (0, ui_1.out)('');
105
+ (0, ui_1.fail)('Нет на платформе:');
106
+ (0, ui_1.table)(missing);
107
+ throw new errors_1.CliError(`Переменных не хватает: ${missing.length}`, 'Записать: xflow env set ИМЯ=значение. Пока их нет, функция получит undefined');
108
+ }
109
+ async function envSet(args) {
110
+ const { root, config } = (0, config_1.requireProject)();
111
+ const client = (0, session_1.connect)(config);
112
+ const pair = args.words[1];
113
+ if (!pair || !pair.includes('=')) {
114
+ throw new errors_1.CliError('Нужна пара ИМЯ=значение', 'Например: xflow env set SMTP_PASSWORD=секрет');
115
+ }
116
+ const name = pair.slice(0, pair.indexOf('=')).trim();
117
+ const value = pair.slice(pair.indexOf('=') + 1);
118
+ const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env`, {
119
+ method: 'POST',
120
+ body: { name, value, scope: (0, args_1.flagString)(args, 'scope') === 'project' ? 'project' : 'organization' },
121
+ });
122
+ (0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} записана (${result.scope === 'project' ? 'только этот проект' : 'вся организация'})`);
123
+ // Значение попадает в функцию на выкатке, а не в момент записи: пока функцию
124
+ // не передеплоили, в её окружении лежит прежнее.
125
+ const users = referencedByFunctions(root).get(name);
126
+ if (users && users.length > 0) {
127
+ (0, ui_1.note)((0, ui_1.dim)(` Чтобы значение доехало, перевыложите: xflow functions deploy ${users.join(' && xflow functions deploy ')}`));
128
+ }
129
+ }
130
+ async function envRemove(args) {
131
+ const { config } = (0, config_1.requireProject)();
132
+ const client = (0, session_1.connect)(config);
133
+ const name = args.words[1];
134
+ if (!name)
135
+ throw new errors_1.CliError('Нужно имя переменной', 'Что записано: xflow env');
136
+ const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
137
+ if (!result.removed) {
138
+ (0, ui_1.note)(`Переменной ${name} нет`);
139
+ return;
140
+ }
141
+ (0, ui_1.ok)(`${name} удалена`);
142
+ (0, ui_1.note)((0, ui_1.dim)(' В уже выложенных функциях значение останется до следующей выкатки'));
143
+ }
package/dist/help.js CHANGED
@@ -44,6 +44,9 @@ ${(0, ui_1.bold)('Функции')}
44
44
  xflow schedules set <имя> "0 3 ? * * *"
45
45
  запускать функцию по расписанию (UTC)
46
46
  xflow schedules rm <имя> снять расписание
47
+ xflow env [check] переменные функций: что записано, чего не хватает
48
+ xflow env set ИМЯ=значение записать переменную
49
+ xflow env rm ИМЯ удалить переменную
47
50
 
48
51
  ${(0, ui_1.bold)('База данных')}
49
52
  xflow db status какие миграции применены, а какие ждут
@@ -106,6 +109,27 @@ ${(0, ui_1.bold)('xflow functions invoke')}.
106
109
 
107
110
  Ошибки браузера собирает ${(0, ui_1.bold)('src/utils/error-logger.ts')} шаблона и только с
108
111
  выложенных адресов: локальный ${(0, ui_1.bold)('npm run dev')} сюда не пишет.`,
112
+ env: `${(0, ui_1.bold)('xflow env')} — переменные окружения функций
113
+
114
+ Ключи, пароли и адреса чужих сервисов хранит платформа, а не репозиторий.
115
+ Значение уходит наверх и обратно не возвращается: увидеть его можно только внутри
116
+ функции. Так что своё же значение придётся хранить там, где вы его взяли.
117
+
118
+ xflow env что записано (имена, не значения)
119
+ xflow env check чего не хватает функциям этого проекта
120
+ xflow env set SMTP_PASSWORD=… записать
121
+ xflow env rm SMTP_PASSWORD удалить
122
+
123
+ --scope project видно только этому проекту, а не всей организации
124
+
125
+ ${(0, ui_1.bold)('check')} читает исходники в ${(0, ui_1.bold)('functions/')} и ищет обращения ${(0, ui_1.bold)('process.env.ИМЯ')}. Это же
126
+ правило действует на выкатке: функция получает только те переменные, которые сама
127
+ упоминает по имени. Имя, собранное выражением (${(0, ui_1.bold)("process.env['KEY_' + n]")}), в окружение
128
+ не попадёт — читайте переменные буквально.
129
+
130
+ Значение доезжает до функции на выкатке, а не в момент записи: после ${(0, ui_1.bold)('env set')} нужно
131
+ ${(0, ui_1.bold)('xflow functions deploy')} затронутых функций. То же и после удаления: в уже выложенной
132
+ функции старое значение останется до следующей выкатки.`,
109
133
  schedules: `${(0, ui_1.bold)('xflow schedules')} — запуск функций по времени
110
134
 
111
135
  Расписание это таймер-триггер Yandex: он зовёт функцию сам, без участия
package/dist/version.js CHANGED
@@ -6,6 +6,6 @@ exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
6
6
  * рантайме нельзя, после сборки он лежит на уровень выше dist и в бандл не
7
7
  * попадает.
8
8
  */
9
- exports.CLI_VERSION = '0.1.4';
9
+ exports.CLI_VERSION = '0.1.5';
10
10
  /** Адрес платформы по умолчанию. Переопределяется XFLOW_API_URL и полем `api` в xflow.json. */
11
11
  exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "CLI платформы XFlow: синхронизация кода, деплой и публикация приложений",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -58,8 +58,14 @@ and needs `xflow deploy` again.
58
58
  Treat a function as a public API: the token ships inside the frontend bundle, so anyone
59
59
  who opens the app can call it.
60
60
 
61
- Organization secrets reach a function only if it mentions them via `process.env`, so
62
- read them by name and do not build variable names dynamically.
61
+ Keys and passwords live on the platform, not in the repository: `xflow env set SMTP_PASSWORD=…`
62
+ writes one, `xflow env` lists the names, `xflow env check` tells you which variables your
63
+ functions read but the platform does not have. Values never come back out — the only place
64
+ they exist is inside the running function.
65
+
66
+ A function receives only the variables it mentions by name via `process.env.NAME`, so never
67
+ assemble a variable name from an expression. New values arrive on the next
68
+ `xflow functions deploy`, not at the moment they are written.
63
69
 
64
70
  To run a function on a timer: `xflow schedules set report "0 3 ? * * *"` (daily at 03:00).
65
71
  Six fields, UTC, and exactly one of day-of-month / day-of-week must be `?` — that is