@getxflow/cli 0.1.9 → 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.
@@ -9,40 +9,28 @@ 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)) {
@@ -50,13 +38,13 @@ function skillSource() {
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,13 +81,7 @@ 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) {
@@ -15,9 +15,8 @@ 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}/`] : [])];
@@ -41,12 +40,7 @@ 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)('');
@@ -71,19 +65,18 @@ async function confirmForce(client, projectId, server, local) {
71
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)(`This overwrites somebody else"s work with no way to recover it from the platform.`, 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
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
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 });
@@ -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;
@@ -145,8 +137,7 @@ 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
  }
package/dist/config.js CHANGED
@@ -17,7 +17,7 @@ const STATE_DIR = '.xflow';
17
17
  class ConfigError extends Error {
18
18
  }
19
19
  exports.ConfigError = ConfigError;
20
- /** Ищем корень проекта вверх по дереву: команду запускают из подкаталога чаще, чем из корня. */
20
+ /** Walk up from cwd. */
21
21
  function findProjectRoot(from = process.cwd()) {
22
22
  let dir = (0, node_path_1.resolve)(from);
23
23
  for (;;) {
@@ -47,7 +47,6 @@ function readConfig(root) {
47
47
  function writeConfig(root, config) {
48
48
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, exports.CONFIG_FILE), `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
49
49
  }
50
- /** Конфигурация проекта, из которого запущена команда. Бросает, если папка не связана. */
51
50
  function requireProject() {
52
51
  const root = findProjectRoot();
53
52
  if (!root) {
@@ -58,8 +57,12 @@ function requireProject() {
58
57
  return { root, config: readConfig(root) };
59
58
  }
60
59
  function apiUrlFor(config) {
61
- const raw = process.env.XFLOW_API_URL?.trim() || config?.api?.trim() || version_1.DEFAULT_API_URL;
62
- return raw.replace(/\/+$/, '');
60
+ const raw = (process.env.XFLOW_API_URL?.trim() || config?.api?.trim() || version_1.DEFAULT_API_URL).replace(/\/+$/, '');
61
+ // The address ends up in headers and child processes: keep it a plain http(s) URL.
62
+ if (!/^https?:\/\/[A-Za-z0-9.-]+(:\d+)?(\/[A-Za-z0-9._/-]*)?$/.test(raw)) {
63
+ throw new ConfigError(`Invalid platform address: ${raw}`);
64
+ }
65
+ return raw;
63
66
  }
64
67
  function readState(root) {
65
68
  const path = (0, node_path_1.join)(root, STATE_DIR, 'state.json');
@@ -77,10 +80,7 @@ function writeState(root, state) {
77
80
  (0, node_fs_1.mkdirSync)(dir, { recursive: true });
78
81
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, 'state.json'), `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
79
82
  }
80
- /**
81
- * Состояние в git не нужно никому, но забыть про него легко: дописываем в
82
- * .gitignore сами. Если файла нет, создаём — проект почти всегда под git.
83
- */
83
+ /** Keep .xflow/ out of git. */
84
84
  function ignoreStateInGit(root) {
85
85
  const path = (0, node_path_1.join)(root, '.gitignore');
86
86
  const line = `${STATE_DIR}/`;
@@ -30,12 +30,12 @@ function writeStore(store) {
30
30
  (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
31
31
  const path = credentialsPath();
32
32
  (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
33
- // На случай, когда файл уже существовал: writeFileSync права не меняет.
33
+ // writeFileSync does not change the mode of an existing file.
34
34
  try {
35
35
  (0, node_fs_1.chmodSync)(path, 0o600);
36
36
  }
37
37
  catch {
38
- // Windows правами POSIX не управляет — не повод падать.
38
+ // Windows has no POSIX modes.
39
39
  }
40
40
  }
41
41
  function saveCredential(apiUrl, credential) {
@@ -51,14 +51,13 @@ function forgetCredential(apiUrl) {
51
51
  writeStore(store);
52
52
  return true;
53
53
  }
54
- /**
55
- * Ключ для адреса платформы. Переменная окружения главнее файла: так работает
56
- * запуск из CI, где домашнего каталога может не быть вовсе.
57
- */
58
- function resolveToken(apiUrl) {
59
- const fromEnv = process.env.XFLOW_TOKEN?.trim();
60
- if (fromEnv)
61
- return fromEnv;
54
+ /** XFLOW_TOKEN wins over the store; the caller decides whether env is allowed for this address. */
55
+ function resolveToken(apiUrl, allowEnv = true) {
56
+ if (allowEnv) {
57
+ const fromEnv = process.env.XFLOW_TOKEN?.trim();
58
+ if (fromEnv)
59
+ return fromEnv;
60
+ }
62
61
  return readStore()[apiUrl]?.token ?? null;
63
62
  }
64
63
  function readCredential(apiUrl) {
package/dist/errors.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CliError = void 0;
4
- /** Ошибка, которую показываем человеку как есть: без стека и без «Unhandled». */
4
+ /** Shown to the user as is, without a stack trace. */
5
5
  class CliError extends Error {
6
6
  hint;
7
7
  constructor(message, hint) {