@getxflow/cli 0.34.2 → 1.0.0
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 +1 -1
- package/dist/api.js +21 -6
- package/dist/args.js +1 -1
- package/dist/bin.js +10 -3
- package/dist/commands/connections.js +4 -4
- package/dist/commands/db.js +26 -63
- package/dist/commands/deploy.js +170 -216
- package/dist/commands/dev.js +15 -29
- package/dist/commands/env.js +6 -3
- package/dist/commands/functions.js +27 -119
- package/dist/commands/logs.js +19 -15
- package/dist/commands/pages.js +1 -0
- package/dist/commands/projects.js +134 -190
- package/dist/commands/sources.js +218 -120
- package/dist/commands/storage.js +2 -2
- package/dist/config.js +82 -26
- package/dist/flags.js +19 -12
- package/dist/help.js +210 -209
- package/dist/limits.js +6 -6
- package/dist/session.js +167 -52
- package/dist/template.js +29 -125
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/skills/xflow/SKILL.md +682 -740
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ else's code belongs next to it.
|
|
|
18
18
|
|---|---|
|
|
19
19
|
| `login` / `logout` / `whoami` | sign in through the browser, sign out, whose key this is and what it can do |
|
|
20
20
|
| `org` / `org switch` | organizations with a stored key, and which one this folder works with |
|
|
21
|
-
| `init` / `link`
|
|
21
|
+
| `init` / `link` | new project, link a folder to an existing one |
|
|
22
22
|
| `status` / `pull` | state of the sources on the server and fetching them back |
|
|
23
23
|
| `deploy` / `publish` / `rollback` / `deployments` | build, publish, serve earlier pages, version history |
|
|
24
24
|
| `db status` / `db migrate` | migrations from `migrations/*.sql` with a gate on destructive ones |
|
package/dist/api.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.ApiError = void 0;
|
|
|
4
4
|
exports.isOlder = isOlder;
|
|
5
5
|
exports.newerVersionSeen = newerVersionSeen;
|
|
6
6
|
exports.apiJson = apiJson;
|
|
7
|
-
exports.
|
|
7
|
+
exports.apiForward = apiForward;
|
|
8
8
|
exports.apiUpload = apiUpload;
|
|
9
9
|
exports.downloadUrl = downloadUrl;
|
|
10
10
|
const version_1 = require("./version");
|
|
@@ -61,6 +61,8 @@ function assertProtocol(response) {
|
|
|
61
61
|
const latest = response.headers.get('x-xflow-cli-latest');
|
|
62
62
|
if (latest && isOlder(version_1.CLI_VERSION, latest))
|
|
63
63
|
newerVersion = latest;
|
|
64
|
+
// The platform refuses an outdated CLI itself (outdated_cli) before doing any work;
|
|
65
|
+
// this check keeps the same words for a platform that only sends the header.
|
|
64
66
|
const required = response.headers.get('x-xflow-cli-min');
|
|
65
67
|
if (required && isOlder(version_1.CLI_VERSION, required)) {
|
|
66
68
|
throw new ApiError(`The platform requires CLI ${required} or newer, ${version_1.CLI_VERSION} is installed`, 'outdated_cli', response.status, 'Run: xflow update');
|
|
@@ -141,11 +143,24 @@ async function apiJson(client, path, options = {}) {
|
|
|
141
143
|
throw await readError(response, client);
|
|
142
144
|
return (await response.json());
|
|
143
145
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
/**
|
|
147
|
+
* A function call through the platform: the answer of the function comes back as it
|
|
148
|
+
* is (X-Xflow-Fn-Forwarded), whatever its status. A refusal of the platform itself is
|
|
149
|
+
* an ordinary error.
|
|
150
|
+
*/
|
|
151
|
+
async function apiForward(client, path, options = {}) {
|
|
152
|
+
const response = await send(client, path, options);
|
|
153
|
+
if (response.headers.get('x-xflow-fn-forwarded') !== '1') {
|
|
154
|
+
if (!response.ok)
|
|
155
|
+
throw await readError(response, client);
|
|
156
|
+
throw new ApiError('The platform did not forward the call', 'unknown', response.status);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
status: response.status,
|
|
160
|
+
body: await response.text(),
|
|
161
|
+
elapsedMs: Number(response.headers.get('x-xflow-elapsed-ms') ?? 0),
|
|
162
|
+
hint: response.headers.get('x-xflow-hint') ?? undefined,
|
|
163
|
+
};
|
|
149
164
|
}
|
|
150
165
|
async function apiUpload(client, path, body, headers = {}) {
|
|
151
166
|
return apiJson(client, path, { method: 'POST', body, headers, timeoutMs: TRANSFER_TIMEOUT_MS });
|
package/dist/args.js
CHANGED
package/dist/bin.js
CHANGED
|
@@ -64,9 +64,6 @@ async function run(args) {
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
throw new errors_1.CliError(`Unknown command: org ${second}`, 'Available: list, members and switch');
|
|
67
|
-
case 'templates':
|
|
68
|
-
await (0, projects_1.templates)();
|
|
69
|
-
return;
|
|
70
67
|
case 'init':
|
|
71
68
|
await (0, projects_1.init)(rest);
|
|
72
69
|
return;
|
|
@@ -83,6 +80,16 @@ async function run(args) {
|
|
|
83
80
|
return;
|
|
84
81
|
}
|
|
85
82
|
throw new errors_1.CliError(`Unknown command: projects ${second}`, 'Available: list and get');
|
|
83
|
+
case 'apps':
|
|
84
|
+
if (second === 'add') {
|
|
85
|
+
await (0, projects_1.appsAdd)({ ...args, words: args.words.slice(2) });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (second === undefined || second === 'list') {
|
|
89
|
+
await (0, projects_1.appsList)(rest);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
throw new errors_1.CliError(`Unknown command: apps ${second}`, 'Available: list and add');
|
|
86
93
|
case 'skills':
|
|
87
94
|
await (0, skills_1.skills)(rest);
|
|
88
95
|
return;
|
|
@@ -21,11 +21,11 @@ function state(row) {
|
|
|
21
21
|
return 'linked but switched off, the functions get nothing';
|
|
22
22
|
switch (row.health) {
|
|
23
23
|
case 'expiring':
|
|
24
|
-
return `linked, token expires in ${row.days_left} days,
|
|
24
|
+
return `linked, token expires in ${row.days_left} days, xflow deploy functions renews it`;
|
|
25
25
|
case 'expired':
|
|
26
26
|
return 'linked, access has expired, reconnect the account';
|
|
27
27
|
case 'revoked':
|
|
28
|
-
return 'linked, access was revoked by the provider,
|
|
28
|
+
return 'linked, access was revoked by the provider, another deploy will not help';
|
|
29
29
|
case 'ok':
|
|
30
30
|
return row.days_left === null ? 'linked' : `linked, token valid ${row.days_left} days`;
|
|
31
31
|
default:
|
|
@@ -83,7 +83,7 @@ async function connectionsList(args) {
|
|
|
83
83
|
const variables = data.connections.flatMap((row) => row.env);
|
|
84
84
|
if (variables.length > 0) {
|
|
85
85
|
(0, ui_1.note)((0, ui_1.dim)(` The functions get: ${variables.join(', ')}`));
|
|
86
|
-
(0, ui_1.note)((0, ui_1.dim)(' A change reaches a function on its next deploy: xflow deploy'));
|
|
86
|
+
(0, ui_1.note)((0, ui_1.dim)(' A change reaches a function on its next deploy: xflow deploy functions'));
|
|
87
87
|
}
|
|
88
88
|
if (data.connections.some((row) => placeholders(row))) {
|
|
89
89
|
(0, ui_1.note)((0, ui_1.dim)(' The fourth column is what a call fills in: xflow connections call <name> <path>'));
|
|
@@ -111,7 +111,7 @@ async function connectionsLink(args) {
|
|
|
111
111
|
(0, ui_1.ok)(`${(0, ui_1.bold)(result.label)} linked as ${result.alias}`);
|
|
112
112
|
if (result.env.length > 0)
|
|
113
113
|
(0, ui_1.note)((0, ui_1.dim)(` The functions will get: ${result.env.join(', ')}`));
|
|
114
|
-
(0, ui_1.note)((0, ui_1.dim)(' The values arrive on the next deploy: xflow deploy'));
|
|
114
|
+
(0, ui_1.note)((0, ui_1.dim)(' The values arrive on the next deploy: xflow deploy functions'));
|
|
115
115
|
}
|
|
116
116
|
async function connectionsUnlink(args) {
|
|
117
117
|
const target = args.words[1];
|
package/dist/commands/db.js
CHANGED
|
@@ -7,7 +7,6 @@ exports.dbQuery = dbQuery;
|
|
|
7
7
|
exports.dbMove = dbMove;
|
|
8
8
|
exports.dbMigrate = dbMigrate;
|
|
9
9
|
const node_fs_1 = require("node:fs");
|
|
10
|
-
const node_crypto_1 = require("node:crypto");
|
|
11
10
|
const node_path_1 = require("node:path");
|
|
12
11
|
const api_1 = require("../api");
|
|
13
12
|
const args_1 = require("../args");
|
|
@@ -16,6 +15,7 @@ const errors_1 = require("../errors");
|
|
|
16
15
|
const session_1 = require("../session");
|
|
17
16
|
const text_file_1 = require("../text-file");
|
|
18
17
|
const ui_1 = require("../ui");
|
|
18
|
+
const sources_1 = require("./sources");
|
|
19
19
|
/** Migration files live in the repo; the applied history lives in the database. */
|
|
20
20
|
const MIGRATIONS_DIR = 'migrations';
|
|
21
21
|
function serverLabel(server) {
|
|
@@ -53,9 +53,12 @@ function printMoving(place) {
|
|
|
53
53
|
(0, ui_1.out)(`Move: ${place.move?.note ?? ''}`);
|
|
54
54
|
(0, ui_1.note)((0, ui_1.dim)(' Migrations and queries wait until it is over; the platform finishes it on its own'));
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const STATE_LABEL = {
|
|
57
|
+
pending: 'waiting',
|
|
58
|
+
applied: 'applied',
|
|
59
|
+
changed: 'applied, file changed',
|
|
60
|
+
missing: 'applied, file missing',
|
|
61
|
+
};
|
|
59
62
|
/** File-name order is the queue. */
|
|
60
63
|
function readMigrations(root) {
|
|
61
64
|
const dir = (0, node_path_1.join)(root, MIGRATIONS_DIR);
|
|
@@ -100,35 +103,18 @@ async function dbStatus(args) {
|
|
|
100
103
|
const place = await projectDatabase(client, config.projectId);
|
|
101
104
|
if (place && moving(place.move))
|
|
102
105
|
return printMoving(place);
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
const status = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/status`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: { migrations: local },
|
|
109
|
+
timeoutMs: 60_000,
|
|
110
|
+
});
|
|
111
|
+
printPlace({ schema: status.schema, server: status.server, applied: [] }, place);
|
|
112
|
+
if (status.migrations.length === 0) {
|
|
107
113
|
(0, ui_1.note)(`No migrations. The first one: ${MIGRATIONS_DIR}/0001_init.sql`);
|
|
108
114
|
return;
|
|
109
115
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const row = applied.get(migration.name);
|
|
113
|
-
if (!row) {
|
|
114
|
-
rows.push([migration.name, 'waiting', '']);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
const changed = row.checksum !== checksum(migration.sql);
|
|
118
|
-
rows.push([
|
|
119
|
-
migration.name,
|
|
120
|
-
changed ? 'applied, file changed' : 'applied',
|
|
121
|
-
(0, ui_1.formatAge)(row.applied_at),
|
|
122
|
-
]);
|
|
123
|
-
}
|
|
124
|
-
// Applied but missing locally: usually another project on the same logical database.
|
|
125
|
-
const localNames = new Set(local.map((migration) => migration.name));
|
|
126
|
-
for (const row of history.applied) {
|
|
127
|
-
if (!localNames.has(row.name))
|
|
128
|
-
rows.push([row.name, 'applied, file missing', (0, ui_1.formatAge)(row.applied_at)]);
|
|
129
|
-
}
|
|
130
|
-
(0, ui_1.table)(rows);
|
|
131
|
-
if (rows.some((row) => row[1] === 'applied, file changed')) {
|
|
116
|
+
(0, ui_1.table)(status.migrations.map((row) => [row.name, STATE_LABEL[row.state], row.applied_at ? (0, ui_1.formatAge)(row.applied_at) : '']));
|
|
117
|
+
if (status.migrations.some((row) => row.state === 'changed')) {
|
|
132
118
|
(0, ui_1.out)('');
|
|
133
119
|
(0, ui_1.warn)('A changed file is never applied again and db migrate refuses it: write a new migration');
|
|
134
120
|
}
|
|
@@ -145,19 +131,7 @@ function serverRow(server) {
|
|
|
145
131
|
`${server.databases} ${server.databases === 1 ? 'database' : 'databases'}`,
|
|
146
132
|
];
|
|
147
133
|
}
|
|
148
|
-
|
|
149
|
-
const names = database.projects.map((project) => project.name);
|
|
150
|
-
if (database.hidden_projects > 0)
|
|
151
|
-
names.push(`+${database.hidden_projects} without access`);
|
|
152
|
-
return names.length === 0 ? 'no projects' : names.join(', ');
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Logical databases of the organization, and who is attached to them.
|
|
156
|
-
*
|
|
157
|
-
* The identifier is what `xflow init --database <id>` asks for, and the list of
|
|
158
|
-
* projects is not decoration: a database can be shared, so a migration written
|
|
159
|
-
* for one application lands in the others too.
|
|
160
|
-
*/
|
|
134
|
+
/** The databases of the projects of the organization, and the servers they can live on. */
|
|
161
135
|
async function dbList() {
|
|
162
136
|
const client = (0, session_1.connect)((0, config_1.localConfig)());
|
|
163
137
|
const data = await (0, api_1.apiJson)(client, '/api/v1/org/databases');
|
|
@@ -174,19 +148,10 @@ function moveLine(move) {
|
|
|
174
148
|
return move.finished_at ? `${move.note} (${(0, ui_1.formatAge)(move.finished_at)})` : move.note;
|
|
175
149
|
}
|
|
176
150
|
function printDatabases(databases, total, truncated) {
|
|
177
|
-
(0, ui_1.table)(databases.map((database) => [
|
|
178
|
-
database.name,
|
|
179
|
-
database.id,
|
|
180
|
-
database.schema ?? '-',
|
|
181
|
-
serverLabel(database.server),
|
|
182
|
-
attachedProjects(database),
|
|
183
|
-
]));
|
|
151
|
+
(0, ui_1.table)(databases.map((database) => [database.name, database.id, database.schema ?? '-', serverLabel(database.server)]));
|
|
184
152
|
if (truncated)
|
|
185
153
|
(0, ui_1.note)((0, ui_1.dim)(` Shown ${databases.length} of ${total}`));
|
|
186
154
|
for (const database of databases) {
|
|
187
|
-
const attached = database.projects.length + database.hidden_projects;
|
|
188
|
-
if (attached > 1)
|
|
189
|
-
(0, ui_1.note)((0, ui_1.dim)(` ${database.name} is shared by ${attached} projects: a migration affects all of them`));
|
|
190
155
|
if (database.move)
|
|
191
156
|
(0, ui_1.note)((0, ui_1.dim)(` ${database.name}: ${moveLine(database.move)}`));
|
|
192
157
|
}
|
|
@@ -196,7 +161,7 @@ function printDatabases(databases, total, truncated) {
|
|
|
196
161
|
down.set(server.id, server);
|
|
197
162
|
for (const server of down.values()) {
|
|
198
163
|
(0, ui_1.note)((0, ui_1.dim)(` Server "${server.name}" is not running (${server.status}): its databases do not answer until it is back. ` +
|
|
199
|
-
'Not fixable from the code: tell the person, the server state is in the
|
|
164
|
+
'Not fixable from the code: tell the person, the server state is in the «Базы данных» (Databases) section of the web interface'));
|
|
200
165
|
}
|
|
201
166
|
}
|
|
202
167
|
/** Where a new or a moved database can go, with the room on each. Only the platform: no choice to show. */
|
|
@@ -206,7 +171,7 @@ function printServers(servers) {
|
|
|
206
171
|
(0, ui_1.out)('');
|
|
207
172
|
(0, ui_1.out)((0, ui_1.bold)('Servers'));
|
|
208
173
|
(0, ui_1.table)(servers.map(serverRow));
|
|
209
|
-
(0, ui_1.note)((0, ui_1.dim)(' Without --server a new database
|
|
174
|
+
(0, ui_1.note)((0, ui_1.dim)(' Without --server a new project keeps its database on the platform server. To put it on another: xflow init --server <name>, or xflow db move --to <name> for one that already exists'));
|
|
210
175
|
}
|
|
211
176
|
/** Long values would tear the columns apart; the cut is marked, never silent. */
|
|
212
177
|
const CELL_LIMIT = 60;
|
|
@@ -219,13 +184,7 @@ function cell(value) {
|
|
|
219
184
|
const text = plain(value);
|
|
220
185
|
return text.length > CELL_LIMIT ? `${text.slice(0, CELL_LIMIT - 1)}…` : text;
|
|
221
186
|
}
|
|
222
|
-
/**
|
|
223
|
-
* What the database looks like right now.
|
|
224
|
-
*
|
|
225
|
-
* Not the same question as the migrations/ directory: one logical database is
|
|
226
|
-
* shared by several projects, so the files answer "what did I do", not "what is
|
|
227
|
-
* in there".
|
|
228
|
-
*/
|
|
187
|
+
/** What the database looks like right now, as opposed to what the migrations/ files say was done. */
|
|
229
188
|
async function dbSchema(args) {
|
|
230
189
|
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
231
190
|
const wanted = args.words[0];
|
|
@@ -342,7 +301,7 @@ async function dbMove(args) {
|
|
|
342
301
|
// Said in the words of this command: the flag, and the name that goes in it.
|
|
343
302
|
if (e instanceof api_1.ApiError && e.code === 'confirm_required') {
|
|
344
303
|
const found = await projectDatabase(client, projectId).catch(() => null);
|
|
345
|
-
const name = found?.database?.name ?? '<
|
|
304
|
+
const name = found?.database?.name ?? '<project name>';
|
|
346
305
|
throw new errors_1.CliError(e.message, `To move it: xflow db move --to "${to}" --confirm "${name}"`);
|
|
347
306
|
}
|
|
348
307
|
throw e;
|
|
@@ -376,6 +335,10 @@ async function dbMigrate(args) {
|
|
|
376
335
|
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
377
336
|
const migrations = requireMigrations(root);
|
|
378
337
|
const dryRun = (0, args_1.flagBool)(args, 'dry-run');
|
|
338
|
+
// Migrations live in the revisions of the project root: without this a project
|
|
339
|
+
// without functions would lose them from its history, and a clone would miss them.
|
|
340
|
+
if (!dryRun)
|
|
341
|
+
await (0, sources_1.pushSources)({ kind: 'project', root, projectId: config.projectId }, client, { force: false });
|
|
379
342
|
(0, ui_1.step)(dryRun ? `Checking the migrations (${migrations.length} in the directory)` : `Applying the migrations (${migrations.length} in the directory)`);
|
|
380
343
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`, {
|
|
381
344
|
method: 'POST',
|