@getxflow/cli 0.1.8 → 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/README.md +45 -44
- package/dist/api.js +10 -15
- package/dist/args.js +1 -1
- package/dist/bin.js +11 -7
- package/dist/commands/auth.js +30 -28
- package/dist/commands/db.js +18 -18
- package/dist/commands/deploy.js +18 -18
- package/dist/commands/env.js +17 -17
- package/dist/commands/functions.js +21 -21
- package/dist/commands/logs.js +6 -6
- package/dist/commands/mcp.js +14 -14
- package/dist/commands/projects.js +23 -23
- package/dist/commands/schedules.js +8 -8
- package/dist/commands/skills.js +6 -6
- package/dist/commands/sources.js +32 -32
- package/dist/config.js +5 -5
- package/dist/help.js +252 -250
- package/dist/limits.js +78 -0
- package/dist/session.js +1 -1
- package/dist/template.js +22 -22
- package/dist/ui.js +12 -12
- package/dist/version.js +1 -1
- package/dist/zip.js +7 -7
- package/package.json +22 -22
- package/skills/xflow/SKILL.md +19 -1
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(
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
\`\`\`bash
|
|
21
|
-
npm install #
|
|
22
|
-
npm run dev #
|
|
23
|
-
xflow deploy #
|
|
24
|
-
xflow publish #
|
|
25
|
-
\`\`\`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
\`src/index.css\`
|
|
31
|
-
|
|
32
|
-
|
|
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)('
|
|
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
|
|
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.
|
|
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('
|
|
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('
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
4
|
-
"description": "CLI
|
|
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
|
+
}
|
package/skills/xflow/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|