@getxflow/cli 0.1.7 → 0.1.9

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/dist/limits.js ADDED
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Лимиты тарифа в выводе CLI.
4
+ *
5
+ * Два места, где они видны: `xflow whoami` (что есть) и отказ команды (во что
6
+ * упёрлись). Второе важнее: без явной пометки агент читает 403 как ошибку
7
+ * запроса и уходит переписывать команду, которая была верной.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.limitLine = limitLine;
11
+ exports.quotaRows = quotaRows;
12
+ /** Что кончилось, по коду отказа гейта. */
13
+ const DENIAL_LABELS = {
14
+ limit_projects: 'projects',
15
+ limit_functions: 'cloud functions',
16
+ limit_builds: 'builds this month',
17
+ limit_seats: 'seats in the organization',
18
+ limit_schedule_interval: 'schedule frequency',
19
+ db_write_locked: 'database volume, writes are off',
20
+ plan_no_schedules: 'schedules',
21
+ subscription_blocked: 'the subscription is unpaid',
22
+ billing_unavailable: 'plan check',
23
+ };
24
+ /** Строка под сообщением отказа: почему повторять команду бесполезно. */
25
+ function limitLine(detail) {
26
+ const label = DENIAL_LABELS[detail.code] ?? detail.code;
27
+ const numbers = detail.used !== undefined && detail.limit !== undefined
28
+ ? `, ${detail.used} of ${detail.limit} used`
29
+ : '';
30
+ return `Plan limit: ${label}${numbers}. Repeating the same command will not help.`;
31
+ }
32
+ /** Порядок строк в whoami: сперва штучное, потом объёмы, потом расход месяца. */
33
+ const QUOTA_ORDER = [
34
+ 'projects',
35
+ 'functions',
36
+ 'developers',
37
+ 'members',
38
+ 'database_mb',
39
+ 'files_mb',
40
+ 'function_minutes',
41
+ 'builds',
42
+ ];
43
+ const QUOTA_LABELS = {
44
+ projects: 'Projects',
45
+ functions: 'Cloud functions',
46
+ developers: 'Developers',
47
+ members: 'Staff',
48
+ database_mb: 'Database',
49
+ files_mb: 'Files',
50
+ function_minutes: 'Function minutes',
51
+ builds: 'Builds this month',
52
+ };
53
+ /** Гигабайт десятичный, 1000 МБ: так объём и продаётся, шаг докупки 500 МБ
54
+ * обязан читаться как полгигабайта. Сравнение с байтами живёт на сервере и
55
+ * остаётся на 1024². */
56
+ function sizeLabel(mb) {
57
+ if (mb < 1000)
58
+ return `${Math.round(mb)} MB`;
59
+ const gb = mb / 1000;
60
+ return `${Number.isInteger(gb) ? String(gb) : gb.toFixed(1)} GB`;
61
+ }
62
+ function quotaValue(key, use) {
63
+ // Обе стороны в одних единицах: «0.2 of 15 GB» читается, «245 of 15360» нет.
64
+ if (key.endsWith('_mb')) {
65
+ const used = use.limit >= 1000 ? (use.used / 1000).toFixed(1) : String(use.used);
66
+ return `${used} of ${sizeLabel(use.limit)}`;
67
+ }
68
+ return `${use.used} of ${use.limit}`;
69
+ }
70
+ /** Строки таблицы для whoami. Незнакомые ключи не прячем: сервер мог добавить. */
71
+ function quotaRows(quotas) {
72
+ const known = QUOTA_ORDER.filter((key) => quotas[key]);
73
+ const rest = Object.keys(quotas).filter((key) => !QUOTA_ORDER.includes(key));
74
+ return [...known, ...rest].map((key) => [
75
+ ` ${QUOTA_LABELS[key] ?? key}`,
76
+ quotaValue(key, quotas[key]),
77
+ ]);
78
+ }
package/dist/session.js CHANGED
@@ -16,7 +16,7 @@ function connect(config) {
16
16
  const apiUrl = (0, config_1.apiUrlFor)(config);
17
17
  const token = (0, credentials_1.resolveToken)(apiUrl);
18
18
  if (!token) {
19
- throw new errors_1.CliError(`Нет ключа доступа для ${apiUrl}`, 'Войдите: xflow login. В CI передайте ключ переменной XFLOW_TOKEN');
19
+ throw new errors_1.CliError(`No access key for ${apiUrl}`, 'Sign in: xflow login. In CI pass the key in the XFLOW_TOKEN variable');
20
20
  }
21
21
  return { apiUrl, token };
22
22
  }
package/dist/template.js CHANGED
@@ -7,29 +7,29 @@ exports.writeEnvValue = writeEnvValue;
7
7
  exports.scaffoldFiles = scaffoldFiles;
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = require("node:path");
10
- const GITIGNORE = `node_modules/
11
- dist/
12
- .xflow/
13
- .env
14
- .env.*
10
+ const GITIGNORE = `node_modules/
11
+ dist/
12
+ .xflow/
13
+ .env
14
+ .env.*
15
15
  `;
16
- const README = `# %NAME%
17
-
18
- Приложение на XFlow.
19
-
20
- \`\`\`bash
21
- npm install # зависимости
22
- npm run dev # разработка на localhost:3000
23
- xflow deploy # отправить код и собрать на платформе
24
- xflow publish # показать dev-версию посетителям
25
- \`\`\`
26
-
27
- Интерфейс собран на компонентах шаблона: \`src/components/ui\` (кнопки, поля, диалоги)
28
- и \`src/components/blocks\` (таблица, форма, фильтры, канбан, графики). Своя вёрстка
29
- поверх них выглядит как остальная платформа, а собственные цвета вместо токенов из
30
- \`src/index.css\` эту связность ломают.
31
-
32
- Полезное: \`xflow status\` что на сервере, \`xflow deployments\` история версий.
16
+ const README = `# %NAME%
17
+
18
+ An application on XFlow.
19
+
20
+ \`\`\`bash
21
+ npm install # dependencies
22
+ npm run dev # develop on localhost:3000
23
+ xflow deploy # send the code and build it on the platform
24
+ xflow publish # show the dev version to visitors
25
+ \`\`\`
26
+
27
+ The interface is built on the template components: \`src/components/ui\` (buttons, fields,
28
+ dialogs) and \`src/components/blocks\` (table, form, filters, kanban, charts). Layout built
29
+ on top of them looks like the rest of the platform, while custom colors in place of the
30
+ tokens from \`src/index.css\` break that consistency.
31
+
32
+ Useful: \`xflow status\` for what is on the server, \`xflow deployments\` for version history.
33
33
  `;
34
34
  /** Адреса облачных функций для сборки: имя → URL, одной строкой JSON. */
35
35
  exports.FUNCTIONS_ENV_KEY = 'VITE_XFLOW_FUNCTIONS';
package/dist/ui.js CHANGED
@@ -66,26 +66,26 @@ function table(rows) {
66
66
  }
67
67
  function formatBytes(bytes) {
68
68
  if (bytes < 1024)
69
- return `${bytes} Б`;
69
+ return `${bytes} B`;
70
70
  if (bytes < 1024 * 1024)
71
- return `${(bytes / 1024).toFixed(1)} КБ`;
72
- return `${(bytes / 1024 / 1024).toFixed(1)} МБ`;
71
+ return `${(bytes / 1024).toFixed(1)} KB`;
72
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
73
73
  }
74
74
  function formatAge(iso) {
75
75
  if (!iso)
76
- return '';
76
+ return '-';
77
77
  const ms = Date.now() - new Date(iso).getTime();
78
78
  if (!Number.isFinite(ms) || ms < 0)
79
- return '';
79
+ return '-';
80
80
  const minutes = Math.floor(ms / 60000);
81
81
  if (minutes < 1)
82
- return 'только что';
82
+ return 'just now';
83
83
  if (minutes < 60)
84
- return `${minutes} мин назад`;
84
+ return `${minutes} min ago`;
85
85
  const hours = Math.floor(minutes / 60);
86
86
  if (hours < 24)
87
- return `${hours} ч назад`;
88
- return `${Math.floor(hours / 24)} дн назад`;
87
+ return `${hours} h ago`;
88
+ return `${Math.floor(hours / 24)} d ago`;
89
89
  }
90
90
  /**
91
91
  * Подтверждение опасного действия.
@@ -96,14 +96,14 @@ function formatAge(iso) {
96
96
  */
97
97
  async function confirmWord(question, expected) {
98
98
  if (!process.stdin.isTTY) {
99
- fail('Подтверждение возможно только в интерактивном терминале');
100
- note((0, exports.dim)(' В CI это правильное поведение: расхождение версий должно ронять сборку, а не перетирать чужую работу'));
99
+ fail('Confirmation is only possible in an interactive terminal');
100
+ note((0, exports.dim)(' In CI this is the right behaviour: a version conflict has to fail the build rather than overwrite somebody else"s work'));
101
101
  return false;
102
102
  }
103
103
  const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stderr });
104
104
  try {
105
105
  const answer = await new Promise((resolve) => {
106
- rl.question(`${question}\n Введите «${expected}» для подтверждения: `, resolve);
106
+ rl.question(`${question}\n Type "${expected}" to confirm: `, resolve);
107
107
  });
108
108
  return answer.trim() === expected;
109
109
  }
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.7';
9
+ exports.CLI_VERSION = '0.1.9';
10
10
  /** Адрес платформы по умолчанию. Переопределяется XFLOW_API_URL и полем `api` в xflow.json. */
11
11
  exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/dist/zip.js CHANGED
@@ -120,16 +120,16 @@ function findEocd(buffer) {
120
120
  }
121
121
  function zipRead(buffer) {
122
122
  if (buffer.length < 22)
123
- throw new ZipError('Файл слишком мал для zip');
123
+ throw new ZipError('The file is too small to be a zip');
124
124
  const eocd = findEocd(buffer);
125
125
  if (eocd === -1)
126
- throw new ZipError('Это не zip: не найден конец архива');
126
+ throw new ZipError('This is not a zip: the end of the archive was not found');
127
127
  const count = buffer.readUInt16LE(eocd + 10);
128
128
  let pointer = buffer.readUInt32LE(eocd + 16);
129
129
  const entries = [];
130
130
  for (let i = 0; i < count; i++) {
131
131
  if (pointer + 46 > buffer.length || buffer.readUInt32LE(pointer) !== CENTRAL_SIG) {
132
- throw new ZipError('Повреждён оглавление архива');
132
+ throw new ZipError('The archive directory is damaged');
133
133
  }
134
134
  const method = buffer.readUInt16LE(pointer + 10);
135
135
  const compressedSize = buffer.readUInt32LE(pointer + 20);
@@ -143,10 +143,10 @@ function zipRead(buffer) {
143
143
  if (name.endsWith('/'))
144
144
  continue;
145
145
  if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff) {
146
- throw new ZipError(`Архив в формате zip64 не поддерживается: ${name}`);
146
+ throw new ZipError(`The zip64 format is not supported: ${name}`);
147
147
  }
148
148
  if (buffer.readUInt32LE(localOffset) !== LOCAL_SIG) {
149
- throw new ZipError(`Повреждена запись архива: ${name}`);
149
+ throw new ZipError(`A damaged archive entry: ${name}`);
150
150
  }
151
151
  const localNameLength = buffer.readUInt16LE(localOffset + 26);
152
152
  const localExtraLength = buffer.readUInt16LE(localOffset + 28);
@@ -161,11 +161,11 @@ function zipRead(buffer) {
161
161
  content = (0, node_zlib_1.inflateRawSync)(raw);
162
162
  }
163
163
  catch (e) {
164
- throw new ZipError(`Не удалось распаковать ${name}: ${e instanceof Error ? e.message : e}`);
164
+ throw new ZipError(`Could not unpack ${name}: ${e instanceof Error ? e.message : e}`);
165
165
  }
166
166
  }
167
167
  else {
168
- throw new ZipError(`Неизвестный метод сжатия (${method}) у ${name}`);
168
+ throw new ZipError(`Unknown compression method (${method}) on ${name}`);
169
169
  }
170
170
  entries.push({ path: name.replace(/\\/g, '/'), content });
171
171
  }
package/package.json CHANGED
@@ -1,22 +1,22 @@
1
- {
2
- "name": "@getxflow/cli",
3
- "version": "0.1.7",
4
- "description": "CLI платформы XFlow: синхронизация кода, деплой и публикация приложений",
5
- "license": "UNLICENSED",
6
- "engines": {
7
- "node": ">=20"
8
- },
9
- "bin": {
10
- "xflow": "dist/bin.js"
11
- },
12
- "files": [
13
- "dist",
14
- "skills"
15
- ],
16
- "scripts": {
17
- "build": "tsc -p tsconfig.json"
18
- },
19
- "publishConfig": {
20
- "access": "public"
21
- }
22
- }
1
+ {
2
+ "name": "@getxflow/cli",
3
+ "version": "0.1.9",
4
+ "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
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
+ }
@@ -14,7 +14,25 @@ A platform project is recognized by the `xflow.json` file in its root.
14
14
  Check commands and flags against `xflow help` and `xflow help <command>`, not against
15
15
  memory. If a command is not in the help output, it does not exist: guessing flags is
16
16
  pointless. The CLI prints a hint with almost every error, read it in full, it usually
17
- contains the fix. Help output is in Russian.
17
+ contains the fix.
18
+
19
+ ## Plan limits
20
+
21
+ The organization runs on a plan with finite limits: projects, cloud functions, developer
22
+ and staff seats, database and file storage, function minutes per month, plus the right to
23
+ use schedules. `xflow whoami` prints every one of them next to what is already used, and
24
+ reading it before a long task is cheaper than hitting a wall mid-way.
25
+
26
+ A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`,
27
+ the API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`), and
28
+ MCP tools carry the same field. Retrying the command, renaming things or rewriting the
29
+ code changes nothing: tell the user what ran out and stop. Only the owner or an admin
30
+ lifts it, in the web interface, by freeing the resource or moving to a bigger plan.
31
+
32
+ One refusal looks like a code error but is not: when the database is over its plan size,
33
+ Postgres itself rejects inserts (`db_write_locked`). Reads and deletes still work, so the
34
+ fix is a migration that deletes data, never a rewrite of the failing SQL. Write is
35
+ restored within an hour of the data going back under the limit.
18
36
 
19
37
  ## Workflow
20
38