@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/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 +33 -18
- package/dist/commands/db.js +18 -18
- package/dist/commands/deploy.js +18 -18
- package/dist/commands/env.js +36 -25
- 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/README.md
CHANGED
|
@@ -1,44 +1,45 @@
|
|
|
1
|
-
# @getxflow/cli
|
|
2
|
-
|
|
3
|
-
CLI
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npm i -g @getxflow/cli
|
|
7
|
-
xflow login
|
|
8
|
-
xflow init my-app
|
|
9
|
-
cd my-app && npm install
|
|
10
|
-
xflow deploy
|
|
11
|
-
xflow publish
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
|
18
|
-
|---|---|
|
|
19
|
-
| `login` / `logout` / `whoami` |
|
|
20
|
-
| `init` / `link` |
|
|
21
|
-
| `status` / `push` / `pull` |
|
|
22
|
-
| `deploy` / `publish` / `rollback` / `deployments` |
|
|
23
|
-
| `db status` / `db migrate` |
|
|
24
|
-
| `functions list` / `functions deploy` |
|
|
25
|
-
| `functions invoke` / `functions logs` |
|
|
26
|
-
| `schedules list` / `set` / `rm` |
|
|
27
|
-
| `env` / `env check` / `env set` |
|
|
28
|
-
| `logs` |
|
|
29
|
-
| `projects list` / `projects get` / `open` |
|
|
30
|
-
| `skills` |
|
|
31
|
-
| `mcp install` |
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
# @getxflow/cli
|
|
2
|
+
|
|
3
|
+
CLI for the [XFlow](https://getxflow.com) platform: source sync, builds and releases.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i -g @getxflow/cli
|
|
7
|
+
xflow login
|
|
8
|
+
xflow init my-app
|
|
9
|
+
cd my-app && npm install
|
|
10
|
+
xflow deploy
|
|
11
|
+
xflow publish
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Node 20+. The package has no dependencies: it holds an access key, and nobody
|
|
15
|
+
else's code belongs next to it.
|
|
16
|
+
|
|
17
|
+
| Command | What it does |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `login` / `logout` / `whoami` | sign in through the browser, sign out, whose key this is and what it can do |
|
|
20
|
+
| `init` / `link` | new project, link a folder to an existing one |
|
|
21
|
+
| `status` / `push` / `pull` | state, sending and fetching sources |
|
|
22
|
+
| `deploy` / `publish` / `rollback` / `deployments` | build, publish, roll back, version history |
|
|
23
|
+
| `db status` / `db migrate` | migrations from `migrations/*.sql` with a gate on destructive ones |
|
|
24
|
+
| `functions list` / `functions deploy` | cloud functions of the project from `functions/<name>/index.ts` |
|
|
25
|
+
| `functions invoke` / `functions logs` | call a function, look at its crashes with the stack |
|
|
26
|
+
| `schedules list` / `set` / `rm` | running functions on a timer (timer triggers) |
|
|
27
|
+
| `env` / `env check` / `env set` | environment variables of the functions, values are never handed back |
|
|
28
|
+
| `logs` | browser errors from the released application |
|
|
29
|
+
| `projects list` / `projects get` / `open` | projects of the organization and the application addresses |
|
|
30
|
+
| `skills` | platform instructions for an AI agent ([Agent Skills](https://agentskills.io) format) |
|
|
31
|
+
| `mcp install` | connect the agent to the platform directly, without a terminal |
|
|
32
|
+
|
|
33
|
+
Help for one command: `xflow help <command>`.
|
|
34
|
+
|
|
35
|
+
You write the application yourself or with your agent: the platform stays out of the code.
|
|
36
|
+
`xflow skills` puts a description of the platform alongside it, which Claude Code, Codex and
|
|
37
|
+
OpenClaw pick up on their own when a task touches XFlow. Your `AGENTS.md` and `CLAUDE.md` are
|
|
38
|
+
left alone.
|
|
39
|
+
|
|
40
|
+
`xflow mcp install` connects the agent to the platform directly: the database, migrations,
|
|
41
|
+
functions, schedules, variables and versions become its tools, with no command output to parse.
|
|
42
|
+
Code, builds and releases stay in the CLI.
|
|
43
|
+
|
|
44
|
+
The key is stored in `~/.xflow/credentials.json`, the project configuration in `xflow.json`.
|
|
45
|
+
For CI, pass the key in the `XFLOW_TOKEN` variable.
|
package/dist/api.js
CHANGED
|
@@ -6,27 +6,22 @@ exports.apiBinary = apiBinary;
|
|
|
6
6
|
exports.apiUpload = apiUpload;
|
|
7
7
|
exports.downloadUrl = downloadUrl;
|
|
8
8
|
const version_1 = require("./version");
|
|
9
|
-
/**
|
|
10
|
-
* Клиент публичного API платформы.
|
|
11
|
-
*
|
|
12
|
-
* Ошибка сервера приходит в одном формате (`error`, `code`, `hint`) — здесь она
|
|
13
|
-
* превращается в исключение, у которого есть что показать человеку. Плюс
|
|
14
|
-
* проверка версии протокола: старый CLI обязан сказать «обновись», а не падать
|
|
15
|
-
* на разборе неизвестного ответа.
|
|
16
|
-
*/
|
|
17
9
|
class ApiError extends Error {
|
|
18
10
|
code;
|
|
19
11
|
status;
|
|
20
12
|
hint;
|
|
21
13
|
/** Разбор отказа, если он есть: например, нарушения, из-за которых не пошла сборка. */
|
|
22
14
|
issues;
|
|
23
|
-
|
|
15
|
+
/** Заполнен, когда упёрлись в тариф: команда верна, повторять её бесполезно. */
|
|
16
|
+
limit;
|
|
17
|
+
constructor(message, code, status, hint, issues, limit) {
|
|
24
18
|
super(message);
|
|
25
19
|
this.name = 'ApiError';
|
|
26
20
|
this.code = code;
|
|
27
21
|
this.status = status;
|
|
28
22
|
this.hint = hint;
|
|
29
23
|
this.issues = issues;
|
|
24
|
+
this.limit = limit;
|
|
30
25
|
}
|
|
31
26
|
}
|
|
32
27
|
exports.ApiError = ApiError;
|
|
@@ -53,7 +48,7 @@ function isOlder(current, required) {
|
|
|
53
48
|
function assertProtocol(response) {
|
|
54
49
|
const required = response.headers.get('x-xflow-cli-min');
|
|
55
50
|
if (required && isOlder(version_1.CLI_VERSION, required)) {
|
|
56
|
-
throw new ApiError(
|
|
51
|
+
throw new ApiError(`The platform requires CLI ${required} or newer, ${version_1.CLI_VERSION} is installed`, 'outdated_cli', response.status, 'Update: npm i -g @getxflow/cli');
|
|
57
52
|
}
|
|
58
53
|
}
|
|
59
54
|
async function send(client, path, options = {}) {
|
|
@@ -88,7 +83,7 @@ async function send(client, path, options = {}) {
|
|
|
88
83
|
}
|
|
89
84
|
catch (e) {
|
|
90
85
|
const reason = e instanceof Error ? e.message : String(e);
|
|
91
|
-
throw new ApiError(
|
|
86
|
+
throw new ApiError(`The platform is unreachable: ${reason}`, 'network', 0, `Check the connection and the address ${client.apiUrl}`);
|
|
92
87
|
}
|
|
93
88
|
assertProtocol(response);
|
|
94
89
|
return response;
|
|
@@ -97,10 +92,10 @@ async function readError(response) {
|
|
|
97
92
|
const text = await response.text().catch(() => '');
|
|
98
93
|
try {
|
|
99
94
|
const parsed = JSON.parse(text);
|
|
100
|
-
return new ApiError(parsed.error ||
|
|
95
|
+
return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed.issues, parsed.limit);
|
|
101
96
|
}
|
|
102
97
|
catch {
|
|
103
|
-
return new ApiError(
|
|
98
|
+
return new ApiError(`Request rejected (${response.status})`, 'unknown', response.status, text.slice(0, 200) || undefined);
|
|
104
99
|
}
|
|
105
100
|
}
|
|
106
101
|
async function apiJson(client, path, options = {}) {
|
|
@@ -127,10 +122,10 @@ async function downloadUrl(url) {
|
|
|
127
122
|
response = await fetch(url, { signal: AbortSignal.timeout(TRANSFER_TIMEOUT_MS) });
|
|
128
123
|
}
|
|
129
124
|
catch (e) {
|
|
130
|
-
throw new ApiError(
|
|
125
|
+
throw new ApiError(`Could not download the archive: ${e instanceof Error ? e.message : e}`, 'network', 0);
|
|
131
126
|
}
|
|
132
127
|
if (!response.ok) {
|
|
133
|
-
throw new ApiError(
|
|
128
|
+
throw new ApiError(`Could not download the archive (${response.status})`, 'download_failed', response.status);
|
|
134
129
|
}
|
|
135
130
|
return Buffer.from(await response.arrayBuffer());
|
|
136
131
|
}
|
package/dist/args.js
CHANGED
|
@@ -80,7 +80,7 @@ function flagNumber(args, name) {
|
|
|
80
80
|
return undefined;
|
|
81
81
|
const parsed = Number(value);
|
|
82
82
|
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
83
|
-
throw new Error(
|
|
83
|
+
throw new Error(`The value of --${name} has to be an integer, got "${value}"`);
|
|
84
84
|
}
|
|
85
85
|
return parsed;
|
|
86
86
|
}
|
package/dist/bin.js
CHANGED
|
@@ -6,6 +6,7 @@ const args_1 = require("./args");
|
|
|
6
6
|
const config_1 = require("./config");
|
|
7
7
|
const errors_1 = require("./errors");
|
|
8
8
|
const help_1 = require("./help");
|
|
9
|
+
const limits_1 = require("./limits");
|
|
9
10
|
const ui_1 = require("./ui");
|
|
10
11
|
const zip_1 = require("./zip");
|
|
11
12
|
const version_1 = require("./version");
|
|
@@ -58,7 +59,7 @@ async function run(args) {
|
|
|
58
59
|
await (0, projects_1.list)();
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
|
-
throw new errors_1.CliError(
|
|
62
|
+
throw new errors_1.CliError(`Unknown command: projects ${second}`, 'Available: list and get');
|
|
62
63
|
case 'skills':
|
|
63
64
|
(0, skills_1.skills)(rest);
|
|
64
65
|
return;
|
|
@@ -67,7 +68,7 @@ async function run(args) {
|
|
|
67
68
|
await (0, mcp_1.mcpInstall)(rest);
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
70
|
-
throw new errors_1.CliError(
|
|
71
|
+
throw new errors_1.CliError(`Unknown command: mcp ${second}`, 'Available: install');
|
|
71
72
|
case 'functions':
|
|
72
73
|
if (second === 'deploy') {
|
|
73
74
|
await (0, functions_1.functionsDeploy)(rest);
|
|
@@ -85,7 +86,7 @@ async function run(args) {
|
|
|
85
86
|
await (0, functions_1.functionsList)();
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
|
-
throw new errors_1.CliError(
|
|
89
|
+
throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list, deploy, invoke and logs');
|
|
89
90
|
case 'logs':
|
|
90
91
|
await (0, logs_1.logs)(rest);
|
|
91
92
|
return;
|
|
@@ -106,7 +107,7 @@ async function run(args) {
|
|
|
106
107
|
await (0, env_1.envList)();
|
|
107
108
|
return;
|
|
108
109
|
}
|
|
109
|
-
throw new errors_1.CliError(
|
|
110
|
+
throw new errors_1.CliError(`Unknown command: env ${second}`, 'Available: list, check, set and rm');
|
|
110
111
|
case 'schedules':
|
|
111
112
|
if (second === 'set') {
|
|
112
113
|
await (0, schedules_1.schedulesSet)(rest);
|
|
@@ -120,7 +121,7 @@ async function run(args) {
|
|
|
120
121
|
await (0, schedules_1.schedulesList)();
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
|
-
throw new errors_1.CliError(
|
|
124
|
+
throw new errors_1.CliError(`Unknown command: schedules ${second}`, 'Available: list, set and rm');
|
|
124
125
|
case 'db':
|
|
125
126
|
if (second === 'migrate') {
|
|
126
127
|
await (0, db_1.dbMigrate)(rest);
|
|
@@ -130,7 +131,7 @@ async function run(args) {
|
|
|
130
131
|
await (0, db_1.dbStatus)();
|
|
131
132
|
return;
|
|
132
133
|
}
|
|
133
|
-
throw new errors_1.CliError(
|
|
134
|
+
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status and migrate');
|
|
134
135
|
case 'status':
|
|
135
136
|
await (0, sources_1.status)();
|
|
136
137
|
return;
|
|
@@ -153,7 +154,7 @@ async function run(args) {
|
|
|
153
154
|
await (0, deploy_1.deployments)();
|
|
154
155
|
return;
|
|
155
156
|
default:
|
|
156
|
-
throw new errors_1.CliError(
|
|
157
|
+
throw new errors_1.CliError(`Unknown command: ${first}`, 'The list of commands: xflow help');
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
async function main() {
|
|
@@ -173,6 +174,9 @@ async function main() {
|
|
|
173
174
|
catch (e) {
|
|
174
175
|
if (e instanceof api_1.ApiError) {
|
|
175
176
|
(0, ui_1.fail)(e.message);
|
|
177
|
+
// Отказ по тарифу помечаем явно: иначе агент правит команду, которая верна.
|
|
178
|
+
if (e.limit)
|
|
179
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
|
|
176
180
|
if (e.hint)
|
|
177
181
|
(0, ui_1.note)((0, ui_1.dim)(` ${e.hint}`));
|
|
178
182
|
return 1;
|
package/dist/commands/auth.js
CHANGED
|
@@ -8,6 +8,7 @@ const api_1 = require("../api");
|
|
|
8
8
|
const config_1 = require("../config");
|
|
9
9
|
const credentials_1 = require("../credentials");
|
|
10
10
|
const errors_1 = require("../errors");
|
|
11
|
+
const limits_1 = require("../limits");
|
|
11
12
|
const session_1 = require("../session");
|
|
12
13
|
const ui_1 = require("../ui");
|
|
13
14
|
function localConfig() {
|
|
@@ -38,18 +39,18 @@ async function login() {
|
|
|
38
39
|
body: { client_name: (0, node_os_1.hostname)() },
|
|
39
40
|
});
|
|
40
41
|
(0, ui_1.note)('');
|
|
41
|
-
(0, ui_1.note)(`
|
|
42
|
-
(0, ui_1.note)(`
|
|
43
|
-
(0, ui_1.note)(`
|
|
42
|
+
(0, ui_1.note)(` Confirmation code: ${(0, ui_1.bold)(start.user_code)}`);
|
|
43
|
+
(0, ui_1.note)(` Computer: ${(0, node_os_1.hostname)()}`);
|
|
44
|
+
(0, ui_1.note)(` Sign-in link: ${start.verification_uri_complete}`);
|
|
44
45
|
(0, ui_1.note)('');
|
|
45
46
|
// Браузер не открываем сами: команду часто запускает агент, и всплывшее окно
|
|
46
47
|
// забирает у человека внимание, не объяснив, что он подтверждает.
|
|
47
|
-
(0, ui_1.step)('
|
|
48
|
+
(0, ui_1.step)('Open the link and check the code on the page against the one above');
|
|
48
49
|
const deadline = Date.now() + start.expires_in * 1000;
|
|
49
50
|
let intervalMs = Math.max(start.interval, 1) * 1000;
|
|
50
51
|
for (;;) {
|
|
51
52
|
if (Date.now() > deadline) {
|
|
52
|
-
throw new errors_1.CliError('
|
|
53
|
+
throw new errors_1.CliError('The code has expired', 'Try again: xflow login');
|
|
53
54
|
}
|
|
54
55
|
await sleep(intervalMs);
|
|
55
56
|
let poll;
|
|
@@ -74,41 +75,55 @@ async function login() {
|
|
|
74
75
|
organizationId: poll.organization_id,
|
|
75
76
|
projectId: poll.project_id,
|
|
76
77
|
});
|
|
77
|
-
(0, ui_1.ok)(
|
|
78
|
+
(0, ui_1.ok)(`Signed in, the key is stored for ${client.apiUrl}`);
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
function logout() {
|
|
82
83
|
const apiUrl = (0, config_1.apiUrlFor)(localConfig());
|
|
83
84
|
if ((0, credentials_1.forgetCredential)(apiUrl)) {
|
|
84
|
-
(0, ui_1.ok)(
|
|
85
|
+
(0, ui_1.ok)(`The key for ${apiUrl} is deleted`);
|
|
85
86
|
}
|
|
86
87
|
else {
|
|
87
|
-
(0, ui_1.warn)(
|
|
88
|
+
(0, ui_1.warn)(`There is no stored key for ${apiUrl}`);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
/** Приписка к тарифу. У живой подписки её нет: молчание и значит «всё в порядке». */
|
|
92
|
+
const SUBSCRIPTION_STATE = {
|
|
93
|
+
past_due: ', the period has ended',
|
|
94
|
+
suspended: ', work is suspended',
|
|
95
|
+
terminated: ', the subscription is terminated',
|
|
96
|
+
};
|
|
90
97
|
async function whoami() {
|
|
91
98
|
const config = localConfig();
|
|
92
99
|
const client = (0, session_1.connect)(config);
|
|
93
100
|
const me = await (0, api_1.apiJson)(client, '/api/v1/me');
|
|
94
|
-
(0, ui_1.out)(
|
|
95
|
-
(0, ui_1.out)(
|
|
96
|
-
(0, ui_1.out)(
|
|
101
|
+
(0, ui_1.out)(`Organization: ${me.organization.name ?? me.organization.id}`);
|
|
102
|
+
(0, ui_1.out)(`Key: xfk_${me.key.prefix}… (${me.key.scopes.join(', ')})`);
|
|
103
|
+
(0, ui_1.out)(`Platform: ${client.apiUrl}`);
|
|
97
104
|
if (process.env.XFLOW_TOKEN) {
|
|
98
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
105
|
+
(0, ui_1.note)((0, ui_1.dim)(' The key comes from XFLOW_TOKEN, the one stored in ~/.xflow is not used'));
|
|
99
106
|
}
|
|
100
107
|
else if (!(0, credentials_1.readCredential)(client.apiUrl)) {
|
|
101
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
108
|
+
(0, ui_1.note)((0, ui_1.dim)(' The key is not stored'));
|
|
102
109
|
}
|
|
103
110
|
if (me.billing) {
|
|
111
|
+
const pkg = me.billing.package_title ? `, ${me.billing.package_title}` : '';
|
|
104
112
|
(0, ui_1.out)('');
|
|
105
|
-
(0, ui_1.out)(
|
|
113
|
+
(0, ui_1.out)(`Plan: ${me.billing.plan_title}${pkg}${SUBSCRIPTION_STATE[me.billing.status] ?? ''}`);
|
|
106
114
|
}
|
|
107
115
|
if (me.quotas) {
|
|
108
116
|
(0, ui_1.out)('');
|
|
109
|
-
(0, ui_1.out)('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
(0, ui_1.out)('Plan limits:');
|
|
118
|
+
(0, ui_1.table)((0, limits_1.quotaRows)(me.quotas));
|
|
119
|
+
}
|
|
120
|
+
if (me.features) {
|
|
121
|
+
const interval = me.features.schedule_min_interval_min;
|
|
122
|
+
(0, ui_1.out)('');
|
|
123
|
+
(0, ui_1.out)(`Schedules: ${me.features.schedules
|
|
124
|
+
? interval
|
|
125
|
+
? `yes, no more often than one run every ${interval} min`
|
|
126
|
+
: 'yes'
|
|
127
|
+
: 'no'}`);
|
|
113
128
|
}
|
|
114
129
|
}
|
package/dist/commands/db.js
CHANGED
|
@@ -38,7 +38,7 @@ function readMigrations(root) {
|
|
|
38
38
|
function requireMigrations(root) {
|
|
39
39
|
const migrations = readMigrations(root);
|
|
40
40
|
if (migrations.length === 0) {
|
|
41
|
-
throw new errors_1.CliError(
|
|
41
|
+
throw new errors_1.CliError(`The project has no migrations`, `Put the SQL in ${MIGRATIONS_DIR}/0001_init.sql and try again`);
|
|
42
42
|
}
|
|
43
43
|
return migrations;
|
|
44
44
|
}
|
|
@@ -48,23 +48,23 @@ async function dbStatus() {
|
|
|
48
48
|
const local = readMigrations(root);
|
|
49
49
|
const history = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`);
|
|
50
50
|
const applied = new Map(history.applied.map((row) => [row.name, row]));
|
|
51
|
-
(0, ui_1.out)(
|
|
51
|
+
(0, ui_1.out)(`Schema: ${(0, ui_1.bold)(history.schema)}`);
|
|
52
52
|
(0, ui_1.out)('');
|
|
53
53
|
if (local.length === 0 && applied.size === 0) {
|
|
54
|
-
(0, ui_1.note)(
|
|
54
|
+
(0, ui_1.note)(`No migrations. The first one: ${MIGRATIONS_DIR}/0001_init.sql`);
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
const rows = [];
|
|
58
58
|
for (const migration of local) {
|
|
59
59
|
const row = applied.get(migration.name);
|
|
60
60
|
if (!row) {
|
|
61
|
-
rows.push([migration.name, '
|
|
61
|
+
rows.push([migration.name, 'waiting', '']);
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
64
|
const changed = row.checksum !== checksum(migration.sql);
|
|
65
65
|
rows.push([
|
|
66
66
|
migration.name,
|
|
67
|
-
changed ? '
|
|
67
|
+
changed ? 'applied, file changed' : 'applied',
|
|
68
68
|
(0, ui_1.formatAge)(row.applied_at),
|
|
69
69
|
]);
|
|
70
70
|
}
|
|
@@ -73,12 +73,12 @@ async function dbStatus() {
|
|
|
73
73
|
const localNames = new Set(local.map((migration) => migration.name));
|
|
74
74
|
for (const row of history.applied) {
|
|
75
75
|
if (!localNames.has(row.name))
|
|
76
|
-
rows.push([row.name, '
|
|
76
|
+
rows.push([row.name, 'applied, file missing', (0, ui_1.formatAge)(row.applied_at)]);
|
|
77
77
|
}
|
|
78
78
|
(0, ui_1.table)(rows);
|
|
79
|
-
if (rows.some((row) => row[1] === '
|
|
79
|
+
if (rows.some((row) => row[1] === 'applied, file changed')) {
|
|
80
80
|
(0, ui_1.out)('');
|
|
81
|
-
(0, ui_1.warn)('
|
|
81
|
+
(0, ui_1.warn)('A changed file will not be applied again: write a new migration');
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
async function dbMigrate(args) {
|
|
@@ -86,7 +86,7 @@ async function dbMigrate(args) {
|
|
|
86
86
|
const client = (0, session_1.connect)(config);
|
|
87
87
|
const migrations = requireMigrations(root);
|
|
88
88
|
const dryRun = (0, args_1.flagBool)(args, 'dry-run');
|
|
89
|
-
(0, ui_1.step)(dryRun ?
|
|
89
|
+
(0, ui_1.step)(dryRun ? `Checking the migrations (${migrations.length} in the directory)` : `Applying the migrations (${migrations.length} in the directory)`);
|
|
90
90
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`, {
|
|
91
91
|
method: 'POST',
|
|
92
92
|
body: {
|
|
@@ -97,34 +97,34 @@ async function dbMigrate(args) {
|
|
|
97
97
|
timeoutMs: 300_000,
|
|
98
98
|
});
|
|
99
99
|
if (result.destructive.length > 0) {
|
|
100
|
-
(0, ui_1.warn)('
|
|
100
|
+
(0, ui_1.warn)('Destructive operations:');
|
|
101
101
|
for (const hit of result.destructive)
|
|
102
102
|
(0, ui_1.note)((0, ui_1.dim)(` ${hit}`));
|
|
103
103
|
}
|
|
104
104
|
if (result.dry_run) {
|
|
105
105
|
if (result.pending.length === 0) {
|
|
106
|
-
(0, ui_1.ok)('
|
|
106
|
+
(0, ui_1.ok)('Nothing to apply: every migration is already in the database');
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
109
|
(0, ui_1.out)('');
|
|
110
|
-
(0, ui_1.out)(
|
|
110
|
+
(0, ui_1.out)(`Will be applied (${result.pending.length}):`);
|
|
111
111
|
for (const name of result.pending)
|
|
112
112
|
(0, ui_1.out)(` ${name}`);
|
|
113
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
113
|
+
(0, ui_1.note)((0, ui_1.dim)(' Dry run: the database was not changed'));
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
for (const dump of result.dumps) {
|
|
117
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
117
|
+
(0, ui_1.note)((0, ui_1.dim)(` Dump of ${dump.table}: ${dump.rows} rows${dump.truncated ? ' (cut at the cap)' : ''}, kept for ${result.dump_retention_days} days`));
|
|
118
118
|
}
|
|
119
119
|
if (result.applied.length > 0) {
|
|
120
|
-
(0, ui_1.ok)(
|
|
120
|
+
(0, ui_1.ok)(`Applied: ${result.applied.join(', ')}`);
|
|
121
121
|
}
|
|
122
122
|
else if (!result.failed) {
|
|
123
|
-
(0, ui_1.ok)('
|
|
123
|
+
(0, ui_1.ok)('Nothing to apply: every migration is already in the database');
|
|
124
124
|
}
|
|
125
125
|
if (result.failed) {
|
|
126
126
|
throw new errors_1.CliError(`${result.failed.name}: ${result.failed.error}`, result.applied.length > 0
|
|
127
|
-
?
|
|
128
|
-
: '
|
|
127
|
+
? `What ran before it stays in the database (${result.applied.join(', ')}), the rest did not run`
|
|
128
|
+
: 'The database is unchanged: the migration rolled back as a whole');
|
|
129
129
|
}
|
|
130
130
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -53,7 +53,7 @@ async function waitForBuild(client, projectId, started) {
|
|
|
53
53
|
if (build.phase === 'done' || build.phase === 'failed')
|
|
54
54
|
return build;
|
|
55
55
|
}
|
|
56
|
-
throw new errors_1.CliError('
|
|
56
|
+
throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Version ${started.deploy_id}`);
|
|
57
57
|
}
|
|
58
58
|
async function deploy(args) {
|
|
59
59
|
const { root, config } = (0, config_1.requireProject)();
|
|
@@ -62,15 +62,15 @@ async function deploy(args) {
|
|
|
62
62
|
if ((0, args_1.flagBool)(args, 'no-push')) {
|
|
63
63
|
const server = await (0, sources_1.latestRevision)(client, config.projectId);
|
|
64
64
|
if (!server) {
|
|
65
|
-
throw new errors_1.CliError('
|
|
65
|
+
throw new errors_1.CliError('The server holds no sources', 'Send them: xflow push');
|
|
66
66
|
}
|
|
67
67
|
revision = server.revision;
|
|
68
|
-
(0, ui_1.warn)(
|
|
68
|
+
(0, ui_1.warn)(`Sources not sent (--no-push), the build runs from revision ${revision}`);
|
|
69
69
|
}
|
|
70
70
|
else {
|
|
71
71
|
revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
72
72
|
}
|
|
73
|
-
(0, ui_1.step)(
|
|
73
|
+
(0, ui_1.step)(`Building on the platform from revision ${revision}`);
|
|
74
74
|
let started;
|
|
75
75
|
try {
|
|
76
76
|
started = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/builds`, {
|
|
@@ -89,22 +89,22 @@ async function deploy(args) {
|
|
|
89
89
|
(0, ui_1.out)();
|
|
90
90
|
(0, ui_1.out)(build.log_tail);
|
|
91
91
|
}
|
|
92
|
-
throw new errors_1.CliError(build.error || '
|
|
92
|
+
throw new errors_1.CliError(build.error || 'The build failed', `Fix the code and try again: xflow deploy. Version ${build.deploy_id}`);
|
|
93
93
|
}
|
|
94
|
-
(0, ui_1.ok)(
|
|
94
|
+
(0, ui_1.ok)(`Version ${build.deploy_id} built from revision ${build.revision} in ${build.elapsed_s} s`);
|
|
95
95
|
(0, ui_1.out)(build.project_url);
|
|
96
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
96
|
+
(0, ui_1.note)((0, ui_1.dim)(' Show it to visitors: xflow publish'));
|
|
97
97
|
}
|
|
98
98
|
async function publish() {
|
|
99
99
|
const { config } = (0, config_1.requireProject)();
|
|
100
100
|
const client = (0, session_1.connect)(config);
|
|
101
|
-
(0, ui_1.step)('
|
|
101
|
+
(0, ui_1.step)('Publishing the current dev version');
|
|
102
102
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/publish`, { method: 'POST', timeoutMs: 120_000 });
|
|
103
103
|
if (result.already_published) {
|
|
104
|
-
(0, ui_1.ok)('
|
|
104
|
+
(0, ui_1.ok)('This version is already published');
|
|
105
105
|
}
|
|
106
106
|
else {
|
|
107
|
-
(0, ui_1.ok)(
|
|
107
|
+
(0, ui_1.ok)(`Version ${result.deploy_id} published: visitors see it now`);
|
|
108
108
|
}
|
|
109
109
|
(0, ui_1.out)(result.project_url);
|
|
110
110
|
}
|
|
@@ -112,29 +112,29 @@ async function rollback(args) {
|
|
|
112
112
|
const { config } = (0, config_1.requireProject)();
|
|
113
113
|
const deployId = args.words[0];
|
|
114
114
|
if (!deployId) {
|
|
115
|
-
throw new errors_1.CliError('
|
|
115
|
+
throw new errors_1.CliError('A version number is required', 'To see the versions: xflow deployments. For example: xflow rollback 481203');
|
|
116
116
|
}
|
|
117
117
|
const client = (0, session_1.connect)(config);
|
|
118
118
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/rollback`, { method: 'POST', body: { deploy_id: deployId } });
|
|
119
|
-
(0, ui_1.ok)(`dev
|
|
119
|
+
(0, ui_1.ok)(`The dev version of the project is switched to ${result.deploy_id}`);
|
|
120
120
|
(0, ui_1.out)(result.project_url);
|
|
121
121
|
if (result.revision !== null) {
|
|
122
|
-
(0, ui_1.note)((0, ui_1.dim)(`
|
|
122
|
+
(0, ui_1.note)((0, ui_1.dim)(` The code of this version: xflow pull --revision ${result.revision} --into ./v${result.deploy_id}`));
|
|
123
123
|
}
|
|
124
|
-
(0, ui_1.note)((0, ui_1.dim)('
|
|
124
|
+
(0, ui_1.note)((0, ui_1.dim)(' Visitors see it only after xflow publish'));
|
|
125
125
|
}
|
|
126
126
|
async function deployments() {
|
|
127
127
|
const { config } = (0, config_1.requireProject)();
|
|
128
128
|
const client = (0, session_1.connect)(config);
|
|
129
129
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/deployments`);
|
|
130
130
|
if (data.deployments.length === 0) {
|
|
131
|
-
(0, ui_1.note)('
|
|
131
|
+
(0, ui_1.note)('No versions yet. To build and release: xflow deploy');
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
(0, ui_1.table)(data.deployments.map((d) => [
|
|
135
|
-
d.deploy_id ?? '
|
|
136
|
-
d.revision !== null ?
|
|
137
|
-
d.status === 'deployed' ? '
|
|
135
|
+
d.deploy_id ?? '-',
|
|
136
|
+
d.revision !== null ? `revision ${d.revision}` : 'no revision',
|
|
137
|
+
d.status === 'deployed' ? 'ready' : d.status,
|
|
138
138
|
(0, ui_1.formatAge)(d.deployed_at ?? d.created_at),
|
|
139
139
|
[d.is_dev ? (0, ui_1.bold)('dev') : '', d.is_live ? (0, ui_1.bold)('live') : ''].filter(Boolean).join(' '),
|
|
140
140
|
]));
|