@getxflow/cli 0.1.8 → 0.1.10

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.
@@ -16,7 +16,7 @@ const skills_1 = require("./skills");
16
16
  const template_1 = require("../template");
17
17
  const zip_1 = require("../zip");
18
18
  const ui_1 = require("../ui");
19
- /** Пустой ли каталог. `.git` не считается: связывать свежий клон обычное дело. */
19
+ /** .git alone does not count: linking a fresh clone is normal. */
20
20
  function isEmptyEnough(dir) {
21
21
  if (!(0, node_fs_1.existsSync)(dir))
22
22
  return true;
@@ -30,7 +30,7 @@ function write(root, path, content) {
30
30
  function escapeHtml(value) {
31
31
  return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
32
32
  }
33
- /** Заголовок вкладки в шаблоне общий на всех: у проекта он должен быть свой. */
33
+ /** Give the project its own tab title. */
34
34
  function titleFromProject(path, content, name) {
35
35
  if (path !== 'index.html')
36
36
  return content;
@@ -41,40 +41,30 @@ async function templates() {
41
41
  const client = (0, session_1.connect)();
42
42
  const { templates: list } = await (0, api_1.apiJson)(client, '/api/v1/templates');
43
43
  if (list.length === 0) {
44
- (0, ui_1.note)('Шаблонов нет');
44
+ (0, ui_1.note)('No templates');
45
45
  return;
46
46
  }
47
- (0, ui_1.table)(list.map((t) => [t.id, t.name, t.is_default ? 'по умолчанию' : '', t.description ?? '']));
47
+ (0, ui_1.table)(list.map((t) => [t.id, t.name, t.is_default ? 'default' : '', t.description ?? '']));
48
48
  }
49
- /**
50
- * Создать проект: шаблон платформы, локальная папка, проект на платформе.
51
- *
52
- * Шаблон приезжает с платформы, а не лежит в этом пакете. Иначе появились бы две
53
- * стартовые точки — веб и CLI, — и приложения из них выглядели бы по-разному, хотя
54
- * платформа у них одна.
55
- *
56
- * Отсюда и порядок: проект заводится первым, потому что без него неоткуда взять
57
- * токен для `.env`. Если запись файлов сорвётся, проект останется пустым, и CLI
58
- * скажет, как его подобрать (`xflow link`), — молча оставлять сироту нельзя.
59
- */
49
+ /** Create a project from the platform template. */
60
50
  async function init(args) {
61
51
  const target = (0, node_path_1.resolve)(args.words[0] ?? '.');
62
52
  const name = (0, args_1.flagString)(args, 'name') ?? (0, node_path_1.basename)(target);
63
53
  const client = (0, session_1.connect)();
64
54
  if (!isEmptyEnough(target)) {
65
- throw new errors_1.CliError(`Каталог ${target} не пуст`, 'Создайте проект в пустой папке: xflow init my-app. Связать существующую папку с проектом: xflow link <id>');
55
+ throw new errors_1.CliError(`The directory ${target} is not empty`, 'Create the project in an empty folder: xflow init my-app. To link an existing folder to a project: xflow link <id>');
66
56
  }
67
57
  let templateId = (0, args_1.flagString)(args, 'template');
68
58
  if (!templateId) {
69
59
  const { templates: list } = await (0, api_1.apiJson)(client, '/api/v1/templates');
70
60
  templateId = (list.find((t) => t.is_default) ?? list[0])?.id;
71
61
  if (!templateId)
72
- throw new errors_1.CliError('На платформе нет ни одного шаблона');
62
+ throw new errors_1.CliError('The platform has no templates at all');
73
63
  }
74
- (0, ui_1.step)('Забираю шаблон');
64
+ (0, ui_1.step)('Fetching the template');
75
65
  const archive = await (0, api_1.apiBinary)(client, `/api/v1/templates/${templateId}/archive`);
76
66
  const files = (0, zip_1.zipRead)(archive);
77
- (0, ui_1.step)('Создаю проект на платформе');
67
+ (0, ui_1.step)('Creating the project on the platform');
78
68
  const project = await (0, api_1.apiJson)(client, '/api/v1/projects', {
79
69
  method: 'POST',
80
70
  body: { name, database_id: (0, args_1.flagString)(args, 'database') ?? null },
@@ -94,27 +84,26 @@ async function init(args) {
94
84
  (0, config_1.ignoreStateInGit)(target);
95
85
  }
96
86
  catch (e) {
97
- (0, ui_1.note)((0, ui_1.dim)(` Проект «${project.name}» уже создан (${project.id}).`));
98
- (0, ui_1.note)((0, ui_1.dim)(' Разберитесь с папкой и подберите его: xflow link ' + project.id));
87
+ (0, ui_1.note)((0, ui_1.dim)(` The project "${project.name}" is already created (${project.id}).`));
88
+ (0, ui_1.note)((0, ui_1.dim)(' Sort the folder out and pick it up: xflow link ' + project.id));
99
89
  throw e;
100
90
  }
101
91
  (0, skills_1.installSkillQuietly)(target);
102
- (0, ui_1.ok)(`Проект «${project.name}» создан: ${files.length} файлов шаблона`);
92
+ (0, ui_1.ok)(`Project "${project.name}" created: ${files.length} template files`);
103
93
  (0, ui_1.out)('');
104
- (0, ui_1.out)(` ${(0, ui_1.bold)('Дальше:')}`);
94
+ (0, ui_1.out)(` ${(0, ui_1.bold)('Next:')}`);
105
95
  (0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}`);
106
96
  (0, ui_1.out)(' npm install');
107
- (0, ui_1.out)(' npm run dev # разработка');
108
- (0, ui_1.out)(' xflow deploy # отправить код, собрать и выложить');
97
+ (0, ui_1.out)(' npm run dev # develop');
98
+ (0, ui_1.out)(' xflow deploy # send the code, build and release');
109
99
  }
110
- /** Связать текущую папку с уже существующим проектом. */
111
100
  async function link(args) {
112
101
  const root = (0, config_1.findProjectRoot)() ?? process.cwd();
113
102
  const existing = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, config_1.CONFIG_FILE)) ? (0, config_1.readConfig)(root) : undefined;
114
103
  const client = (0, session_1.connect)(existing);
115
104
  const projectId = args.words[0];
116
105
  if (!projectId) {
117
- throw new errors_1.CliError('Нужен идентификатор проекта', 'Список: xflow projects list');
106
+ throw new errors_1.CliError('A project identifier is required', 'The list: xflow projects list');
118
107
  }
119
108
  const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
120
109
  const dir = existing ? root : process.cwd();
@@ -124,14 +113,13 @@ async function link(args) {
124
113
  build: existing?.build ?? { command: 'npm run build', dir: 'dist' },
125
114
  });
126
115
  (0, config_1.ignoreStateInGit)(dir);
127
- // В исходники .env не уходит, поэтому в свежем клоне его нет вовсе, и приложение
128
- // молча теряет доступ к облачным функциям. Восстанавливаем, но чужой не трогаем.
116
+ // A fresh clone has no .env: recreate it, never overwrite an existing one.
129
117
  if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, '.env')) && card.project_token) {
130
118
  write(dir, '.env', (0, template_1.envFile)(card.project_token, client.apiUrl, card.functions));
131
- (0, ui_1.note)((0, ui_1.dim)(' Создан .env с токеном проекта и адресами функций'));
119
+ (0, ui_1.note)((0, ui_1.dim)(' Created .env with the project token and the function addresses'));
132
120
  }
133
121
  (0, skills_1.installSkillQuietly)(dir);
134
- (0, ui_1.ok)(`Папка связана с проектом «${card.name}»`);
122
+ (0, ui_1.ok)(`The folder is linked to the project "${card.name}"`);
135
123
  (0, ui_1.note)((0, ui_1.dim)(` ${(0, node_path_1.join)(dir, config_1.CONFIG_FILE)}`));
136
124
  }
137
125
  async function list() {
@@ -139,13 +127,13 @@ async function list() {
139
127
  const client = (0, session_1.connect)(root ? (0, config_1.readConfig)(root) : undefined);
140
128
  const { projects } = await (0, api_1.apiJson)(client, '/api/v1/projects');
141
129
  if (projects.length === 0) {
142
- (0, ui_1.note)('Проектов нет. Создать: xflow init');
130
+ (0, ui_1.note)('No projects. To create one: xflow init');
143
131
  return;
144
132
  }
145
133
  (0, ui_1.table)(projects.map((p) => [
146
134
  p.id,
147
135
  p.name,
148
- p.live_deploy_id ? 'опубликован' : p.dev_deploy_id ? 'только dev' : 'без сборки',
136
+ p.live_deploy_id ? 'published' : p.dev_deploy_id ? 'dev only' : 'no build',
149
137
  (0, ui_1.formatAge)(p.updated_at),
150
138
  ]));
151
139
  }
@@ -154,7 +142,7 @@ async function get(args) {
154
142
  const config = root ? (0, config_1.readConfig)(root) : undefined;
155
143
  const projectId = args.words[0] ?? config?.projectId;
156
144
  if (!projectId) {
157
- throw new errors_1.CliError('Нужен идентификатор проекта', 'Либо запустите команду в папке проекта');
145
+ throw new errors_1.CliError('A project identifier is required', 'Or run the command inside the project folder');
158
146
  }
159
147
  const client = (0, session_1.connect)(config);
160
148
  const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
@@ -162,19 +150,19 @@ async function get(args) {
162
150
  if (card.description)
163
151
  (0, ui_1.out)(card.description);
164
152
  (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})` : ''}`);
153
+ (0, ui_1.out)(`Built version: ${card.dev_deploy_id ?? '(no build)'}`);
154
+ (0, ui_1.out)(`Visitors see: ${card.live_deploy_id ?? '(never published)'}`);
155
+ (0, ui_1.out)(`Database: ${card.database ? `${card.database.name} (${card.database.schema})` : '(none)'}`);
168
156
  (0, ui_1.out)('');
169
157
  (0, ui_1.out)(card.project_url);
170
158
  if (card.functions.length > 0) {
171
159
  (0, ui_1.out)('');
172
- (0, ui_1.out)('Функции:');
160
+ (0, ui_1.out)('Functions:');
173
161
  (0, ui_1.table)(card.functions.map((f) => [` ${f.name}`, f.status, (0, ui_1.formatAge)(f.last_deployed_at)]));
174
162
  }
175
163
  if (card.schedules.length > 0) {
176
164
  (0, ui_1.out)('');
177
- (0, ui_1.out)('Расписания:');
165
+ (0, ui_1.out)('Schedules:');
178
166
  (0, ui_1.table)(card.schedules.map((s) => [` ${s.function_name}`, s.cron_expression, s.status]));
179
167
  }
180
168
  }
@@ -14,8 +14,8 @@ async function schedulesList() {
14
14
  const client = (0, session_1.connect)(config);
15
15
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules`);
16
16
  if (data.schedules.length === 0) {
17
- (0, ui_1.note)('Расписаний нет');
18
- (0, ui_1.note)((0, ui_1.dim)(' Запускать функцию по времени: xflow schedules set <функция> "0 3 ? * * *"'));
17
+ (0, ui_1.note)('No schedules');
18
+ (0, ui_1.note)((0, ui_1.dim)(' To run a function on a timer: xflow schedules set <function> "0 3 ? * * *"'));
19
19
  return;
20
20
  }
21
21
  (0, ui_1.table)(data.schedules.map((row) => [
@@ -31,21 +31,21 @@ async function schedulesSet(args) {
31
31
  const functionName = args.words[1];
32
32
  const cron = args.words[2];
33
33
  if (!functionName || !cron) {
34
- throw new errors_1.CliError('Нужны имя функции и расписание', 'Например: xflow schedules set nightly-report "0 3 ? * * *" каждый день в 03:00 UTC');
34
+ throw new errors_1.CliError('A function name and a schedule are required', 'For example: xflow schedules set nightly-report "0 3 ? * * *" runs every day at 03:00 UTC');
35
35
  }
36
36
  const row = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules`, {
37
37
  method: 'POST',
38
38
  body: { function: functionName, cron, payload: (0, args_1.flagString)(args, 'payload') ?? null },
39
39
  });
40
- (0, ui_1.ok)(`${(0, ui_1.bold)(row.function)} запускается ${row.description}`);
41
- (0, ui_1.note)((0, ui_1.dim)(` Время в UTC. Проверить вручную: xflow functions invoke ${row.function}`));
40
+ (0, ui_1.ok)(`${(0, ui_1.bold)(row.function)} runs ${row.description}`);
41
+ (0, ui_1.note)((0, ui_1.dim)(` The time is UTC. To check by hand: xflow functions invoke ${row.function}`));
42
42
  }
43
43
  async function schedulesRemove(args) {
44
44
  const { config } = (0, config_1.requireProject)();
45
45
  const client = (0, session_1.connect)(config);
46
46
  const functionName = args.words[1];
47
47
  if (!functionName) {
48
- throw new errors_1.CliError('Нужно имя функции', 'Что запускается по времени: xflow schedules list');
48
+ throw new errors_1.CliError('A function name is required', 'What runs on a timer: xflow schedules list');
49
49
  }
50
50
  const cron = args.words[2];
51
51
  const query = new URLSearchParams({ function: functionName });
@@ -53,8 +53,8 @@ async function schedulesRemove(args) {
53
53
  query.set('cron', cron);
54
54
  const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules?${query.toString()}`, { method: 'DELETE' });
55
55
  if (result.removed === 0) {
56
- (0, ui_1.note)( функции ${functionName} нет такого расписания`);
56
+ (0, ui_1.note)(`Function ${functionName} has no such schedule`);
57
57
  return;
58
58
  }
59
- (0, ui_1.ok)(`Снято расписаний: ${result.removed}`);
59
+ (0, ui_1.ok)(`Schedules removed: ${result.removed}`);
60
60
  }
@@ -9,54 +9,42 @@ const args_1 = require("../args");
9
9
  const errors_1 = require("../errors");
10
10
  const ui_1 = require("../ui");
11
11
  /**
12
- * Формат SKILL.md общий для инструментов (agentskills.io), а папку каждый ищет
13
- * свою: Claude Code в .claude/skills, Codex и OpenClaw в .agents/skills. Cursor к
14
- * общему формату не пришёл и читает .cursor/rules/*.mdc, поэтому туда едет тот же
15
- * текст под его заголовком. Спрашивать, чей агент, мы не хотим: раскладываем всем.
12
+ * The SKILL.md format is shared across tools (agentskills.io); each agent reads
13
+ * its own folder. Cursor reads .cursor/rules/*.mdc instead, so it gets the same
14
+ * text under its own header.
16
15
  */
17
16
  const SKILL_DIRS = [
18
17
  ['.claude', 'skills'],
19
18
  ['.agents', 'skills'],
20
19
  ];
21
- /**
22
- * В домашней папке набор другой. Общий .agents/skills там читает OpenClaw, а
23
- * Codex смотрит его только внутри репозитория: свой личный он держит отдельно,
24
- * в .codex/skills. Без этой папки `--global` тихо не работает для Codex.
25
- */
20
+ /** Codex reads its global skills from .codex/skills, not from .agents. */
26
21
  const GLOBAL_SKILL_DIRS = [...SKILL_DIRS, ['.codex', 'skills']];
27
22
  const CURSOR_RULE = ['.cursor', 'rules', 'xflow.mdc'];
28
- /**
29
- * Указатель в AGENTS.md.
30
- *
31
- * Скилл подхватывается лениво: только когда описание совпало с задачей. Просьба
32
- * «добавь таблицу клиентов» с ним не совпадает, и агент не узнаёт, что проект
33
- * вообще живёт на платформе. AGENTS.md читается всегда и без условий, поэтому
34
- * пара строк здесь решает ту часть, которую ленивый скилл закрыть не может.
35
- */
23
+ /** AGENTS.md is always read, unlike the lazily loaded skill: a short pointer lives there. */
36
24
  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\`.
25
+ const POINTER = `${POINTER_MARKER}
26
+ ## XFlow
27
+
28
+ This project is hosted on the XFlow platform and ships through the \`xflow\` CLI, not
29
+ through a git push. Read \`.agents/skills/xflow/SKILL.md\` before deploying, publishing,
30
+ rolling back, touching the database or migrations, cloud functions, schedules,
31
+ environment variables or production logs. Command list: \`xflow help\`.
44
32
  `;
45
- /** Скилл едет в пакете рядом с dist: у CLI и у инструкций для агента одна версия. */
33
+ /** The skill ships inside the package, next to dist. */
46
34
  function skillSource() {
47
35
  const path = (0, node_path_1.join)(__dirname, '..', '..', 'skills', 'xflow', 'SKILL.md');
48
36
  if (!(0, node_fs_1.existsSync)(path)) {
49
- throw new errors_1.CliError('В пакете CLI нет файла скилла', 'Переустановите @getxflow/cli');
37
+ throw new errors_1.CliError('The CLI package has no skill file', 'Reinstall @getxflow/cli');
50
38
  }
51
39
  return (0, node_fs_1.readFileSync)(path, 'utf-8');
52
40
  }
53
- /** Тот же текст под заголовком Cursor: поля у него свои, а тело общее. */
41
+ /** The same text under Cursor's own header. */
54
42
  function cursorRule(skill) {
55
43
  const description = /^description:\s*(.+)$/m.exec(skill)?.[1] ?? 'XFlow platform';
56
44
  const body = skill.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
57
45
  return `---\ndescription: ${description}\nalwaysApply: false\n---\n\n${body}`;
58
46
  }
59
- /** Пишет, только если содержимое отличается. Возвращает путь тронутого файла. */
47
+ /** Writes only when the content differs. Returns the touched path. */
60
48
  function writeIfChanged(path, content) {
61
49
  const before = (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, 'utf-8') : null;
62
50
  if (before === content)
@@ -65,11 +53,7 @@ function writeIfChanged(path, content) {
65
53
  (0, node_fs_1.writeFileSync)(path, content, 'utf-8');
66
54
  return path;
67
55
  }
68
- /**
69
- * Дописать указатель в AGENTS.md: только в конец и только один раз по маркеру.
70
- * Файл принадлежит пользователю, там его правила, переписывать его мы не вправе.
71
- * CLAUDE.md не трогаем вовсе: Claude Code и так читает .claude/skills.
72
- */
56
+ /** Append once, by marker: the file belongs to the user. */
73
57
  function appendPointer(base) {
74
58
  const path = (0, node_path_1.join)(base, 'AGENTS.md');
75
59
  const before = (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, 'utf-8') : null;
@@ -78,7 +62,6 @@ function appendPointer(base) {
78
62
  (0, node_fs_1.writeFileSync)(path, before ? `${before.replace(/\s*$/, '')}\n\n${POINTER}` : POINTER, 'utf-8');
79
63
  return path;
80
64
  }
81
- /** Разложить инструкцию по папкам, где её найдёт агент. Отдаёт тронутые файлы. */
82
65
  function install(base, global) {
83
66
  const skill = skillSource();
84
67
  const touched = [];
@@ -87,8 +70,7 @@ function install(base, global) {
87
70
  if (path)
88
71
  touched.push(path);
89
72
  }
90
- // Правило Cursor и указатель кладём только рядом с проектом: глобальных
91
- // аналогов у них нет, а в домашней папке их никто не читает.
73
+ // Project-level only: these have no global equivalents.
92
74
  if (!global) {
93
75
  const rule = writeIfChanged((0, node_path_1.join)(base, ...CURSOR_RULE), cursorRule(skill));
94
76
  if (rule)
@@ -99,21 +81,15 @@ function install(base, global) {
99
81
  }
100
82
  return touched;
101
83
  }
102
- /**
103
- * Установка попутно, из `init` и `link`.
104
- *
105
- * Отдельной командой до этого доходили единицы, и агент оставался без знания о
106
- * платформе — той самой, на которой ему предстоит работать. При этом инструкция
107
- * не то, ради чего запускали команду, поэтому её сбой не роняет привязку папки.
108
- */
84
+ /** Best-effort install from init and link: a failure must not break the linking. */
109
85
  function installSkillQuietly(base) {
110
86
  try {
111
87
  if (install(base, false).length > 0) {
112
- (0, ui_1.note)((0, ui_1.dim)(' Инструкция о платформе разложена для ИИ-агента'));
88
+ (0, ui_1.note)((0, ui_1.dim)(' The platform instructions are laid out for the AI agent'));
113
89
  }
114
90
  }
115
91
  catch {
116
- (0, ui_1.note)((0, ui_1.dim)(' Инструкцию для ИИ-агента положить не удалось: xflow skills'));
92
+ (0, ui_1.note)((0, ui_1.dim)(' Could not lay out the AI agent instructions: xflow skills'));
117
93
  }
118
94
  }
119
95
  function skills(args) {
@@ -122,7 +98,7 @@ function skills(args) {
122
98
  for (const path of touched)
123
99
  (0, ui_1.out)(path);
124
100
  if (touched.length === 0)
125
- (0, ui_1.note)((0, ui_1.dim)(' всё уже актуально'));
126
- (0, ui_1.ok)(global ? 'Скилл xflow доступен во всех проектах' : 'Скилл xflow на месте');
127
- (0, ui_1.note)((0, ui_1.dim)(' После обновления CLI повторите команду: скилл обновляется вместе с ним'));
101
+ (0, ui_1.note)((0, ui_1.dim)(' everything is already up to date'));
102
+ (0, ui_1.ok)(global ? 'The xflow skill is available in every project' : 'The xflow skill is in place');
103
+ (0, ui_1.note)((0, ui_1.dim)(' After a CLI update run the command again: the skill ships with it'));
128
104
  }
@@ -15,25 +15,24 @@ const session_1 = require("../session");
15
15
  const tree_1 = require("../tree");
16
16
  const zip_1 = require("../zip");
17
17
  const ui_1 = require("../ui");
18
- /** Потолок сервера. Проверяем и здесь, чтобы не гнать 25 МБ ради отказа. */
18
+ /** Server cap, checked locally to fail before uploading. */
19
19
  const MAX_ARCHIVE_BYTES = 25 * 1024 * 1024;
20
- /** Собрать дерево исходников: что уходит на сервер и с каким хешем. */
21
20
  function prepareTree(root, config) {
22
21
  const buildDir = config.build?.dir?.replace(/^\.\//, '').replace(/\/+$/, '');
23
22
  const extra = [...(config.ignore ?? []), ...(buildDir ? [`${buildDir}/`] : [])];
24
23
  const { files, skippedLinks } = (0, tree_1.collectFiles)(root, (0, tree_1.loadIgnoreRules)(root, extra));
25
24
  if (files.length === 0) {
26
- throw new errors_1.CliError('В папке нет файлов для отправки', 'Проверьте .xflowignore: возможно, исключено всё');
25
+ throw new errors_1.CliError('The folder has no files to send', 'Check .xflowignore: everything may be excluded');
27
26
  }
28
27
  for (const link of skippedLinks) {
29
- (0, ui_1.warn)(`Пропущена ссылка ${link}: символические ссылки не синхронизируются`);
28
+ (0, ui_1.warn)(`Skipped the link ${link}: symbolic links are not synchronized`);
30
29
  }
31
30
  const archive = (0, zip_1.zipCreate)(files);
32
31
  if (archive.length > MAX_ARCHIVE_BYTES) {
33
32
  const top = (0, tree_1.heaviest)(files)
34
- .map((f) => ` ${f.path}${(0, ui_1.formatBytes)(f.content.length)}`)
33
+ .map((f) => ` ${f.path} ${(0, ui_1.formatBytes)(f.content.length)}`)
35
34
  .join('\n');
36
- throw new errors_1.CliError(`Архив ${(0, ui_1.formatBytes)(archive.length)}, потолок ${(0, ui_1.formatBytes)(MAX_ARCHIVE_BYTES)}`, `Самые тяжёлые файлы:\n${top}\n Исключите лишнее в .xflowignore`);
35
+ throw new errors_1.CliError(`The archive is ${(0, ui_1.formatBytes)(archive.length)}, the cap is ${(0, ui_1.formatBytes)(MAX_ARCHIVE_BYTES)}`, `The heaviest files:\n${top}\n Exclude what is not needed in .xflowignore`);
37
36
  }
38
37
  return { files, hash: (0, tree_1.treeHash)(files), archive };
39
38
  }
@@ -41,56 +40,50 @@ async function latestRevision(client, projectId) {
41
40
  const { revisions } = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/revisions`);
42
41
  return revisions[0] ?? null;
43
42
  }
44
- /**
45
- * Показать, чью работу перетирает `--force`, и спросить подтверждение.
46
- *
47
- * Без этого текст «используйте --force» превращается в инструкцию, которую
48
- * агент выполнит не задумываясь, а чужие правки исчезнут без следа (D23).
49
- */
43
+ /** Show what --force would overwrite and ask for confirmation. */
50
44
  async function confirmForce(client, projectId, server, local) {
51
45
  const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
52
46
  (0, ui_1.note)('');
53
- (0, ui_1.warn)(`На сервере ревизия ${server.revision} от ${(0, ui_1.formatAge)(server.created_at)}, ${server.file_count} файлов`);
47
+ (0, ui_1.warn)(`The server holds revision ${server.revision} from ${(0, ui_1.formatAge)(server.created_at)}, ${server.file_count} files`);
54
48
  try {
55
49
  const info = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/pull`);
56
50
  const serverPaths = new Set((0, zip_1.zipRead)(await (0, api_1.downloadUrl)(info.download_url)).map((e) => e.path));
57
51
  const localPaths = new Set(local.map((f) => f.path));
58
52
  const disappearing = [...serverPaths].filter((p) => !localPaths.has(p));
59
53
  if (disappearing.length > 0) {
60
- (0, ui_1.note)(` Исчезнут файлы (${disappearing.length}):`);
54
+ (0, ui_1.note)(` Files that will disappear (${disappearing.length}):`);
61
55
  for (const path of disappearing.slice(0, 20))
62
56
  (0, ui_1.note)((0, ui_1.dim)(` ${path}`));
63
57
  if (disappearing.length > 20)
64
- (0, ui_1.note)((0, ui_1.dim)(` …и ещё ${disappearing.length - 20}`));
58
+ (0, ui_1.note)((0, ui_1.dim)(` …and ${disappearing.length - 20} more`));
65
59
  }
66
60
  else {
67
- (0, ui_1.note)((0, ui_1.dim)(' Файлы не исчезнут, но содержимое серверной ревизии будет заменено вашим'));
61
+ (0, ui_1.note)((0, ui_1.dim)(' No files disappear, but the contents of the server revision are replaced by yours'));
68
62
  }
69
63
  }
70
64
  catch {
71
- (0, ui_1.warn)('Не удалось прочитать серверную копию: список исчезающих файлов недоступен');
65
+ (0, ui_1.warn)('Could not read the server copy: the list of disappearing files is unavailable');
72
66
  }
73
67
  (0, ui_1.note)('');
74
- const confirmed = await (0, ui_1.confirmWord)(`Это перетрёт чужую работу без возможности восстановить её из платформы.`, card.name);
68
+ const confirmed = await (0, ui_1.confirmWord)(`This overwrites work that is not yours, with no way to recover it from the platform.`, card.name);
75
69
  if (!confirmed)
76
- throw new errors_1.CliError('Отменено');
70
+ throw new errors_1.CliError('Cancelled');
77
71
  }
78
- /** Отправка исходников. Используется и командой push, и первым шагом deploy. */
72
+ /** Used by push and by the first step of deploy. */
79
73
  async function pushSources(root, config, client, options) {
80
74
  const tree = prepareTree(root, config);
81
75
  const state = (0, config_1.readState)(root);
82
- (0, ui_1.step)(`Отправляю ${tree.files.length} файлов (${(0, ui_1.formatBytes)(tree.archive.length)})`);
76
+ (0, ui_1.step)(`Sending ${tree.files.length} files (${(0, ui_1.formatBytes)(tree.archive.length)})`);
83
77
  const server = await latestRevision(client, config.projectId);
84
- // Ревизия сервера есть, а мы не знаем, от какой работали (свежий клон, чужая
85
- // машина). Совпал хеш просто синхронизируемся; не совпал — молча заливать
86
- // нельзя, иначе чужая работа исчезнет без единой ошибки.
78
+ // Unknown base revision (fresh clone): a matching hash just syncs,
79
+ // a mismatch must not overwrite silently.
87
80
  if (server && state.revision === undefined && !options.force) {
88
81
  if (server.tree_hash === tree.hash) {
89
82
  (0, config_1.writeState)(root, { revision: server.revision, treeHash: server.tree_hash });
90
- (0, ui_1.ok)(`Уже синхронизировано, ревизия ${server.revision}`);
83
+ (0, ui_1.ok)(`Already in sync, revision ${server.revision}`);
91
84
  return { revision: server.revision, status: 'unchanged' };
92
85
  }
93
- throw new errors_1.CliError(`На сервере ревизия ${server.revision}, а эта папка не помнит, от какой версии работали`, 'Заберите серверную копию рядом и сравните: xflow pull --into ./server-copy');
86
+ throw new errors_1.CliError(`The server holds revision ${server.revision} while this folder does not remember which version it worked from`, 'Fetch the server copy alongside and compare: xflow pull --into ./server-copy');
94
87
  }
95
88
  if (options.force && server && server.tree_hash !== tree.hash) {
96
89
  await confirmForce(client, config.projectId, server, tree.files);
@@ -103,10 +96,10 @@ async function pushSources(root, config, client, options) {
103
96
  const result = await (0, api_1.apiUpload)(client, `/api/v1/projects/${config.projectId}/push`, tree.archive, headers);
104
97
  (0, config_1.writeState)(root, { revision: result.revision, treeHash: result.tree_hash });
105
98
  if (result.status === 'unchanged') {
106
- (0, ui_1.ok)(`Изменений нет, ревизия ${result.revision}`);
99
+ (0, ui_1.ok)(`No changes, revision ${result.revision}`);
107
100
  }
108
101
  else {
109
- (0, ui_1.ok)(`Ревизия ${result.revision}: ${result.file_count} файлов, ${(0, ui_1.formatBytes)(result.size_bytes)}`);
102
+ (0, ui_1.ok)(`Revision ${result.revision}: ${result.file_count} files, ${(0, ui_1.formatBytes)(result.size_bytes)}`);
110
103
  }
111
104
  return { revision: result.revision, status: result.status };
112
105
  }
@@ -115,7 +108,6 @@ async function push(args) {
115
108
  const client = (0, session_1.connect)(config);
116
109
  await pushSources(root, config, client, { force: (0, args_1.flagBool)(args, 'force') });
117
110
  }
118
- /** Есть ли в каталоге что-то, кроме служебного. */
119
111
  function hasContent(dir) {
120
112
  if (!(0, node_fs_1.existsSync)(dir))
121
113
  return false;
@@ -130,14 +122,14 @@ async function pull(args) {
130
122
  const query = revision !== undefined ? `?revision=${revision}` : '';
131
123
  const info = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/pull${query}`);
132
124
  if (hasContent(target) && !(0, args_1.flagBool)(args, 'force')) {
133
- throw new errors_1.CliError(`Каталог ${target} не пуст`, 'Слить изменения платформа не умеет это работа git. Заберите серверную копию рядом: ' +
134
- 'xflow pull --into ./server-copy, либо перезапишите папку целиком: xflow pull --force');
125
+ throw new errors_1.CliError(`The directory ${target} is not empty`, 'The platform cannot merge changes, that is git work. Fetch the server copy alongside: ' +
126
+ 'xflow pull --into ./server-copy, or overwrite the folder completely: xflow pull --force');
135
127
  }
136
- (0, ui_1.step)(`Скачиваю ревизию ${info.revision} (${(0, ui_1.formatBytes)(info.size_bytes)})`);
128
+ (0, ui_1.step)(`Downloading revision ${info.revision} (${(0, ui_1.formatBytes)(info.size_bytes)})`);
137
129
  const entries = (0, zip_1.zipRead)(await (0, api_1.downloadUrl)(info.download_url));
138
130
  const hash = (0, tree_1.treeHash)(entries.map((e) => ({ path: e.path, content: e.content })));
139
131
  if (hash !== info.tree_hash) {
140
- (0, ui_1.warn)('Хеш скачанного дерева не совпал с серверным: содержимое могло измениться при передаче');
132
+ (0, ui_1.warn)('The hash of the downloaded tree did not match the server one: the contents may have changed in transit');
141
133
  }
142
134
  (0, node_fs_1.mkdirSync)(target, { recursive: true });
143
135
  for (const entry of entries) {
@@ -145,12 +137,11 @@ async function pull(args) {
145
137
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
146
138
  (0, node_fs_1.writeFileSync)(path, entry.content);
147
139
  }
148
- // Состояние пишем только для рабочей папки: копия рядом (`--into`) к отсчёту
149
- // ревизий отношения не имеет, и запись туда сбила бы его у рабочей.
140
+ // Only the working folder tracks revisions; a side copy (--into) must not reset them.
150
141
  if (!into) {
151
142
  (0, config_1.writeState)(root, { revision: info.revision, treeHash: info.tree_hash });
152
143
  }
153
- (0, ui_1.ok)(`Ревизия ${info.revision}: ${entries.length} файлов в ${target}`);
144
+ (0, ui_1.ok)(`Revision ${info.revision}: ${entries.length} files in ${target}`);
154
145
  }
155
146
  async function status() {
156
147
  const { root, config } = (0, config_1.requireProject)();
@@ -163,28 +154,28 @@ async function status() {
163
154
  ]);
164
155
  (0, ui_1.out)(`${(0, ui_1.bold)(card.name)} ${(0, ui_1.dim)(card.id)}`);
165
156
  (0, ui_1.out)('');
166
- (0, ui_1.out)(`Локально: ${tree.files.length} файлов, ${(0, ui_1.formatBytes)(tree.archive.length)} в архиве`);
157
+ (0, ui_1.out)(`Local: ${tree.files.length} files, ${(0, ui_1.formatBytes)(tree.archive.length)} archived`);
167
158
  if (!server) {
168
- (0, ui_1.out)('На сервере: исходников ещё нет');
169
- (0, ui_1.note)((0, ui_1.dim)(' Отправить: xflow push'));
159
+ (0, ui_1.out)('On the server: no sources yet');
160
+ (0, ui_1.note)((0, ui_1.dim)(' Send them: xflow push'));
170
161
  }
171
162
  else {
172
- (0, ui_1.out)(`На сервере: ревизия ${server.revision}, ${server.file_count} файлов, ${(0, ui_1.formatAge)(server.created_at)}`);
163
+ (0, ui_1.out)(`On the server: revision ${server.revision}, ${server.file_count} files, ${(0, ui_1.formatAge)(server.created_at)}`);
173
164
  if (server.tree_hash === tree.hash) {
174
- (0, ui_1.out)('Состояние: совпадает с сервером');
165
+ (0, ui_1.out)('State: matches the server');
175
166
  }
176
167
  else if (state.revision !== undefined && state.revision < server.revision) {
177
- (0, ui_1.out)(`Состояние: ${(0, ui_1.bold)('расхождение')} на сервере новее (вы работали от ${state.revision})`);
178
- (0, ui_1.note)((0, ui_1.dim)(' Забрать серверную копию рядом: xflow pull --into ./server-copy'));
168
+ (0, ui_1.out)(`State: ${(0, ui_1.bold)('diverged')}, the server is newer (you worked from ${state.revision})`);
169
+ (0, ui_1.note)((0, ui_1.dim)(' Fetch the server copy alongside: xflow pull --into ./server-copy'));
179
170
  }
180
171
  else {
181
- (0, ui_1.out)(`Состояние: ${(0, ui_1.bold)('есть локальные изменения')}`);
182
- (0, ui_1.note)((0, ui_1.dim)(' Отправить: xflow push'));
172
+ (0, ui_1.out)(`State: ${(0, ui_1.bold)('local changes')}`);
173
+ (0, ui_1.note)((0, ui_1.dim)(' Send them: xflow push'));
183
174
  }
184
175
  }
185
176
  (0, ui_1.out)('');
186
- (0, ui_1.out)(`Собранная версия: ${card.dev_deploy_id ?? '(сборки нет)'}`);
187
- (0, ui_1.out)(`Видят посетители: ${card.live_deploy_id ?? '(не публиковался)'}`);
177
+ (0, ui_1.out)(`Built version: ${card.dev_deploy_id ?? '(no build)'}`);
178
+ (0, ui_1.out)(`Visitors see: ${card.live_deploy_id ?? '(never published)'}`);
188
179
  (0, ui_1.out)('');
189
180
  (0, ui_1.out)(card.project_url);
190
181
  }