@getxflow/cli 0.1.4 → 0.1.7
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 +6 -0
- package/dist/api.js +5 -2
- package/dist/args.js +3 -1
- package/dist/bin.js +26 -3
- package/dist/commands/auth.js +12 -9
- package/dist/commands/deploy.js +65 -50
- package/dist/commands/env.js +143 -0
- package/dist/commands/functions.js +1 -2
- package/dist/commands/mcp.js +99 -0
- package/dist/commands/projects.js +13 -23
- package/dist/commands/skills.js +97 -19
- package/dist/commands/sources.js +4 -2
- package/dist/help.js +74 -25
- package/dist/template.js +1 -1
- package/dist/tree.js +0 -26
- package/dist/types.js +8 -1
- package/dist/version.js +1 -1
- package/package.json +22 -22
- package/skills/xflow/SKILL.md +92 -10
- package/dist/open-url.js +0 -24
package/README.md
CHANGED
|
@@ -24,9 +24,11 @@ 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)) |
|
|
31
|
+
| `mcp install` | подключить агента к платформе напрямую, без терминала |
|
|
30
32
|
|
|
31
33
|
Справка по команде: `xflow help <команда>`.
|
|
32
34
|
|
|
@@ -34,5 +36,9 @@ xflow publish
|
|
|
34
36
|
рядом описание платформы, которое Claude Code, Codex и OpenClaw подхватывают сами, когда задача
|
|
35
37
|
касается XFlow. Ваши `AGENTS.md` и `CLAUDE.md` команда не трогает.
|
|
36
38
|
|
|
39
|
+
`xflow mcp install` подключает агента к платформе напрямую: база, миграции, функции, расписания,
|
|
40
|
+
переменные и версии становятся его инструментами, без разбора вывода команд. Код, сборка и выкладка
|
|
41
|
+
остаются в CLI.
|
|
42
|
+
|
|
37
43
|
Ключ хранится в `~/.xflow/credentials.json`, конфигурация проекта — в `xflow.json`.
|
|
38
44
|
Для CI передайте ключ переменной `XFLOW_TOKEN`.
|
package/dist/api.js
CHANGED
|
@@ -18,12 +18,15 @@ class ApiError extends Error {
|
|
|
18
18
|
code;
|
|
19
19
|
status;
|
|
20
20
|
hint;
|
|
21
|
-
|
|
21
|
+
/** Разбор отказа, если он есть: например, нарушения, из-за которых не пошла сборка. */
|
|
22
|
+
issues;
|
|
23
|
+
constructor(message, code, status, hint, issues) {
|
|
22
24
|
super(message);
|
|
23
25
|
this.name = 'ApiError';
|
|
24
26
|
this.code = code;
|
|
25
27
|
this.status = status;
|
|
26
28
|
this.hint = hint;
|
|
29
|
+
this.issues = issues;
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
exports.ApiError = ApiError;
|
|
@@ -94,7 +97,7 @@ async function readError(response) {
|
|
|
94
97
|
const text = await response.text().catch(() => '');
|
|
95
98
|
try {
|
|
96
99
|
const parsed = JSON.parse(text);
|
|
97
|
-
return new ApiError(parsed.error || `Запрос отклонён (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint);
|
|
100
|
+
return new ApiError(parsed.error || `Запрос отклонён (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed.issues);
|
|
98
101
|
}
|
|
99
102
|
catch {
|
|
100
103
|
return new ApiError(`Запрос отклонён (${response.status})`, 'unknown', response.status, text.slice(0, 200) || undefined);
|
package/dist/args.js
CHANGED
|
@@ -20,12 +20,14 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
20
20
|
'json',
|
|
21
21
|
'help',
|
|
22
22
|
'version',
|
|
23
|
-
'
|
|
23
|
+
'global',
|
|
24
|
+
'live',
|
|
24
25
|
'no-push',
|
|
25
26
|
'skip-build',
|
|
26
27
|
'all',
|
|
27
28
|
'dry-run',
|
|
28
29
|
'allow-destructive',
|
|
30
|
+
'show-token',
|
|
29
31
|
]);
|
|
30
32
|
function parseArgs(argv) {
|
|
31
33
|
const words = [];
|
package/dist/bin.js
CHANGED
|
@@ -12,8 +12,10 @@ 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");
|
|
18
|
+
const mcp_1 = require("./commands/mcp");
|
|
17
19
|
const schedules_1 = require("./commands/schedules");
|
|
18
20
|
const skills_1 = require("./commands/skills");
|
|
19
21
|
const sources_1 = require("./commands/sources");
|
|
@@ -47,9 +49,6 @@ async function run(args) {
|
|
|
47
49
|
case 'link':
|
|
48
50
|
await (0, projects_1.link)(rest);
|
|
49
51
|
return;
|
|
50
|
-
case 'open':
|
|
51
|
-
await (0, projects_1.open)(rest);
|
|
52
|
-
return;
|
|
53
52
|
case 'projects':
|
|
54
53
|
if (second === 'get') {
|
|
55
54
|
await (0, projects_1.get)({ ...args, words: args.words.slice(2) });
|
|
@@ -63,6 +62,12 @@ async function run(args) {
|
|
|
63
62
|
case 'skills':
|
|
64
63
|
(0, skills_1.skills)(rest);
|
|
65
64
|
return;
|
|
65
|
+
case 'mcp':
|
|
66
|
+
if (second === undefined || second === 'install') {
|
|
67
|
+
await (0, mcp_1.mcpInstall)(rest);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw new errors_1.CliError(`Неизвестная команда: mcp ${second}`, 'Есть install');
|
|
66
71
|
case 'functions':
|
|
67
72
|
if (second === 'deploy') {
|
|
68
73
|
await (0, functions_1.functionsDeploy)(rest);
|
|
@@ -84,6 +89,24 @@ async function run(args) {
|
|
|
84
89
|
case 'logs':
|
|
85
90
|
await (0, logs_1.logs)(rest);
|
|
86
91
|
return;
|
|
92
|
+
case 'env':
|
|
93
|
+
if (second === 'set') {
|
|
94
|
+
await (0, env_1.envSet)(rest);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (second === 'rm' || second === 'remove') {
|
|
98
|
+
await (0, env_1.envRemove)(rest);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (second === 'check') {
|
|
102
|
+
await (0, env_1.envCheck)();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (second === undefined || second === 'list' || second === 'pull') {
|
|
106
|
+
await (0, env_1.envList)();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
throw new errors_1.CliError(`Неизвестная команда: env ${second}`, 'Есть list, check, set и rm');
|
|
87
110
|
case 'schedules':
|
|
88
111
|
if (second === 'set') {
|
|
89
112
|
await (0, schedules_1.schedulesSet)(rest);
|
package/dist/commands/auth.js
CHANGED
|
@@ -3,11 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.login = login;
|
|
4
4
|
exports.logout = logout;
|
|
5
5
|
exports.whoami = whoami;
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
6
7
|
const api_1 = require("../api");
|
|
7
8
|
const config_1 = require("../config");
|
|
8
9
|
const credentials_1 = require("../credentials");
|
|
9
10
|
const errors_1 = require("../errors");
|
|
10
|
-
const open_url_1 = require("../open-url");
|
|
11
11
|
const session_1 = require("../session");
|
|
12
12
|
const ui_1 = require("../ui");
|
|
13
13
|
function localConfig() {
|
|
@@ -31,13 +31,20 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
31
31
|
async function login() {
|
|
32
32
|
const config = localConfig();
|
|
33
33
|
const client = (0, session_1.anonymous)(config);
|
|
34
|
-
|
|
34
|
+
// Имя машины уходит на сервер и становится названием ключа: иначе в списке
|
|
35
|
+
// доступов лежат одинаковые строки и непонятно, какой компьютер отзывать.
|
|
36
|
+
const start = await (0, api_1.apiJson)(client, '/api/v1/auth/device', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: { client_name: (0, node_os_1.hostname)() },
|
|
39
|
+
});
|
|
35
40
|
(0, ui_1.note)('');
|
|
36
41
|
(0, ui_1.note)(` Код подтверждения: ${(0, ui_1.bold)(start.user_code)}`);
|
|
37
|
-
(0, ui_1.note)(`
|
|
42
|
+
(0, ui_1.note)(` Компьютер: ${(0, node_os_1.hostname)()}`);
|
|
43
|
+
(0, ui_1.note)(` Ссылка для входа: ${start.verification_uri_complete}`);
|
|
38
44
|
(0, ui_1.note)('');
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
// Браузер не открываем сами: команду часто запускает агент, и всплывшее окно
|
|
46
|
+
// забирает у человека внимание, не объяснив, что он подтверждает.
|
|
47
|
+
(0, ui_1.step)('Откройте ссылку и сверьте код на странице с показанным выше');
|
|
41
48
|
const deadline = Date.now() + start.expires_in * 1000;
|
|
42
49
|
let intervalMs = Math.max(start.interval, 1) * 1000;
|
|
43
50
|
for (;;) {
|
|
@@ -68,9 +75,6 @@ async function login() {
|
|
|
68
75
|
projectId: poll.project_id,
|
|
69
76
|
});
|
|
70
77
|
(0, ui_1.ok)(`Вход выполнен, ключ сохранён для ${client.apiUrl}`);
|
|
71
|
-
if (poll.project_id) {
|
|
72
|
-
(0, ui_1.note)((0, ui_1.dim)(` Ключ выдан на проект ${poll.project_id}: другие проекты им недоступны`));
|
|
73
|
-
}
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
76
80
|
}
|
|
@@ -89,7 +93,6 @@ async function whoami() {
|
|
|
89
93
|
const me = await (0, api_1.apiJson)(client, '/api/v1/me');
|
|
90
94
|
(0, ui_1.out)(`Организация: ${me.organization.name ?? me.organization.id}`);
|
|
91
95
|
(0, ui_1.out)(`Ключ: xfk_${me.key.prefix}… (${me.key.scopes.join(', ')})`);
|
|
92
|
-
(0, ui_1.out)(`Проект ключа: ${me.key.project_id ?? 'вся организация'}`);
|
|
93
96
|
(0, ui_1.out)(`Платформа: ${client.apiUrl}`);
|
|
94
97
|
if (process.env.XFLOW_TOKEN) {
|
|
95
98
|
(0, ui_1.note)((0, ui_1.dim)(' Ключ взят из XFLOW_TOKEN, сохранённый в ~/.xflow не используется'));
|
package/dist/commands/deploy.js
CHANGED
|
@@ -4,45 +4,60 @@ exports.deploy = deploy;
|
|
|
4
4
|
exports.publish = publish;
|
|
5
5
|
exports.rollback = rollback;
|
|
6
6
|
exports.deployments = deployments;
|
|
7
|
-
const node_child_process_1 = require("node:child_process");
|
|
8
|
-
const node_path_1 = require("node:path");
|
|
9
7
|
const api_1 = require("../api");
|
|
10
8
|
const args_1 = require("../args");
|
|
11
9
|
const config_1 = require("../config");
|
|
12
10
|
const errors_1 = require("../errors");
|
|
13
11
|
const session_1 = require("../session");
|
|
14
|
-
const tree_1 = require("../tree");
|
|
15
|
-
const zip_1 = require("../zip");
|
|
16
12
|
const ui_1 = require("../ui");
|
|
17
|
-
const functions_1 = require("./functions");
|
|
18
13
|
const sources_1 = require("./sources");
|
|
19
|
-
|
|
20
|
-
const MAX_ARTIFACT_BYTES = 100 * 1024 * 1024;
|
|
14
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
15
|
/**
|
|
22
|
-
*
|
|
16
|
+
* Потолок ожидания сборки в терминале. Чуть больше, чем срок, после которого
|
|
17
|
+
* платформа закрывает молчащую сборку: так разработчик увидит внятный отказ, а
|
|
18
|
+
* не «идёт дольше ожидаемого» на сборке, которую уже похоронили.
|
|
19
|
+
*/
|
|
20
|
+
const WAIT_LIMIT_MS = 16 * 60_000;
|
|
21
|
+
/**
|
|
22
|
+
* Показать, чем проект не подошёл платформе.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* в терминал, поэтому кодировки и прогресс-бары выглядят как обычно.
|
|
24
|
+
* Печатаем весь список: правила известны заранее (скилл xflow), а узнавать их по
|
|
25
|
+
* одному через повторные запуски значит терять круг на каждое нарушение.
|
|
27
26
|
*/
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
function reportIssues(e) {
|
|
28
|
+
const issues = e.issues ?? [];
|
|
29
|
+
for (const issue of issues) {
|
|
30
|
+
const where = issue.file ? `${issue.file}${issue.lines?.length ? `:${issue.lines.join(', ')}` : ''}` : '';
|
|
31
|
+
(0, ui_1.out)(` ${(0, ui_1.bold)(issue.code)} ${where}`);
|
|
32
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${issue.message}`));
|
|
33
|
+
}
|
|
34
|
+
throw new errors_1.CliError(e.message, e.hint);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Дождаться конца сборки, печатая фазы.
|
|
38
|
+
*
|
|
39
|
+
* Платформа отвечает на запуск сразу и не держит соединение: сборка живёт в
|
|
40
|
+
* песочнице и переживает перезапуск платформы, поэтому её состояние забирается
|
|
41
|
+
* опросом, а не потоком.
|
|
42
|
+
*/
|
|
43
|
+
async function waitForBuild(client, projectId, started) {
|
|
44
|
+
const deadline = Date.now() + WAIT_LIMIT_MS;
|
|
45
|
+
let shown = '';
|
|
46
|
+
while (Date.now() < deadline) {
|
|
47
|
+
await sleep(started.poll_after_ms || 3_000);
|
|
48
|
+
const build = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/builds/${started.deploy_id}`);
|
|
49
|
+
if (build.phase !== shown) {
|
|
50
|
+
shown = build.phase;
|
|
51
|
+
(0, ui_1.step)(build.phase_title);
|
|
52
|
+
}
|
|
53
|
+
if (build.phase === 'done' || build.phase === 'failed')
|
|
54
|
+
return build;
|
|
55
|
+
}
|
|
56
|
+
throw new errors_1.CliError('Сборка идёт дольше ожидаемого', `Посмотреть состояние: xflow deployments. Версия ${started.deploy_id}`);
|
|
40
57
|
}
|
|
41
58
|
async function deploy(args) {
|
|
42
59
|
const { root, config } = (0, config_1.requireProject)();
|
|
43
60
|
const client = (0, session_1.connect)(config);
|
|
44
|
-
const buildCommand = config.build?.command ?? 'npm run build';
|
|
45
|
-
const buildDir = (0, node_path_1.resolve)(root, config.build?.dir ?? 'dist');
|
|
46
61
|
let revision;
|
|
47
62
|
if ((0, args_1.flagBool)(args, 'no-push')) {
|
|
48
63
|
const server = await (0, sources_1.latestRevision)(client, config.projectId);
|
|
@@ -50,34 +65,34 @@ async function deploy(args) {
|
|
|
50
65
|
throw new errors_1.CliError('На сервере нет исходников', 'Отправьте их: xflow push');
|
|
51
66
|
}
|
|
52
67
|
revision = server.revision;
|
|
53
|
-
(0, ui_1.warn)(`Исходники не отправлены (--no-push),
|
|
68
|
+
(0, ui_1.warn)(`Исходники не отправлены (--no-push), сборка пойдёт от ревизии ${revision}`);
|
|
54
69
|
}
|
|
55
70
|
else {
|
|
56
71
|
revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
57
72
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
(0,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (files.length === 0) {
|
|
66
|
-
throw new errors_1.CliError(`Каталог сборки пуст: ${buildDir}`, 'Проверьте build.dir в xflow.json: там должен быть каталог, куда сборщик кладёт результат');
|
|
73
|
+
(0, ui_1.step)(`Собираю на платформе из ревизии ${revision}`);
|
|
74
|
+
let started;
|
|
75
|
+
try {
|
|
76
|
+
started = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/builds`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
body: { revision },
|
|
79
|
+
});
|
|
67
80
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
throw
|
|
81
|
+
catch (e) {
|
|
82
|
+
if (e instanceof api_1.ApiError && e.issues?.length)
|
|
83
|
+
reportIssues(e);
|
|
84
|
+
throw e;
|
|
72
85
|
}
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
|
|
86
|
+
const build = await waitForBuild(client, config.projectId, started);
|
|
87
|
+
if (build.phase === 'failed') {
|
|
88
|
+
if (build.log_tail) {
|
|
89
|
+
(0, ui_1.out)();
|
|
90
|
+
(0, ui_1.out)(build.log_tail);
|
|
91
|
+
}
|
|
92
|
+
throw new errors_1.CliError(build.error || 'Сборка не удалась', `Исправьте код и повторите: xflow deploy. Версия ${build.deploy_id}`);
|
|
76
93
|
}
|
|
77
|
-
(0, ui_1.
|
|
78
|
-
|
|
79
|
-
(0, ui_1.ok)(`Версия ${result.deploy_id} собрана из ревизии ${result.revision}`);
|
|
80
|
-
(0, ui_1.out)(result.url);
|
|
94
|
+
(0, ui_1.ok)(`Версия ${build.deploy_id} собрана из ревизии ${build.revision} за ${build.elapsed_s} с`);
|
|
95
|
+
(0, ui_1.out)(build.project_url);
|
|
81
96
|
(0, ui_1.note)((0, ui_1.dim)(' Показать посетителям: xflow publish'));
|
|
82
97
|
}
|
|
83
98
|
async function publish() {
|
|
@@ -89,9 +104,9 @@ async function publish() {
|
|
|
89
104
|
(0, ui_1.ok)('Эта версия уже опубликована');
|
|
90
105
|
}
|
|
91
106
|
else {
|
|
92
|
-
(0, ui_1.ok)(`Опубликована версия ${result.deploy_id}
|
|
107
|
+
(0, ui_1.ok)(`Опубликована версия ${result.deploy_id}: её видят посетители`);
|
|
93
108
|
}
|
|
94
|
-
(0, ui_1.out)(result.
|
|
109
|
+
(0, ui_1.out)(result.project_url);
|
|
95
110
|
}
|
|
96
111
|
async function rollback(args) {
|
|
97
112
|
const { config } = (0, config_1.requireProject)();
|
|
@@ -101,8 +116,8 @@ async function rollback(args) {
|
|
|
101
116
|
}
|
|
102
117
|
const client = (0, session_1.connect)(config);
|
|
103
118
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/rollback`, { method: 'POST', body: { deploy_id: deployId } });
|
|
104
|
-
(0, ui_1.ok)(`dev
|
|
105
|
-
(0, ui_1.out)(result.
|
|
119
|
+
(0, ui_1.ok)(`dev-версия проекта переключена на ${result.deploy_id}`);
|
|
120
|
+
(0, ui_1.out)(result.project_url);
|
|
106
121
|
if (result.revision !== null) {
|
|
107
122
|
(0, ui_1.note)((0, ui_1.dim)(` Код этой версии: xflow pull --revision ${result.revision} --into ./v${result.deploy_id}`));
|
|
108
123
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.functionsList = functionsList;
|
|
4
|
-
exports.refreshFunctionsEnv = refreshFunctionsEnv;
|
|
5
4
|
exports.functionsInvoke = functionsInvoke;
|
|
6
5
|
exports.functionsDeploy = functionsDeploy;
|
|
7
6
|
const node_fs_1 = require("node:fs");
|
|
@@ -78,7 +77,7 @@ async function functionsList() {
|
|
|
78
77
|
fn.name,
|
|
79
78
|
fn.status === 'deployed' ? 'выложена' : fn.status === 'failed' ? 'ошибка' : fn.status,
|
|
80
79
|
fn.last_deployed_at ? (0, ui_1.formatAge)(fn.last_deployed_at) : '—',
|
|
81
|
-
fn.error_message ??
|
|
80
|
+
fn.error_message ?? '',
|
|
82
81
|
]));
|
|
83
82
|
}
|
|
84
83
|
/**
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mcpInstall = mcpInstall;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const api_1 = require("../api");
|
|
8
|
+
const args_1 = require("../args");
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
const session_1 = require("../session");
|
|
11
|
+
const ui_1 = require("../ui");
|
|
12
|
+
/** Клиенты, у которых есть своя команда для добавления сервера. */
|
|
13
|
+
const CLIENTS = [
|
|
14
|
+
{
|
|
15
|
+
binary: 'claude',
|
|
16
|
+
label: 'Claude Code',
|
|
17
|
+
args: (url, token) => [
|
|
18
|
+
'mcp',
|
|
19
|
+
'add',
|
|
20
|
+
'--transport',
|
|
21
|
+
'http',
|
|
22
|
+
'xflow',
|
|
23
|
+
url,
|
|
24
|
+
'--header',
|
|
25
|
+
`Authorization: Bearer ${token}`,
|
|
26
|
+
],
|
|
27
|
+
// Повторный запуск должен обновлять запись, а не спотыкаться о неё: команду
|
|
28
|
+
// выполняют и после смены ключа, и после обновления пакета.
|
|
29
|
+
reset: ['mcp', 'remove', 'xflow', '-s', 'local'],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
const WINDOWS = process.platform === 'win32';
|
|
33
|
+
/**
|
|
34
|
+
* Запустить чужую программу.
|
|
35
|
+
*
|
|
36
|
+
* На Windows без оболочки не найти `.cmd`-обёртки, которыми ставятся все
|
|
37
|
+
* консольные пакеты npm. А оболочка не экранирует аргументы, а склеивает их:
|
|
38
|
+
* заголовок «Authorization: Bearer …» разваливается по пробелу. Поэтому на
|
|
39
|
+
* Windows собираем строку сами и сами же расставляем кавычки.
|
|
40
|
+
*/
|
|
41
|
+
function quote(value) {
|
|
42
|
+
return WINDOWS ? `"${value.replace(/"/g, '""')}"` : value;
|
|
43
|
+
}
|
|
44
|
+
function execute(binary, args) {
|
|
45
|
+
// Вывод чужой команды перехватываем, а не пропускаем на экран: клиенты любят
|
|
46
|
+
// напечатать добавленные заголовки целиком, вместе с ключом.
|
|
47
|
+
const options = { encoding: 'utf-8' };
|
|
48
|
+
return WINDOWS
|
|
49
|
+
? (0, node_child_process_1.spawnSync)([binary, ...args.map(quote)].join(' '), { ...options, shell: true })
|
|
50
|
+
: (0, node_child_process_1.spawnSync)(binary, args, options);
|
|
51
|
+
}
|
|
52
|
+
function hasBinary(binary) {
|
|
53
|
+
return execute(binary, ['--version']).status === 0;
|
|
54
|
+
}
|
|
55
|
+
async function mcpInstall(args) {
|
|
56
|
+
// Сервер общий для организации, поэтому команда работает и вне папки проекта.
|
|
57
|
+
const root = (0, config_1.findProjectRoot)();
|
|
58
|
+
const client = (0, session_1.connect)(root ? (0, config_1.readConfig)(root) : undefined);
|
|
59
|
+
const url = `${client.apiUrl.replace(/\/+$/, '')}/api/mcp`;
|
|
60
|
+
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)(' Ключ на всю организацию: проект агент называет сам'));
|
|
64
|
+
let found = 0;
|
|
65
|
+
let installed = 0;
|
|
66
|
+
for (const target of CLIENTS) {
|
|
67
|
+
if (!hasBinary(target.binary))
|
|
68
|
+
continue;
|
|
69
|
+
found++;
|
|
70
|
+
(0, ui_1.step)(`Прописываю в ${target.label}`);
|
|
71
|
+
execute(target.binary, [...target.reset]);
|
|
72
|
+
const result = execute(target.binary, target.args(url, client.token));
|
|
73
|
+
if (result.status === 0) {
|
|
74
|
+
installed++;
|
|
75
|
+
(0, ui_1.ok)(`${target.label}: сервер xflow подключён`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const reason = `${result.stderr ?? ''}${result.stdout ?? ''}`.trim();
|
|
79
|
+
(0, ui_1.note)(`${target.label}: команда завершилась с ошибкой`);
|
|
80
|
+
if (reason)
|
|
81
|
+
(0, ui_1.out)((0, ui_1.dim)(` ${reason.split(client.token).join('<ключ>').split('\n').slice(0, 5).join('\n ')}`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
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 <ключ>`);
|
|
89
|
+
}
|
|
90
|
+
// Ключ печатаем только по явной просьбе: см. комментарий в шапке файла.
|
|
91
|
+
if ((0, args_1.flagBool)(args, 'show-token')) {
|
|
92
|
+
(0, ui_1.out)('');
|
|
93
|
+
(0, ui_1.out)(`${(0, ui_1.bold)('Ключ')} ${client.token}`);
|
|
94
|
+
(0, ui_1.note)((0, ui_1.dim)(' Не оставляйте его в переписке с агентом'));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
(0, ui_1.note)((0, ui_1.dim)(` Ключ лежит в ${(0, node_path_1.join)((0, node_os_1.homedir)(), '.xflow', 'credentials.json')} и на экран не выводится`));
|
|
98
|
+
}
|
|
99
|
+
}
|