@getxflow/cli 0.1.5 → 0.1.8

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
@@ -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
- constructor(message, code, status, hint) {
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
- 'open',
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);
@@ -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
- const start = await (0, api_1.apiJson)(client, '/api/v1/auth/device', { method: 'POST' });
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)(` Страница входа: ${start.verification_uri}`);
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
- (0, ui_1.step)('Открываю браузер, код там надо сверить с показанным выше');
40
- (0, open_url_1.openUrl)(start.verification_uri_complete);
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
  }
@@ -83,13 +87,24 @@ function logout() {
83
87
  (0, ui_1.warn)(`Сохранённого ключа для ${apiUrl} нет`);
84
88
  }
85
89
  }
90
+ /** Приписка к тарифу. У живой подписки её нет: молчание и значит «всё в порядке». */
91
+ const SUBSCRIPTION_STATE = {
92
+ past_due: ', период кончился',
93
+ suspended: ', работа приостановлена',
94
+ terminated: ', подписка прекращена',
95
+ };
96
+ const QUOTA_LABELS = {
97
+ projects: 'проекты',
98
+ functions: 'функции',
99
+ developers: 'разработчики',
100
+ members: 'участники',
101
+ };
86
102
  async function whoami() {
87
103
  const config = localConfig();
88
104
  const client = (0, session_1.connect)(config);
89
105
  const me = await (0, api_1.apiJson)(client, '/api/v1/me');
90
106
  (0, ui_1.out)(`Организация: ${me.organization.name ?? me.organization.id}`);
91
107
  (0, ui_1.out)(`Ключ: xfk_${me.key.prefix}… (${me.key.scopes.join(', ')})`);
92
- (0, ui_1.out)(`Проект ключа: ${me.key.project_id ?? 'вся организация'}`);
93
108
  (0, ui_1.out)(`Платформа: ${client.apiUrl}`);
94
109
  if (process.env.XFLOW_TOKEN) {
95
110
  (0, ui_1.note)((0, ui_1.dim)(' Ключ взят из XFLOW_TOKEN, сохранённый в ~/.xflow не используется'));
@@ -99,13 +114,14 @@ async function whoami() {
99
114
  }
100
115
  if (me.billing) {
101
116
  (0, ui_1.out)('');
102
- (0, ui_1.out)(`Баланс: ${me.billing.balance ?? '—'} (статус ${me.billing.status})`);
117
+ (0, ui_1.out)(`Тариф: ${me.billing.plan_title}${SUBSCRIPTION_STATE[me.billing.status] ?? ''}`);
118
+ (0, ui_1.out)(`Баланс: ${me.billing.balance ?? '—'}`);
103
119
  }
104
120
  if (me.quotas) {
105
121
  (0, ui_1.out)('');
106
- (0, ui_1.out)('Квоты:');
122
+ (0, ui_1.out)('Лимиты тарифа:');
107
123
  for (const [name, value] of Object.entries(me.quotas)) {
108
- (0, ui_1.out)(` ${name}: ${value.used} из ${value.limit}`);
124
+ (0, ui_1.out)(` ${QUOTA_LABELS[name] ?? name}: ${value.used} из ${value.limit}`);
109
125
  }
110
126
  }
111
127
  }
@@ -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
- * Через оболочку: команда задана разработчиком в xflow.json (`npm run build`),
25
- * и разбирать её самим значит спорить с тем, что он написал. Вывод идёт прямо
26
- * в терминал, поэтому кодировки и прогресс-бары выглядят как обычно.
24
+ * Печатаем весь список: правила известны заранее (скилл xflow), а узнавать их по
25
+ * одному через повторные запуски значит терять круг на каждое нарушение.
27
26
  */
28
- function runBuild(command, cwd) {
29
- return new Promise((done, reject) => {
30
- const child = (0, node_child_process_1.spawn)(command, { cwd, shell: true, stdio: 'inherit' });
31
- child.on('error', (e) => reject(new errors_1.CliError(`Не удалось запустить сборку: ${e.message}`)));
32
- child.on('close', (code) => {
33
- if (code === 0) {
34
- done();
35
- return;
36
- }
37
- reject(new errors_1.CliError(`Сборка завершилась с кодом ${code}`, 'Деплой остановлен: на сервер уходит только то, что собралось'));
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), деплой пойдёт от ревизии ${revision}`);
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
- if (!(0, args_1.flagBool)(args, 'skip-build')) {
59
- // Перед сборкой, а не после: адреса функций попадают в бандл на этом шаге.
60
- await (0, functions_1.refreshFunctionsEnv)(root, client, config.projectId);
61
- (0, ui_1.step)(`Сборка: ${buildCommand}`);
62
- await runBuild(buildCommand, root);
63
- }
64
- const files = (0, tree_1.collectArtifact)(buildDir);
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
- if (!files.some((f) => f.path === 'index.html')) {
71
- throw new errors_1.CliError(`В корне ${buildDir} нет index.html`, 'Похоже на серверную сборку (SSR), а платформа раздаёт статику: соберите приложение как SPA');
81
+ catch (e) {
82
+ if (e instanceof api_1.ApiError && e.issues?.length)
83
+ reportIssues(e);
84
+ throw e;
72
85
  }
73
- const archive = (0, zip_1.zipCreate)(files);
74
- if (archive.length > MAX_ARTIFACT_BYTES) {
75
- throw new errors_1.CliError(`Артефакт ${(0, ui_1.formatBytes)(archive.length)}, потолок ${(0, ui_1.formatBytes)(MAX_ARTIFACT_BYTES)}`, 'Уберите из сборки исходные карты (sourcemaps) и неиспользуемые ассеты');
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.step)(`Выкладываю ${files.length} файлов (${(0, ui_1.formatBytes)(archive.length)})`);
78
- const result = await (0, api_1.apiUpload)(client, `/api/v1/projects/${config.projectId}/deploy`, archive, { 'X-Revision': String(revision) });
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.url);
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-адрес переключён на версию ${result.deploy_id}`);
105
- (0, ui_1.out)(result.url);
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
  }
@@ -39,21 +39,23 @@ function sourceFiles(dir, found = []) {
39
39
  }
40
40
  /** Какие переменные упоминают функции проекта, и в какой из них. */
41
41
  function referencedByFunctions(root) {
42
- const references = new Map();
42
+ const needed = new Map();
43
+ const provided = new Map();
43
44
  for (const file of sourceFiles((0, node_path_1.join)(root, FUNCTIONS_DIR))) {
44
45
  const functionName = file.slice((0, node_path_1.join)(root, FUNCTIONS_DIR).length + 1).split(/[\\/]/)[0];
45
46
  const code = (0, node_fs_1.readFileSync)(file, 'utf-8');
46
47
  for (const match of code.matchAll(ENV_REFERENCE)) {
47
48
  const name = match[1] || match[2];
48
- if (!name || PROVIDED.has(name))
49
+ if (!name)
49
50
  continue;
50
- const users = references.get(name) ?? [];
51
+ const target = PROVIDED.has(name) ? provided : needed;
52
+ const users = target.get(name) ?? [];
51
53
  if (!users.includes(functionName))
52
54
  users.push(functionName);
53
- references.set(name, users);
55
+ target.set(name, users);
54
56
  }
55
57
  }
56
- return references;
58
+ return { needed, provided };
57
59
  }
58
60
  async function fetchVariables() {
59
61
  const { root, config } = (0, config_1.requireProject)();
@@ -79,14 +81,23 @@ async function envList() {
79
81
  */
80
82
  async function envCheck() {
81
83
  const { names, root } = await fetchVariables();
82
- const referenced = referencedByFunctions(root);
83
- if (referenced.size === 0) {
84
+ const { needed, provided } = referencedByFunctions(root);
85
+ if (needed.size === 0 && provided.size === 0) {
84
86
  (0, ui_1.note)('Функции проекта не читают переменных окружения');
85
87
  return;
86
88
  }
89
+ // Платформенные показываем отдельной таблицей, а не молчим о них: раньше
90
+ // функция, читающая только DATABASE_URL, получала ответ «переменных не
91
+ // читают», то есть команда делала ложное утверждение о собственном коде.
92
+ if (provided.size > 0) {
93
+ (0, ui_1.out)((0, ui_1.bold)('Даёт платформа:'));
94
+ (0, ui_1.table)([...provided.entries()].sort().map(([name, users]) => [name, users.join(', ')]));
95
+ if (needed.size > 0)
96
+ (0, ui_1.out)('');
97
+ }
87
98
  const missing = [];
88
99
  const present = [];
89
- for (const [name, users] of [...referenced.entries()].sort()) {
100
+ for (const [name, users] of [...needed.entries()].sort()) {
90
101
  const row = [name, users.join(', ')];
91
102
  if (names.has(name))
92
103
  present.push(row);
@@ -94,7 +105,7 @@ async function envCheck() {
94
105
  missing.push(row);
95
106
  }
96
107
  if (present.length > 0) {
97
- (0, ui_1.out)((0, ui_1.bold)('На платформе:'));
108
+ (0, ui_1.out)((0, ui_1.bold)('Записаны на платформе:'));
98
109
  (0, ui_1.table)(present);
99
110
  }
100
111
  if (missing.length === 0) {
@@ -122,7 +133,7 @@ async function envSet(args) {
122
133
  (0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} записана (${result.scope === 'project' ? 'только этот проект' : 'вся организация'})`);
123
134
  // Значение попадает в функцию на выкатке, а не в момент записи: пока функцию
124
135
  // не передеплоили, в её окружении лежит прежнее.
125
- const users = referencedByFunctions(root).get(name);
136
+ const users = referencedByFunctions(root).needed.get(name);
126
137
  if (users && users.length > 0) {
127
138
  (0, ui_1.note)((0, ui_1.dim)(` Чтобы значение доехало, перевыложите: xflow functions deploy ${users.join(' && xflow functions deploy ')}`));
128
139
  }
@@ -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 ?? fn.invoke_url ?? '',
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.live_url ? 'опубликован' : p.dev_url ? 'только dev' : 'без сборки',
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)(`dev: ${card.dev_url ?? '—'}`);
166
- (0, ui_1.out)(`live: ${card.live_url ?? '— (не публиковался)'}`);
167
- (0, ui_1.out)(`база: ${card.database ? `${card.database.name} (${card.database.schema})` : '—'}`);
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
- }
@@ -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 ROOTS = [
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
- * Файлы AGENTS.md и CLAUDE.md не трогаем: они принадлежат пользователю, а наше
31
- * знание живёт в своей папке и переустанавливается поверх без спора за файл.
105
+ * Отдельной командой до этого доходили единицы, и агент оставался без знания о
106
+ * платформе той самой, на которой ему предстоит работать. При этом инструкция
107
+ * не то, ради чего запускали команду, поэтому её сбой не роняет привязку папки.
32
108
  */
33
- function skills(args) {
34
- const content = skillSource();
35
- const global = (0, args_1.flagBool)(args, 'global');
36
- const base = global ? (0, node_os_1.homedir)() : process.cwd();
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
  }
@@ -183,6 +183,8 @@ async function status() {
183
183
  }
184
184
  }
185
185
  (0, ui_1.out)('');
186
- (0, ui_1.out)(`dev: ${card.dev_url ?? '—'}`);
187
- (0, ui_1.out)(`live: ${card.live_url ?? '— (не публиковался)'}`);
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] [--skip-build]
32
- отправить код, собрать локально и выложить на dev
32
+ xflow deploy [--no-push] отправить код, собрать на платформе и выложить на dev
33
33
  xflow publish показать dev-версию посетителям
34
- xflow rollback <номер версии> вернуть dev-адрес на прошлую версию
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
- Кладёт файл в формате Agent Skills (agentskills.io): что такое XFlow, как выкладывать
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
- --global положить в домашнюю папку, тогда скилл виден во всех проектах
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
- ${(0, ui_1.bold)('AGENTS.md')} и ${(0, ui_1.bold)('CLAUDE.md')} команда не трогает: это ваши файлы, в них ваши правила.
178
- Наше знание живёт в своей папке и переустанавливается поверх без спора за файл.
195
+ --global то же в домашней папке, тогда скилл виден во всех проектах.
196
+ Там добавляется ~/.codex/skills: общий каталог Codex смотрит
197
+ только внутри репозитория. Правило Cursor и AGENTS.md остаются
198
+ проектными, глобальных аналогов у них нет
179
199
 
180
- Скилл едет вместе с CLI, поэтому после обновления пакета команду стоит повторить.`,
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
- сборки и каталог результата берутся из xflow.json (по умолчанию npm run build
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
- Сборка идёт на вашей машине, поэтому её результат зависит от вашей версии Node.
214
- Платформа раздаёт статику: сборка без index.html в корне отклоняется.
234
+ Собирает платформа, в чистой песочнице с одинаковой для всех версией Node,
235
+ поэтому «у меня работало» больше не зависит от вашей машины. Перед сборкой
236
+ проект проверяется на соответствие шаблону: несоответствия печатаются списком,
237
+ и сборка не запускается вовсе.
215
238
 
216
- Выложенное видно только по dev-адресу. Посетителям — после ${(0, ui_1.bold)('xflow publish')}.`,
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
- Переключает dev-адрес на выбранную версию. Возвращается только сборка: исходники
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 # отправить код, собрать и выложить на dev-адрес
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
- /** Ответы публичного API, которые разбирает CLI. */
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.5';
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.5",
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.8",
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,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 when the project root has xflow.json or VITE_XFLOW_* variables, when asked to deploy, publish, roll back a version, sync sources with the platform, look into errors from a deployed app, or build UI on the platform design system.
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, builds locally, uploads the version. It prints
26
- a URL: that is the dev address, visible to the team, not to visitors.
27
- 5. Open and check it: the URL from the output, or `xflow open`.
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 dev address back at an earlier build. Sources stay on their own revision.
35
-
36
- The build runs on the developer machine. The platform has no builder, it serves
37
- static files: a build without `index.html` in the root is rejected.
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
- }