@getxflow/cli 0.1.5 → 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 +5 -0
- package/dist/api.js +5 -2
- package/dist/args.js +3 -1
- package/dist/bin.js +7 -3
- package/dist/commands/auth.js +12 -9
- package/dist/commands/deploy.js +65 -50
- 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 +50 -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 +84 -8
- package/dist/open-url.js +0 -24
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ xflow publish
|
|
|
28
28
|
| `logs` | ошибки выложенного приложения в браузере |
|
|
29
29
|
| `projects list` / `projects get` / `open` | проекты организации и адреса приложения |
|
|
30
30
|
| `skills` | инструкция о платформе для ИИ-агента (формат [Agent Skills](https://agentskills.io)) |
|
|
31
|
+
| `mcp install` | подключить агента к платформе напрямую, без терминала |
|
|
31
32
|
|
|
32
33
|
Справка по команде: `xflow help <команда>`.
|
|
33
34
|
|
|
@@ -35,5 +36,9 @@ xflow publish
|
|
|
35
36
|
рядом описание платформы, которое Claude Code, Codex и OpenClaw подхватывают сами, когда задача
|
|
36
37
|
касается XFlow. Ваши `AGENTS.md` и `CLAUDE.md` команда не трогает.
|
|
37
38
|
|
|
39
|
+
`xflow mcp install` подключает агента к платформе напрямую: база, миграции, функции, расписания,
|
|
40
|
+
переменные и версии становятся его инструментами, без разбора вывода команд. Код, сборка и выкладка
|
|
41
|
+
остаются в CLI.
|
|
42
|
+
|
|
38
43
|
Ключ хранится в `~/.xflow/credentials.json`, конфигурация проекта — в `xflow.json`.
|
|
39
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
|
@@ -15,6 +15,7 @@ const db_1 = require("./commands/db");
|
|
|
15
15
|
const env_1 = require("./commands/env");
|
|
16
16
|
const functions_1 = require("./commands/functions");
|
|
17
17
|
const logs_1 = require("./commands/logs");
|
|
18
|
+
const mcp_1 = require("./commands/mcp");
|
|
18
19
|
const schedules_1 = require("./commands/schedules");
|
|
19
20
|
const skills_1 = require("./commands/skills");
|
|
20
21
|
const sources_1 = require("./commands/sources");
|
|
@@ -48,9 +49,6 @@ async function run(args) {
|
|
|
48
49
|
case 'link':
|
|
49
50
|
await (0, projects_1.link)(rest);
|
|
50
51
|
return;
|
|
51
|
-
case 'open':
|
|
52
|
-
await (0, projects_1.open)(rest);
|
|
53
|
-
return;
|
|
54
52
|
case 'projects':
|
|
55
53
|
if (second === 'get') {
|
|
56
54
|
await (0, projects_1.get)({ ...args, words: args.words.slice(2) });
|
|
@@ -64,6 +62,12 @@ async function run(args) {
|
|
|
64
62
|
case 'skills':
|
|
65
63
|
(0, skills_1.skills)(rest);
|
|
66
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');
|
|
67
71
|
case 'functions':
|
|
68
72
|
if (second === 'deploy') {
|
|
69
73
|
await (0, functions_1.functionsDeploy)(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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -5,15 +5,14 @@ exports.init = init;
|
|
|
5
5
|
exports.link = link;
|
|
6
6
|
exports.list = list;
|
|
7
7
|
exports.get = get;
|
|
8
|
-
exports.open = open;
|
|
9
8
|
const node_fs_1 = require("node:fs");
|
|
10
9
|
const node_path_1 = require("node:path");
|
|
11
10
|
const api_1 = require("../api");
|
|
12
11
|
const args_1 = require("../args");
|
|
13
12
|
const config_1 = require("../config");
|
|
14
13
|
const errors_1 = require("../errors");
|
|
15
|
-
const open_url_1 = require("../open-url");
|
|
16
14
|
const session_1 = require("../session");
|
|
15
|
+
const skills_1 = require("./skills");
|
|
17
16
|
const template_1 = require("../template");
|
|
18
17
|
const zip_1 = require("../zip");
|
|
19
18
|
const ui_1 = require("../ui");
|
|
@@ -99,24 +98,24 @@ async function init(args) {
|
|
|
99
98
|
(0, ui_1.note)((0, ui_1.dim)(' Разберитесь с папкой и подберите его: xflow link ' + project.id));
|
|
100
99
|
throw e;
|
|
101
100
|
}
|
|
101
|
+
(0, skills_1.installSkillQuietly)(target);
|
|
102
102
|
(0, ui_1.ok)(`Проект «${project.name}» создан: ${files.length} файлов шаблона`);
|
|
103
103
|
(0, ui_1.out)('');
|
|
104
104
|
(0, ui_1.out)(` ${(0, ui_1.bold)('Дальше:')}`);
|
|
105
105
|
(0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}`);
|
|
106
106
|
(0, ui_1.out)(' npm install');
|
|
107
107
|
(0, ui_1.out)(' npm run dev # разработка');
|
|
108
|
-
(0, ui_1.out)(' xflow skills # рассказать вашему ИИ-агенту про платформу');
|
|
109
108
|
(0, ui_1.out)(' xflow deploy # отправить код, собрать и выложить');
|
|
110
109
|
}
|
|
111
110
|
/** Связать текущую папку с уже существующим проектом. */
|
|
112
111
|
async function link(args) {
|
|
113
|
-
const projectId = args.words[0];
|
|
114
|
-
if (!projectId) {
|
|
115
|
-
throw new errors_1.CliError('Нужен идентификатор проекта', 'Список проектов: xflow projects list');
|
|
116
|
-
}
|
|
117
112
|
const root = (0, config_1.findProjectRoot)() ?? process.cwd();
|
|
118
113
|
const existing = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, config_1.CONFIG_FILE)) ? (0, config_1.readConfig)(root) : undefined;
|
|
119
114
|
const client = (0, session_1.connect)(existing);
|
|
115
|
+
const projectId = args.words[0];
|
|
116
|
+
if (!projectId) {
|
|
117
|
+
throw new errors_1.CliError('Нужен идентификатор проекта', 'Список: xflow projects list');
|
|
118
|
+
}
|
|
120
119
|
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
121
120
|
const dir = existing ? root : process.cwd();
|
|
122
121
|
(0, config_1.writeConfig)(dir, {
|
|
@@ -131,6 +130,7 @@ async function link(args) {
|
|
|
131
130
|
write(dir, '.env', (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions));
|
|
132
131
|
(0, ui_1.note)((0, ui_1.dim)(' Создан .env с токеном проекта и адресами функций'));
|
|
133
132
|
}
|
|
133
|
+
(0, skills_1.installSkillQuietly)(dir);
|
|
134
134
|
(0, ui_1.ok)(`Папка связана с проектом «${card.name}»`);
|
|
135
135
|
(0, ui_1.note)((0, ui_1.dim)(` ${(0, node_path_1.join)(dir, config_1.CONFIG_FILE)}`));
|
|
136
136
|
}
|
|
@@ -145,7 +145,7 @@ async function list() {
|
|
|
145
145
|
(0, ui_1.table)(projects.map((p) => [
|
|
146
146
|
p.id,
|
|
147
147
|
p.name,
|
|
148
|
-
p.
|
|
148
|
+
p.live_deploy_id ? 'опубликован' : p.dev_deploy_id ? 'только dev' : 'без сборки',
|
|
149
149
|
(0, ui_1.formatAge)(p.updated_at),
|
|
150
150
|
]));
|
|
151
151
|
}
|
|
@@ -162,9 +162,11 @@ async function get(args) {
|
|
|
162
162
|
if (card.description)
|
|
163
163
|
(0, ui_1.out)(card.description);
|
|
164
164
|
(0, ui_1.out)('');
|
|
165
|
-
(0, ui_1.out)(
|
|
166
|
-
(0, ui_1.out)(
|
|
167
|
-
(0, ui_1.out)(
|
|
165
|
+
(0, ui_1.out)(`Собранная версия: ${card.dev_deploy_id ?? '— (сборки нет)'}`);
|
|
166
|
+
(0, ui_1.out)(`Видят посетители: ${card.live_deploy_id ?? '— (не публиковался)'}`);
|
|
167
|
+
(0, ui_1.out)(`База: ${card.database ? `${card.database.name} (${card.database.schema})` : '—'}`);
|
|
168
|
+
(0, ui_1.out)('');
|
|
169
|
+
(0, ui_1.out)(card.project_url);
|
|
168
170
|
if (card.functions.length > 0) {
|
|
169
171
|
(0, ui_1.out)('');
|
|
170
172
|
(0, ui_1.out)('Функции:');
|
|
@@ -176,15 +178,3 @@ async function get(args) {
|
|
|
176
178
|
(0, ui_1.table)(card.schedules.map((s) => [` ${s.function_name}`, s.cron_expression, s.status]));
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
|
-
async function open(args) {
|
|
180
|
-
const { config } = (0, config_1.requireProject)();
|
|
181
|
-
const client = (0, session_1.connect)(config);
|
|
182
|
-
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}`);
|
|
183
|
-
const live = (0, args_1.flagBool)(args, 'live');
|
|
184
|
-
const url = live ? card.live_url : card.dev_url;
|
|
185
|
-
if (!url) {
|
|
186
|
-
throw new errors_1.CliError(live ? 'Проект не публиковался' : 'У проекта нет сборки', live ? 'Опубликовать текущую версию: xflow publish' : 'Собрать и выложить: xflow deploy');
|
|
187
|
-
}
|
|
188
|
-
(0, ui_1.note)((0, ui_1.dim)(url));
|
|
189
|
-
(0, open_url_1.openUrl)(url);
|
|
190
|
-
}
|
package/dist/commands/skills.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installSkillQuietly = installSkillQuietly;
|
|
3
4
|
exports.skills = skills;
|
|
4
5
|
const node_fs_1 = require("node:fs");
|
|
5
6
|
const node_os_1 = require("node:os");
|
|
@@ -9,13 +10,38 @@ const errors_1 = require("../errors");
|
|
|
9
10
|
const ui_1 = require("../ui");
|
|
10
11
|
/**
|
|
11
12
|
* Формат SKILL.md общий для инструментов (agentskills.io), а папку каждый ищет
|
|
12
|
-
* свою: Claude Code в .claude/skills, Codex и OpenClaw в .agents/skills.
|
|
13
|
-
*
|
|
13
|
+
* свою: Claude Code в .claude/skills, Codex и OpenClaw в .agents/skills. Cursor к
|
|
14
|
+
* общему формату не пришёл и читает .cursor/rules/*.mdc, поэтому туда едет тот же
|
|
15
|
+
* текст под его заголовком. Спрашивать, чей агент, мы не хотим: раскладываем всем.
|
|
14
16
|
*/
|
|
15
|
-
const
|
|
17
|
+
const SKILL_DIRS = [
|
|
16
18
|
['.claude', 'skills'],
|
|
17
19
|
['.agents', 'skills'],
|
|
18
20
|
];
|
|
21
|
+
/**
|
|
22
|
+
* В домашней папке набор другой. Общий .agents/skills там читает OpenClaw, а
|
|
23
|
+
* Codex смотрит его только внутри репозитория: свой личный он держит отдельно,
|
|
24
|
+
* в .codex/skills. Без этой папки `--global` тихо не работает для Codex.
|
|
25
|
+
*/
|
|
26
|
+
const GLOBAL_SKILL_DIRS = [...SKILL_DIRS, ['.codex', 'skills']];
|
|
27
|
+
const CURSOR_RULE = ['.cursor', 'rules', 'xflow.mdc'];
|
|
28
|
+
/**
|
|
29
|
+
* Указатель в AGENTS.md.
|
|
30
|
+
*
|
|
31
|
+
* Скилл подхватывается лениво: только когда описание совпало с задачей. Просьба
|
|
32
|
+
* «добавь таблицу клиентов» с ним не совпадает, и агент не узнаёт, что проект
|
|
33
|
+
* вообще живёт на платформе. AGENTS.md читается всегда и без условий, поэтому
|
|
34
|
+
* пара строк здесь решает ту часть, которую ленивый скилл закрыть не может.
|
|
35
|
+
*/
|
|
36
|
+
const POINTER_MARKER = '<!-- xflow-skill -->';
|
|
37
|
+
const POINTER = `${POINTER_MARKER}
|
|
38
|
+
## XFlow
|
|
39
|
+
|
|
40
|
+
This project is hosted on the XFlow platform and ships through the \`xflow\` CLI, not
|
|
41
|
+
through a git push. Read \`.agents/skills/xflow/SKILL.md\` before deploying, publishing,
|
|
42
|
+
rolling back, touching the database or migrations, cloud functions, schedules,
|
|
43
|
+
environment variables or production logs. Command list: \`xflow help\`.
|
|
44
|
+
`;
|
|
19
45
|
/** Скилл едет в пакете рядом с dist: у CLI и у инструкций для агента одна версия. */
|
|
20
46
|
function skillSource() {
|
|
21
47
|
const path = (0, node_path_1.join)(__dirname, '..', '..', 'skills', 'xflow', 'SKILL.md');
|
|
@@ -24,27 +50,79 @@ function skillSource() {
|
|
|
24
50
|
}
|
|
25
51
|
return (0, node_fs_1.readFileSync)(path, 'utf-8');
|
|
26
52
|
}
|
|
53
|
+
/** Тот же текст под заголовком Cursor: поля у него свои, а тело общее. */
|
|
54
|
+
function cursorRule(skill) {
|
|
55
|
+
const description = /^description:\s*(.+)$/m.exec(skill)?.[1] ?? 'XFlow platform';
|
|
56
|
+
const body = skill.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
|
|
57
|
+
return `---\ndescription: ${description}\nalwaysApply: false\n---\n\n${body}`;
|
|
58
|
+
}
|
|
59
|
+
/** Пишет, только если содержимое отличается. Возвращает путь тронутого файла. */
|
|
60
|
+
function writeIfChanged(path, content) {
|
|
61
|
+
const before = (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, 'utf-8') : null;
|
|
62
|
+
if (before === content)
|
|
63
|
+
return null;
|
|
64
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
65
|
+
(0, node_fs_1.writeFileSync)(path, content, 'utf-8');
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
27
68
|
/**
|
|
28
|
-
*
|
|
69
|
+
* Дописать указатель в AGENTS.md: только в конец и только один раз по маркеру.
|
|
70
|
+
* Файл принадлежит пользователю, там его правила, переписывать его мы не вправе.
|
|
71
|
+
* CLAUDE.md не трогаем вовсе: Claude Code и так читает .claude/skills.
|
|
72
|
+
*/
|
|
73
|
+
function appendPointer(base) {
|
|
74
|
+
const path = (0, node_path_1.join)(base, 'AGENTS.md');
|
|
75
|
+
const before = (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, 'utf-8') : null;
|
|
76
|
+
if (before?.includes(POINTER_MARKER))
|
|
77
|
+
return null;
|
|
78
|
+
(0, node_fs_1.writeFileSync)(path, before ? `${before.replace(/\s*$/, '')}\n\n${POINTER}` : POINTER, 'utf-8');
|
|
79
|
+
return path;
|
|
80
|
+
}
|
|
81
|
+
/** Разложить инструкцию по папкам, где её найдёт агент. Отдаёт тронутые файлы. */
|
|
82
|
+
function install(base, global) {
|
|
83
|
+
const skill = skillSource();
|
|
84
|
+
const touched = [];
|
|
85
|
+
for (const dir of global ? GLOBAL_SKILL_DIRS : SKILL_DIRS) {
|
|
86
|
+
const path = writeIfChanged((0, node_path_1.join)(base, ...dir, 'xflow', 'SKILL.md'), skill);
|
|
87
|
+
if (path)
|
|
88
|
+
touched.push(path);
|
|
89
|
+
}
|
|
90
|
+
// Правило Cursor и указатель кладём только рядом с проектом: глобальных
|
|
91
|
+
// аналогов у них нет, а в домашней папке их никто не читает.
|
|
92
|
+
if (!global) {
|
|
93
|
+
const rule = writeIfChanged((0, node_path_1.join)(base, ...CURSOR_RULE), cursorRule(skill));
|
|
94
|
+
if (rule)
|
|
95
|
+
touched.push(rule);
|
|
96
|
+
const pointer = appendPointer(base);
|
|
97
|
+
if (pointer)
|
|
98
|
+
touched.push(pointer);
|
|
99
|
+
}
|
|
100
|
+
return touched;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Установка попутно, из `init` и `link`.
|
|
29
104
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
105
|
+
* Отдельной командой до этого доходили единицы, и агент оставался без знания о
|
|
106
|
+
* платформе — той самой, на которой ему предстоит работать. При этом инструкция
|
|
107
|
+
* не то, ради чего запускали команду, поэтому её сбой не роняет привязку папки.
|
|
32
108
|
*/
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (const root of ROOTS) {
|
|
38
|
-
const target = (0, node_path_1.join)(base, ...root, 'xflow', 'SKILL.md');
|
|
39
|
-
const before = (0, node_fs_1.existsSync)(target) ? (0, node_fs_1.readFileSync)(target, 'utf-8') : null;
|
|
40
|
-
if (before === content) {
|
|
41
|
-
(0, ui_1.note)((0, ui_1.dim)(` уже актуален: ${target}`));
|
|
42
|
-
continue;
|
|
109
|
+
function installSkillQuietly(base) {
|
|
110
|
+
try {
|
|
111
|
+
if (install(base, false).length > 0) {
|
|
112
|
+
(0, ui_1.note)((0, ui_1.dim)(' Инструкция о платформе разложена для ИИ-агента'));
|
|
43
113
|
}
|
|
44
|
-
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
|
|
45
|
-
(0, node_fs_1.writeFileSync)(target, content, 'utf-8');
|
|
46
|
-
(0, ui_1.out)(`${before === null ? 'создан' : 'обновлён'}: ${target}`);
|
|
47
114
|
}
|
|
115
|
+
catch {
|
|
116
|
+
(0, ui_1.note)((0, ui_1.dim)(' Инструкцию для ИИ-агента положить не удалось: xflow skills'));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function skills(args) {
|
|
120
|
+
const global = (0, args_1.flagBool)(args, 'global');
|
|
121
|
+
const touched = install(global ? (0, node_os_1.homedir)() : process.cwd(), global);
|
|
122
|
+
for (const path of touched)
|
|
123
|
+
(0, ui_1.out)(path);
|
|
124
|
+
if (touched.length === 0)
|
|
125
|
+
(0, ui_1.note)((0, ui_1.dim)(' всё уже актуально'));
|
|
48
126
|
(0, ui_1.ok)(global ? 'Скилл xflow доступен во всех проектах' : 'Скилл xflow на месте');
|
|
49
127
|
(0, ui_1.note)((0, ui_1.dim)(' После обновления CLI повторите команду: скилл обновляется вместе с ним'));
|
|
50
128
|
}
|
package/dist/commands/sources.js
CHANGED
|
@@ -183,6 +183,8 @@ async function status() {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
(0, ui_1.out)('');
|
|
186
|
-
(0, ui_1.out)(
|
|
187
|
-
(0, ui_1.out)(
|
|
186
|
+
(0, ui_1.out)(`Собранная версия: ${card.dev_deploy_id ?? '— (сборки нет)'}`);
|
|
187
|
+
(0, ui_1.out)(`Видят посетители: ${card.live_deploy_id ?? '— (не публиковался)'}`);
|
|
188
|
+
(0, ui_1.out)('');
|
|
189
|
+
(0, ui_1.out)(card.project_url);
|
|
188
190
|
}
|
package/dist/help.js
CHANGED
|
@@ -18,8 +18,9 @@ ${(0, ui_1.bold)('Начало работы')}
|
|
|
18
18
|
xflow login вход через браузер
|
|
19
19
|
xflow init [папка] новый проект из шаблона платформы
|
|
20
20
|
xflow templates какие шаблоны доступны
|
|
21
|
-
xflow link <id> связать текущую папку с
|
|
22
|
-
xflow skills [--global]
|
|
21
|
+
xflow link <id> связать текущую папку с проектом (id: xflow projects list)
|
|
22
|
+
xflow skills [--global] обновить инструкцию о платформе для ИИ-агента
|
|
23
|
+
xflow mcp install доступ к платформе для агента без терминала
|
|
23
24
|
|
|
24
25
|
${(0, ui_1.bold)('Код')}
|
|
25
26
|
xflow status что на сервере и чем отличается локальная копия
|
|
@@ -28,10 +29,9 @@ ${(0, ui_1.bold)('Код')}
|
|
|
28
29
|
забрать исходники (по умолчанию последнюю ревизию)
|
|
29
30
|
|
|
30
31
|
${(0, ui_1.bold)('Выкладка')}
|
|
31
|
-
xflow deploy [--no-push]
|
|
32
|
-
отправить код, собрать локально и выложить на dev
|
|
32
|
+
xflow deploy [--no-push] отправить код, собрать на платформе и выложить на dev
|
|
33
33
|
xflow publish показать dev-версию посетителям
|
|
34
|
-
xflow rollback <номер версии> вернуть
|
|
34
|
+
xflow rollback <номер версии> вернуть проект на прошлую версию
|
|
35
35
|
xflow deployments история версий
|
|
36
36
|
|
|
37
37
|
${(0, ui_1.bold)('Функции')}
|
|
@@ -57,7 +57,6 @@ ${(0, ui_1.bold)('Справочное')}
|
|
|
57
57
|
xflow logs [--limit N] ошибки выложенного приложения в браузере
|
|
58
58
|
xflow projects list проекты организации
|
|
59
59
|
xflow projects get [id] карточка проекта
|
|
60
|
-
xflow open [--live] открыть приложение в браузере
|
|
61
60
|
xflow whoami чей ключ и что он может
|
|
62
61
|
xflow logout забыть ключ
|
|
63
62
|
|
|
@@ -162,22 +161,46 @@ ${(0, ui_1.bold)('xflow functions deploy')} затронутых функций.
|
|
|
162
161
|
Печатает статус, время ответа и тело. Ненулевой код возврата на статусе 4xx и
|
|
163
162
|
5xx: в CI такой вызов должен ронять шаг. Причину падения показывает
|
|
164
163
|
${(0, ui_1.bold)('xflow functions logs <имя>')}.`,
|
|
164
|
+
mcp: `${(0, ui_1.bold)('xflow mcp install')} — подключить агента к платформе напрямую
|
|
165
|
+
|
|
166
|
+
Прописывает сервер MCP в конфигурацию клиента, у которого есть своя команда
|
|
167
|
+
подключения: сегодня это Claude Code, остальным команда покажет, что вписать
|
|
168
|
+
руками. После этого агент управляет платформой без терминала: смотрит базу,
|
|
169
|
+
применяет миграции, выкладывает и вызывает функции, ставит расписания, читает
|
|
170
|
+
логи. Команды остаются: код, сборку и выкладку по-прежнему делает CLI, гнать
|
|
171
|
+
исходники через MCP значит забивать контекст агента и тратить ваши деньги.
|
|
172
|
+
|
|
173
|
+
Ключ доступа команда берёт из вашего входа и на экран не выводит: обычно её
|
|
174
|
+
запускает сам агент, а всё напечатанное попадает в его контекст и в историю
|
|
175
|
+
переписки.
|
|
176
|
+
|
|
177
|
+
--show-token всё же показать ключ (не под агентом)
|
|
178
|
+
|
|
179
|
+
Ключ один на человека в организации, и область у него не сужается: агент видит
|
|
180
|
+
все проекты организации и называет нужный сам. Проект он берёт из аргумента
|
|
181
|
+
${(0, ui_1.bold)('project_id')}, а не из вашей папки — папки он не видит.`,
|
|
165
182
|
skills: `${(0, ui_1.bold)('xflow skills')} — инструкция о платформе для ИИ-агента
|
|
166
183
|
|
|
167
|
-
|
|
168
|
-
и публиковать, откуда брать компоненты дизайн-системы, где смотреть
|
|
169
|
-
|
|
170
|
-
|
|
184
|
+
Про XFlow агенту знать неоткуда: в его обучении платформы нет. Инструкция объясняет,
|
|
185
|
+
как выкладывать и публиковать, откуда брать компоненты дизайн-системы, где смотреть
|
|
186
|
+
ошибки прода. ${(0, ui_1.bold)('init')} и ${(0, ui_1.bold)('link')} раскладывают её сами — эта команда нужна, чтобы
|
|
187
|
+
обновить её после обновления CLI: они едут одной версией.
|
|
171
188
|
|
|
172
|
-
|
|
173
|
-
${(0, ui_1.bold)('.claude/skills/xflow/')} читает Claude Code, ${(0, ui_1.bold)('.agents/skills/xflow/')} — Codex и OpenClaw.
|
|
189
|
+
Формат общий (agentskills.io), а папки у инструментов разные, поэтому кладём во все:
|
|
174
190
|
|
|
175
|
-
|
|
191
|
+
.claude/skills/xflow/SKILL.md Claude Code
|
|
192
|
+
.agents/skills/xflow/SKILL.md Codex, OpenClaw
|
|
193
|
+
.cursor/rules/xflow.mdc Cursor: общий формат он не читает
|
|
176
194
|
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
--global то же в домашней папке, тогда скилл виден во всех проектах.
|
|
196
|
+
Там добавляется ~/.codex/skills: общий каталог Codex смотрит
|
|
197
|
+
только внутри репозитория. Правило Cursor и AGENTS.md остаются
|
|
198
|
+
проектными, глобальных аналогов у них нет
|
|
179
199
|
|
|
180
|
-
|
|
200
|
+
Плюс несколько строк-указателей в ${(0, ui_1.bold)('AGENTS.md')}. Скилл подхватывается лениво, только
|
|
201
|
+
когда описание совпало с задачей, и на «добавь таблицу клиентов» он не сработает.
|
|
202
|
+
AGENTS.md агент читает всегда, поэтому указатель дописывается туда — в конец файла и
|
|
203
|
+
один раз. ${(0, ui_1.bold)('CLAUDE.md')} не трогаем: Claude Code и так читает .claude/skills.`,
|
|
181
204
|
push: `${(0, ui_1.bold)('xflow push')} — отправить исходники
|
|
182
205
|
|
|
183
206
|
Уходит вся рабочая копия целиком, одной ревизией. Не отправляются: node_modules,
|
|
@@ -202,18 +225,20 @@ ${(0, ui_1.bold)('AGENTS.md')} и ${(0, ui_1.bold)('CLAUDE.md')} команда
|
|
|
202
225
|
--force перезаписать папку целиком`,
|
|
203
226
|
deploy: `${(0, ui_1.bold)('xflow deploy')} — собрать и выложить
|
|
204
227
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
и dist).
|
|
228
|
+
Два шага: отправка исходников и сборка на платформе. Команда сборки и каталог
|
|
229
|
+
результата берутся из xflow.json (по умолчанию npm run build и dist).
|
|
208
230
|
|
|
209
|
-
--no-push не отправлять исходники,
|
|
210
|
-
--skip-build не собирать, взять готовый каталог сборки
|
|
231
|
+
--no-push не отправлять исходники, собрать из последней серверной ревизии
|
|
211
232
|
--force разрешить перезапись серверной ревизии при отправке
|
|
212
233
|
|
|
213
|
-
|
|
214
|
-
|
|
234
|
+
Собирает платформа, в чистой песочнице с одинаковой для всех версией Node,
|
|
235
|
+
поэтому «у меня работало» больше не зависит от вашей машины. Перед сборкой
|
|
236
|
+
проект проверяется на соответствие шаблону: несоответствия печатаются списком,
|
|
237
|
+
и сборка не запускается вовсе.
|
|
215
238
|
|
|
216
|
-
|
|
239
|
+
Собранную версию видно на странице проекта, ссылку CLI печатает. Посетителям — после
|
|
240
|
+
${(0, ui_1.bold)('xflow publish')}. Адрес самой сборки не печатается: в нём зашит номер версии, и
|
|
241
|
+
после следующей публикации такая ссылка молча отдаёт старую копию.`,
|
|
217
242
|
init: `${(0, ui_1.bold)('xflow init')} [папка] — новый проект
|
|
218
243
|
|
|
219
244
|
Разворачивает шаблон платформы: React на Vite, Tailwind, набор компонентов
|
|
@@ -233,7 +258,7 @@ ${(0, ui_1.bold)('AGENTS.md')} и ${(0, ui_1.bold)('CLAUDE.md')} команда
|
|
|
233
258
|
как подобрать его командой link.`,
|
|
234
259
|
rollback: `${(0, ui_1.bold)('xflow rollback')} <номер версии> — вернуть прошлую сборку
|
|
235
260
|
|
|
236
|
-
Переключает
|
|
261
|
+
Переключает проект на выбранную сборку. Возвращается только она: исходники
|
|
237
262
|
остаются на своей ревизии, и вернуть их — отдельное решение
|
|
238
263
|
(${(0, ui_1.bold)('xflow pull --revision N --into ./старая-версия')}).
|
|
239
264
|
|
package/dist/template.js
CHANGED
|
@@ -20,7 +20,7 @@ const README = `# %NAME%
|
|
|
20
20
|
\`\`\`bash
|
|
21
21
|
npm install # зависимости
|
|
22
22
|
npm run dev # разработка на localhost:3000
|
|
23
|
-
xflow deploy # отправить
|
|
23
|
+
xflow deploy # отправить код и собрать на платформе
|
|
24
24
|
xflow publish # показать dev-версию посетителям
|
|
25
25
|
\`\`\`
|
|
26
26
|
|
package/dist/tree.js
CHANGED
|
@@ -4,7 +4,6 @@ exports.loadIgnoreRules = loadIgnoreRules;
|
|
|
4
4
|
exports.collectFiles = collectFiles;
|
|
5
5
|
exports.treeHash = treeHash;
|
|
6
6
|
exports.heaviest = heaviest;
|
|
7
|
-
exports.collectArtifact = collectArtifact;
|
|
8
7
|
const node_crypto_1 = require("node:crypto");
|
|
9
8
|
const node_fs_1 = require("node:fs");
|
|
10
9
|
const node_path_1 = require("node:path");
|
|
@@ -134,28 +133,3 @@ function treeHash(files) {
|
|
|
134
133
|
function heaviest(files, count = 5) {
|
|
135
134
|
return [...files].sort((a, b) => b.content.length - a.content.length).slice(0, count);
|
|
136
135
|
}
|
|
137
|
-
/** Собрать дерево из каталога сборки: пути внутри артефакта считаются от него. */
|
|
138
|
-
function collectArtifact(dir) {
|
|
139
|
-
const files = [];
|
|
140
|
-
const walk = (current) => {
|
|
141
|
-
for (const entry of (0, node_fs_1.readdirSync)(current, { withFileTypes: true })) {
|
|
142
|
-
const full = (0, node_path_1.join)(current, entry.name);
|
|
143
|
-
if (entry.isSymbolicLink())
|
|
144
|
-
continue;
|
|
145
|
-
if (entry.isDirectory()) {
|
|
146
|
-
walk(full);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (!entry.isFile())
|
|
150
|
-
continue;
|
|
151
|
-
files.push({
|
|
152
|
-
path: (0, node_path_1.relative)(dir, full).split(node_path_1.sep).join('/'),
|
|
153
|
-
content: (0, node_fs_1.readFileSync)(full),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
if (!(0, node_fs_1.existsSync)(dir) || !(0, node_fs_1.statSync)(dir).isDirectory())
|
|
158
|
-
return files;
|
|
159
|
-
walk(dir);
|
|
160
|
-
return files;
|
|
161
|
-
}
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Ответы публичного API, которые разбирает CLI.
|
|
4
|
+
*
|
|
5
|
+
* Адресов собранного приложения здесь нет: платформа их наружу не отдаёт. В них
|
|
6
|
+
* зашит номер версии, и после следующей публикации такая ссылка продолжает
|
|
7
|
+
* отвечать старой копией, а не ломается. Версии называем номерами, ссылка одна
|
|
8
|
+
* и ведёт на страницу проекта.
|
|
9
|
+
*/
|
|
3
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
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.
|
|
9
|
+
exports.CLI_VERSION = '0.1.7';
|
|
10
10
|
/** Адрес платформы по умолчанию. Переопределяется XFLOW_API_URL и полем `api` в xflow.json. */
|
|
11
11
|
exports.DEFAULT_API_URL = 'https://app.getxflow.com';
|
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@getxflow/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI платформы XFlow: синхронизация кода, деплой и публикация приложений",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"engines": {
|
|
7
|
-
"node": ">=20"
|
|
8
|
-
},
|
|
9
|
-
"bin": {
|
|
10
|
-
"xflow": "dist/bin.js"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"dist",
|
|
14
|
-
"skills"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsc -p tsconfig.json"
|
|
18
|
-
},
|
|
19
|
-
"publishConfig": {
|
|
20
|
-
"access": "public"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@getxflow/cli",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "CLI платформы XFlow: синхронизация кода, деплой и публикация приложений",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"xflow": "dist/bin.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"skills"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/skills/xflow/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xflow
|
|
3
|
-
description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use
|
|
3
|
+
description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use whenever the project root has xflow.json or VITE_XFLOW_* variables, and for any task that touches deployment, publishing, rollback, source sync, the project database or SQL migrations, cloud functions, schedules, environment variables and secrets, production errors and logs, or UI built on the platform design system.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# XFlow
|
|
@@ -22,19 +22,68 @@ contains the fix. Help output is in Russian.
|
|
|
22
22
|
2. `npm run typecheck` for a two-second type check (older projects may not have the
|
|
23
23
|
script, then `npx tsc --noEmit`).
|
|
24
24
|
3. `npm run build` if the change is substantial, before deploying.
|
|
25
|
-
4. `xflow deploy` sends the sources
|
|
26
|
-
|
|
27
|
-
5.
|
|
25
|
+
4. `xflow deploy` sends the sources and builds them on the platform, printing each
|
|
26
|
+
phase and the six-digit number of the version it built.
|
|
27
|
+
5. Give the user the project link the CLI printed and let them look. Do not open a
|
|
28
|
+
browser for them.
|
|
28
29
|
6. `xflow publish` makes that same version visible to visitors.
|
|
29
30
|
|
|
30
31
|
The split is deliberate: shipping a build and showing it are two separate decisions.
|
|
31
32
|
Until `publish` runs, visitors keep seeing the previous version.
|
|
32
33
|
|
|
33
34
|
Rolling back: `xflow deployments` lists the version history, `xflow rollback <id>`
|
|
34
|
-
points the
|
|
35
|
-
|
|
36
|
-
The
|
|
37
|
-
|
|
35
|
+
points the project back at an earlier build. Sources stay on their own revision.
|
|
36
|
+
|
|
37
|
+
**The only link you give a person is the project page**, `https://app.getxflow.com/projects/<id>`,
|
|
38
|
+
which the CLI prints for you. Refer to builds by their number ("version 481203 is built,
|
|
39
|
+
092399 is what visitors see"), never by address. The platform does not hand out build
|
|
40
|
+
addresses and neither should you: a build address has the version number baked into it,
|
|
41
|
+
and after the next publish it does not break, it keeps answering with the old copy. Anyone
|
|
42
|
+
holding that link then stares at a frozen app and concludes the changes never shipped. The
|
|
43
|
+
project page always shows the current state, and every version is reachable from it.
|
|
44
|
+
|
|
45
|
+
The platform builds the project itself, in a clean sandbox with one Node version for
|
|
46
|
+
everyone, and serves the result as static files. Nothing is built on your machine for
|
|
47
|
+
deployment, so a local `npm run build` is only a fast way to see errors early.
|
|
48
|
+
|
|
49
|
+
## Build gate
|
|
50
|
+
|
|
51
|
+
Before the sandbox starts, the platform checks the sources against the template. Every
|
|
52
|
+
rule below blocks the build, and the whole list of violations comes back at once, with
|
|
53
|
+
files and line numbers. Nothing is charged for a rejected attempt: the sandbox never
|
|
54
|
+
starts. Write code that already satisfies these rules instead of learning them from
|
|
55
|
+
rejections.
|
|
56
|
+
|
|
57
|
+
Build setup:
|
|
58
|
+
|
|
59
|
+
- `package.json` with the build script named in `xflow.json` (`npm run build` by default).
|
|
60
|
+
- Vite: a `vite.config.*` and `vite` in dependencies.
|
|
61
|
+
- No server frameworks: `next`, `nuxt`, `remix`, `@sveltejs/kit`, `astro`.
|
|
62
|
+
- `index.html` in the root, `src/main.tsx` as the entry point.
|
|
63
|
+
- Application code under `src/`. Root `app/`, `pages/`, `next/` are rejected.
|
|
64
|
+
|
|
65
|
+
Platform contract, checked across all of `src/`:
|
|
66
|
+
|
|
67
|
+
- Call cloud functions through `xflow.functions.invoke`, never through a hardcoded
|
|
68
|
+
`*.yandexcloud.net` URL: the address changes and the app breaks silently.
|
|
69
|
+
- No API keys or tokens in the source: they end up in the bundle. Put the call in a
|
|
70
|
+
cloud function and the key in project secrets.
|
|
71
|
+
- No server modules (`fs`, `express`, `http`, `child_process`): there is no server runtime.
|
|
72
|
+
|
|
73
|
+
Interface rules, checked outside `src/components/ui` and `src/components/blocks`:
|
|
74
|
+
|
|
75
|
+
- No `alert()`, `confirm()`, `prompt()`. Use the Dialog and Toast components.
|
|
76
|
+
- No `console.log`. Deployed apps have a public console, and forgotten debugging prints
|
|
77
|
+
user data into it. `console.error` and `console.warn` are fine, they reach the project
|
|
78
|
+
logs.
|
|
79
|
+
- No inline styles with literal values (`style={{ color: '#fff' }}`). Computed styles
|
|
80
|
+
(a drag transform, a progress width) are fine, Tailwind cannot express them.
|
|
81
|
+
- No hex colors or Tailwind palette classes (`text-gray-500`): use the theme tokens.
|
|
82
|
+
Charts are exempt, they need real colors.
|
|
83
|
+
- No importing a `@/components/ui/*` component that does not exist in the project.
|
|
84
|
+
- A library that needs a provider (`@tanstack/react-query`, `react-redux`, `sonner`,
|
|
85
|
+
`react-hot-toast`, `react-dnd`) must have it mounted in `App.tsx`. Missing providers
|
|
86
|
+
build fine and give visitors a white screen.
|
|
38
87
|
|
|
39
88
|
## Cloud functions
|
|
40
89
|
|
|
@@ -78,6 +127,18 @@ in filename order by `xflow db migrate`. `xflow db status` shows what is applied
|
|
|
78
127
|
waits. History lives in the database itself, so an already applied file is never re-run and
|
|
79
128
|
editing it changes nothing: write a new migration instead.
|
|
80
129
|
|
|
130
|
+
The browser never reaches the database directly. The app reads and writes through a cloud
|
|
131
|
+
function, and inside the handler the connection string is already there:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
const { Client } = require('pg')
|
|
135
|
+
const db = new Client({ connectionString: process.env.DATABASE_URL })
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The platform passes `DATABASE_URL` only to functions that mention it, and sets the project
|
|
139
|
+
schema on every connection, so plain table names (`select * from tasks`) hit your project.
|
|
140
|
+
You never write that variable yourself: `xflow env set DATABASE_URL=...` is refused.
|
|
141
|
+
|
|
81
142
|
The platform keeps no database history and no backups. Anything that destroys data
|
|
82
143
|
(`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) is refused
|
|
83
144
|
unless you pass `--allow-destructive`, and with that flag the affected tables are dumped
|
|
@@ -97,6 +158,21 @@ Fetch their work next to yours (`xflow pull --into ./server-copy`), merge it loc
|
|
|
97
158
|
then push again. `--force` destroys their work: a last resort, not a way around the
|
|
98
159
|
error.
|
|
99
160
|
|
|
161
|
+
## Direct access without the terminal
|
|
162
|
+
|
|
163
|
+
The platform also exposes an MCP server, connected with `xflow mcp install`. When its tools
|
|
164
|
+
are available, prefer them for control-plane work: project state, database schema and
|
|
165
|
+
read-only queries, migrations, function logs and invocations, schedules, environment
|
|
166
|
+
variables, versions, publish and rollback. They answer with aggregates and say explicitly
|
|
167
|
+
when a result is truncated, which parsing terminal output does not.
|
|
168
|
+
|
|
169
|
+
Code never travels through those tools. Sending sources and deploying functions stay in
|
|
170
|
+
the CLI (`xflow push`, `xflow functions deploy`): pulling a repository through tool calls
|
|
171
|
+
burns the user's tokens for nothing. Building is available through the tools, because it
|
|
172
|
+
runs from the revision already stored on the server: `deployments action=build` starts it
|
|
173
|
+
and answers immediately, `action=status` reports the phase. A build takes minutes, so
|
|
174
|
+
never expect the starting call to return a finished version.
|
|
175
|
+
|
|
100
176
|
## Do not
|
|
101
177
|
|
|
102
178
|
- Edit `xflow.json` by hand: the CLI writes it.
|
package/dist/open-url.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.openUrl = openUrl;
|
|
4
|
-
const node_child_process_1 = require("node:child_process");
|
|
5
|
-
/**
|
|
6
|
-
* Открыть адрес в браузере пользователя.
|
|
7
|
-
*
|
|
8
|
-
* Best-effort: на сервере без графики открывать нечего, и это не ошибка —
|
|
9
|
-
* вызывающий в любом случае печатает ссылку рядом.
|
|
10
|
-
*/
|
|
11
|
-
function openUrl(url) {
|
|
12
|
-
try {
|
|
13
|
-
const command = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
14
|
-
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
15
|
-
const child = (0, node_child_process_1.spawn)(command, args, { stdio: 'ignore', detached: true });
|
|
16
|
-
child.on('error', () => {
|
|
17
|
-
// Браузера нет — молча, ссылка уже показана.
|
|
18
|
-
});
|
|
19
|
-
child.unref();
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
// То же самое: открыть не удалось, но команда не про это.
|
|
23
|
-
}
|
|
24
|
-
}
|