@getxflow/cli 0.27.1 → 0.29.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/dist/api.js +5 -2
- package/dist/bin.js +6 -2
- package/dist/commands/db.js +146 -9
- package/dist/commands/projects.js +6 -1
- package/dist/credentials.js +0 -6
- package/dist/flags.js +2 -1
- package/dist/help.js +32 -5
- package/dist/ui.js +3 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +20 -1
package/dist/api.js
CHANGED
|
@@ -16,7 +16,9 @@ class ApiError extends Error {
|
|
|
16
16
|
limit;
|
|
17
17
|
removing;
|
|
18
18
|
request;
|
|
19
|
-
|
|
19
|
+
/** The platform that answered: its signed-in organizations are the ones worth naming. */
|
|
20
|
+
apiUrl;
|
|
21
|
+
constructor(message, code, status, hint, details = {}, apiUrl) {
|
|
20
22
|
super(message);
|
|
21
23
|
this.name = 'ApiError';
|
|
22
24
|
this.code = code;
|
|
@@ -26,6 +28,7 @@ class ApiError extends Error {
|
|
|
26
28
|
this.limit = details.limit;
|
|
27
29
|
this.removing = details.removing;
|
|
28
30
|
this.request = details.request;
|
|
31
|
+
this.apiUrl = apiUrl;
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
exports.ApiError = ApiError;
|
|
@@ -126,7 +129,7 @@ async function readError(response, client) {
|
|
|
126
129
|
const text = await response.text().catch(() => '');
|
|
127
130
|
try {
|
|
128
131
|
const parsed = JSON.parse(text);
|
|
129
|
-
return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed);
|
|
132
|
+
return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed, client.apiUrl);
|
|
130
133
|
}
|
|
131
134
|
catch {
|
|
132
135
|
return notAnAnswer(response.status, client.apiUrl, text);
|
package/dist/bin.js
CHANGED
|
@@ -176,11 +176,15 @@ async function run(args) {
|
|
|
176
176
|
await (0, db_1.dbQuery)({ ...args, words: args.words.slice(2) });
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
|
+
if (second === 'move') {
|
|
180
|
+
await (0, db_1.dbMove)(rest);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
179
183
|
if (second === undefined || second === 'status') {
|
|
180
184
|
await (0, db_1.dbStatus)({ ...args, words: args.words.slice(2) });
|
|
181
185
|
return;
|
|
182
186
|
}
|
|
183
|
-
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query and
|
|
187
|
+
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query, migrate and move');
|
|
184
188
|
case 'storage':
|
|
185
189
|
if (second === 'push') {
|
|
186
190
|
await (0, storage_1.storagePush)(rest);
|
|
@@ -327,7 +331,7 @@ async function main() {
|
|
|
327
331
|
if (process.env.XFLOW_TOKEN?.trim()) {
|
|
328
332
|
(0, ui_1.fatalNote)((0, ui_1.dim)(' XFLOW_TOKEN is set: it is the key of one organization and is used as it comes, so a project or a page of another one is not found'));
|
|
329
333
|
}
|
|
330
|
-
else if ((0, credentials_1.
|
|
334
|
+
else if (e.apiUrl && (0, credentials_1.listOrgs)(e.apiUrl).length > 1) {
|
|
331
335
|
(0, ui_1.fatalNote)((0, ui_1.dim)(' Several organizations are signed in: the answer may live in another one, see xflow org'));
|
|
332
336
|
}
|
|
333
337
|
}
|
package/dist/commands/db.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.dbStatus = dbStatus;
|
|
|
4
4
|
exports.dbList = dbList;
|
|
5
5
|
exports.dbSchema = dbSchema;
|
|
6
6
|
exports.dbQuery = dbQuery;
|
|
7
|
+
exports.dbMove = dbMove;
|
|
7
8
|
exports.dbMigrate = dbMigrate;
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
9
10
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -17,6 +18,41 @@ const text_file_1 = require("../text-file");
|
|
|
17
18
|
const ui_1 = require("../ui");
|
|
18
19
|
/** Migration files live in the repo; the applied history lives in the database. */
|
|
19
20
|
const MIGRATIONS_DIR = 'migrations';
|
|
21
|
+
function serverLabel(server) {
|
|
22
|
+
if (server === undefined)
|
|
23
|
+
return '-';
|
|
24
|
+
if (server === null)
|
|
25
|
+
return 'platform';
|
|
26
|
+
return server.status === 'active' ? `own "${server.name}"` : `own "${server.name}", ${server.status}`;
|
|
27
|
+
}
|
|
28
|
+
const moving = (move) => move?.status === 'copying' || move?.status === 'switching';
|
|
29
|
+
/** Asked apart from the history: during a move the database does not answer, this does. */
|
|
30
|
+
async function projectDatabase(client, projectId) {
|
|
31
|
+
try {
|
|
32
|
+
return await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/move`);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
if (e instanceof api_1.ApiError && e.code === 'not_deployed')
|
|
36
|
+
return null;
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function printPlace(history, place) {
|
|
41
|
+
(0, ui_1.out)(`Schema: ${(0, ui_1.bold)(history.schema)}`);
|
|
42
|
+
if (history.server !== undefined)
|
|
43
|
+
(0, ui_1.out)(`Server: ${serverLabel(history.server)}`);
|
|
44
|
+
if (place?.move)
|
|
45
|
+
(0, ui_1.out)(`Move: ${moveLine(place.move)}`);
|
|
46
|
+
(0, ui_1.out)('');
|
|
47
|
+
}
|
|
48
|
+
/** Mid-move the history cannot be read, so the answer is the move itself. */
|
|
49
|
+
function printMoving(place) {
|
|
50
|
+
if (place.database)
|
|
51
|
+
(0, ui_1.out)(`Database: ${(0, ui_1.bold)(place.database.name)}`);
|
|
52
|
+
(0, ui_1.out)(`Server: ${serverLabel(place.server)}`);
|
|
53
|
+
(0, ui_1.out)(`Move: ${place.move?.note ?? ''}`);
|
|
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
|
+
}
|
|
20
56
|
function checksum(sql) {
|
|
21
57
|
return (0, node_crypto_1.createHash)('sha256').update(sql).digest('hex').slice(0, 16);
|
|
22
58
|
}
|
|
@@ -45,9 +81,11 @@ async function dbStatus(args) {
|
|
|
45
81
|
// is the applied history alone, which does not depend on any working copy.
|
|
46
82
|
if ((0, args_1.flagString)(args, 'project') !== undefined) {
|
|
47
83
|
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
84
|
+
const place = await projectDatabase(client, projectId);
|
|
85
|
+
if (place && moving(place.move))
|
|
86
|
+
return printMoving(place);
|
|
48
87
|
const history = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/migrate`);
|
|
49
|
-
(
|
|
50
|
-
(0, ui_1.out)('');
|
|
88
|
+
printPlace(history, place);
|
|
51
89
|
if (history.applied.length === 0) {
|
|
52
90
|
(0, ui_1.note)('No migrations have been applied yet');
|
|
53
91
|
return;
|
|
@@ -59,10 +97,12 @@ async function dbStatus(args) {
|
|
|
59
97
|
const { root, config } = (0, config_1.requireProject)();
|
|
60
98
|
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
61
99
|
const local = readMigrations(root);
|
|
100
|
+
const place = await projectDatabase(client, config.projectId);
|
|
101
|
+
if (place && moving(place.move))
|
|
102
|
+
return printMoving(place);
|
|
62
103
|
const history = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`);
|
|
63
104
|
const applied = new Map(history.applied.map((row) => [row.name, row]));
|
|
64
|
-
(
|
|
65
|
-
(0, ui_1.out)('');
|
|
105
|
+
printPlace(history, place);
|
|
66
106
|
if (local.length === 0 && applied.size === 0) {
|
|
67
107
|
(0, ui_1.note)(`No migrations. The first one: ${MIGRATIONS_DIR}/0001_init.sql`);
|
|
68
108
|
return;
|
|
@@ -93,6 +133,19 @@ async function dbStatus(args) {
|
|
|
93
133
|
(0, ui_1.warn)('A changed file will not be applied again: write a new migration');
|
|
94
134
|
}
|
|
95
135
|
}
|
|
136
|
+
function serverRow(server) {
|
|
137
|
+
const room = server.used_bytes !== null && server.space_bytes !== null
|
|
138
|
+
? `${(0, ui_1.formatBytes)(server.used_bytes)} of ${(0, ui_1.formatBytes)(server.space_bytes)} used`
|
|
139
|
+
: '';
|
|
140
|
+
return [
|
|
141
|
+
server.id === null ? 'platform' : `own "${server.name}"`,
|
|
142
|
+
server.id === null ? 'plan space' : server.tier ?? '',
|
|
143
|
+
server.status === 'active' ? '' : server.status,
|
|
144
|
+
room,
|
|
145
|
+
`${server.databases} ${server.databases === 1 ? 'database' : 'databases'}`,
|
|
146
|
+
server.new_databases_here ? 'new databases go here' : '',
|
|
147
|
+
];
|
|
148
|
+
}
|
|
96
149
|
function attachedProjects(database) {
|
|
97
150
|
const names = database.projects.map((project) => project.name);
|
|
98
151
|
if (database.hidden_projects > 0)
|
|
@@ -112,22 +165,50 @@ async function dbList() {
|
|
|
112
165
|
if (data.databases.length === 0) {
|
|
113
166
|
(0, ui_1.note)('The organization has no databases');
|
|
114
167
|
(0, ui_1.note)((0, ui_1.dim)(' One is created with the first project: xflow init'));
|
|
115
|
-
return;
|
|
116
168
|
}
|
|
117
|
-
|
|
169
|
+
else {
|
|
170
|
+
printDatabases(data.databases, data.total, data.truncated);
|
|
171
|
+
}
|
|
172
|
+
printServers(data.servers);
|
|
173
|
+
}
|
|
174
|
+
function moveLine(move) {
|
|
175
|
+
return move.finished_at ? `${move.note} (${(0, ui_1.formatAge)(move.finished_at)})` : move.note;
|
|
176
|
+
}
|
|
177
|
+
function printDatabases(databases, total, truncated) {
|
|
178
|
+
(0, ui_1.table)(databases.map((database) => [
|
|
118
179
|
database.name,
|
|
119
180
|
database.id,
|
|
120
181
|
database.schema ?? '-',
|
|
182
|
+
serverLabel(database.server),
|
|
121
183
|
attachedProjects(database),
|
|
122
184
|
]));
|
|
123
|
-
if (
|
|
124
|
-
(0, ui_1.note)((0, ui_1.dim)(` Shown ${
|
|
125
|
-
for (const database of
|
|
185
|
+
if (truncated)
|
|
186
|
+
(0, ui_1.note)((0, ui_1.dim)(` Shown ${databases.length} of ${total}`));
|
|
187
|
+
for (const database of databases) {
|
|
126
188
|
const attached = database.projects.length + database.hidden_projects;
|
|
127
189
|
if (attached > 1)
|
|
128
190
|
(0, ui_1.note)((0, ui_1.dim)(` ${database.name} is shared by ${attached} projects: a migration affects all of them`));
|
|
191
|
+
if (database.move)
|
|
192
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${database.name}: ${moveLine(database.move)}`));
|
|
193
|
+
}
|
|
194
|
+
const down = new Map();
|
|
195
|
+
for (const { server } of databases)
|
|
196
|
+
if (server && server.status !== 'active')
|
|
197
|
+
down.set(server.id, server);
|
|
198
|
+
for (const server of down.values()) {
|
|
199
|
+
(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. ` +
|
|
200
|
+
'Not fixable from the code: tell the person, the server state is in the "Data" section of the web interface'));
|
|
129
201
|
}
|
|
130
202
|
}
|
|
203
|
+
/** Where a new or a moved database can go, with the room on each. Only the platform: no choice to show. */
|
|
204
|
+
function printServers(servers) {
|
|
205
|
+
if (!servers || servers.length < 2)
|
|
206
|
+
return;
|
|
207
|
+
(0, ui_1.out)('');
|
|
208
|
+
(0, ui_1.out)((0, ui_1.bold)('Servers'));
|
|
209
|
+
(0, ui_1.table)(servers.map(serverRow));
|
|
210
|
+
(0, ui_1.note)((0, ui_1.dim)(' A new project with its database on one of them: xflow init --server <name>. An existing one: xflow db move --to <name>'));
|
|
211
|
+
}
|
|
131
212
|
/** Long values would tear the columns apart; the cut is marked, never silent. */
|
|
132
213
|
const CELL_LIMIT = 60;
|
|
133
214
|
function plain(value) {
|
|
@@ -235,6 +316,62 @@ async function dbQuery(args) {
|
|
|
235
316
|
: ` Shown the first ${data.rows.length} rows, more exist: narrow the query, with a where clause or an offset`));
|
|
236
317
|
}
|
|
237
318
|
}
|
|
319
|
+
const MOVE_POLL_MS = 3_000;
|
|
320
|
+
/** Past this the command lets go: the move carries on, db status follows it. */
|
|
321
|
+
const MOVE_FOLLOW_MS = 10 * 60_000;
|
|
322
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
323
|
+
/**
|
|
324
|
+
* Moving the project database to another server of the organization. The platform
|
|
325
|
+
* runs the move by itself; this command starts it and follows it to the end.
|
|
326
|
+
*/
|
|
327
|
+
async function dbMove(args) {
|
|
328
|
+
const to = (0, args_1.flagString)(args, 'to');
|
|
329
|
+
if (!to) {
|
|
330
|
+
throw new errors_1.CliError('Name the server: --to <name>', 'The servers and their names: xflow db list ("platform" is the common one)');
|
|
331
|
+
}
|
|
332
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
333
|
+
const path = `/api/v1/projects/${projectId}/db/move`;
|
|
334
|
+
let place;
|
|
335
|
+
try {
|
|
336
|
+
place = await (0, api_1.apiJson)(client, path, {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
body: { to, confirm: (0, args_1.flagString)(args, 'confirm') },
|
|
339
|
+
timeoutMs: 60_000,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
// Said in the words of this command: the flag, and the name that goes in it.
|
|
344
|
+
if (e instanceof api_1.ApiError && e.code === 'confirm_required') {
|
|
345
|
+
const found = await projectDatabase(client, projectId).catch(() => null);
|
|
346
|
+
const name = found?.database?.name ?? '<database>';
|
|
347
|
+
throw new errors_1.CliError(e.message, `To move it: xflow db move --to "${to}" --confirm "${name}"`);
|
|
348
|
+
}
|
|
349
|
+
throw e;
|
|
350
|
+
}
|
|
351
|
+
(0, ui_1.note)((0, ui_1.dim)(' The platform carries the move through by itself: stopping this command does not stop it'));
|
|
352
|
+
const deadline = Date.now() + MOVE_FOLLOW_MS;
|
|
353
|
+
let shown = '';
|
|
354
|
+
while (moving(place.move)) {
|
|
355
|
+
const line = place.move?.note ?? '';
|
|
356
|
+
if (line !== shown)
|
|
357
|
+
(0, ui_1.step)(line);
|
|
358
|
+
shown = line;
|
|
359
|
+
if (Date.now() > deadline) {
|
|
360
|
+
(0, ui_1.note)('Still going: xflow db status follows it');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
await sleep(MOVE_POLL_MS);
|
|
364
|
+
place = await (0, api_1.apiJson)(client, path);
|
|
365
|
+
}
|
|
366
|
+
const { move } = place;
|
|
367
|
+
if (move?.status === 'failed')
|
|
368
|
+
throw new errors_1.CliError(move.note);
|
|
369
|
+
if (move && (move.functions_done ?? 0) < (move.functions_total ?? 0)) {
|
|
370
|
+
(0, ui_1.warn)(move.note);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
(0, ui_1.ok)(move?.note ?? `The database is on ${serverLabel(place.server)}`);
|
|
374
|
+
}
|
|
238
375
|
async function dbMigrate(args) {
|
|
239
376
|
const { root, config } = (0, config_1.requireProject)();
|
|
240
377
|
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
@@ -69,7 +69,8 @@ async function init(args) {
|
|
|
69
69
|
(0, ui_1.step)('Creating the project on the platform');
|
|
70
70
|
const project = await (0, api_1.apiJson)(client, '/api/v1/projects', {
|
|
71
71
|
method: 'POST',
|
|
72
|
-
body: { name, database_id: (0, args_1.flagString)(args, 'database') ?? null },
|
|
72
|
+
body: { name, database_id: (0, args_1.flagString)(args, 'database') ?? null, server: (0, args_1.flagString)(args, 'server') ?? null },
|
|
73
|
+
timeoutMs: 60_000,
|
|
73
74
|
});
|
|
74
75
|
try {
|
|
75
76
|
(0, node_fs_1.mkdirSync)(target, { recursive: true });
|
|
@@ -94,6 +95,10 @@ async function init(args) {
|
|
|
94
95
|
}
|
|
95
96
|
(0, skills_1.installSkillQuietly)(target);
|
|
96
97
|
(0, ui_1.ok)(`Project "${project.name}" created: ${files.length} template files`);
|
|
98
|
+
if (project.database) {
|
|
99
|
+
const { server } = project.database;
|
|
100
|
+
(0, ui_1.note)((0, ui_1.dim)(` Its database "${project.database.name}" is on ${server ? `own "${server.name}"` : 'the platform server'}`));
|
|
101
|
+
}
|
|
97
102
|
(0, ui_1.out)('');
|
|
98
103
|
(0, ui_1.out)(` ${(0, ui_1.bold)('Next:')}`);
|
|
99
104
|
(0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}`);
|
package/dist/credentials.js
CHANGED
|
@@ -7,7 +7,6 @@ exports.rememberOrgName = rememberOrgName;
|
|
|
7
7
|
exports.setActiveOrg = setActiveOrg;
|
|
8
8
|
exports.forgetOrg = forgetOrg;
|
|
9
9
|
exports.forgetAllOrgs = forgetAllOrgs;
|
|
10
|
-
exports.anyMultipleOrgs = anyMultipleOrgs;
|
|
11
10
|
exports.pickKey = pickKey;
|
|
12
11
|
exports.storedKeyInputs = storedKeyInputs;
|
|
13
12
|
const node_fs_1 = require("node:fs");
|
|
@@ -198,11 +197,6 @@ function forgetAllOrgs(apiUrl) {
|
|
|
198
197
|
writeStore(store);
|
|
199
198
|
return true;
|
|
200
199
|
}
|
|
201
|
-
/** True when any address holds keys of more than one organization. */
|
|
202
|
-
function anyMultipleOrgs() {
|
|
203
|
-
const store = readStore();
|
|
204
|
-
return Object.values(store.orgs ?? {}).some((address) => Object.keys(address.keys).length > 1);
|
|
205
|
-
}
|
|
206
200
|
/**
|
|
207
201
|
* One rule for the whole CLI: XFLOW_TOKEN (default address only) beats the
|
|
208
202
|
* folder's organization, which beats the active one. A folder bound to an
|
package/dist/flags.js
CHANGED
|
@@ -40,7 +40,7 @@ exports.KNOWN = {
|
|
|
40
40
|
'org members': [],
|
|
41
41
|
'org switch': [],
|
|
42
42
|
templates: [],
|
|
43
|
-
init: ['name', 'template', 'database'],
|
|
43
|
+
init: ['name', 'template', 'database', 'server'],
|
|
44
44
|
link: [],
|
|
45
45
|
projects: [],
|
|
46
46
|
'projects list': [],
|
|
@@ -75,6 +75,7 @@ exports.KNOWN = {
|
|
|
75
75
|
'db schema': ['project'],
|
|
76
76
|
'db query': ['limit', 'json', 'project'],
|
|
77
77
|
'db migrate': ['dry-run', 'allow-destructive'],
|
|
78
|
+
'db move': ['to', 'confirm', 'project'],
|
|
78
79
|
storage: ['json', 'project'],
|
|
79
80
|
'storage ls': ['json', 'project'],
|
|
80
81
|
'storage list': ['json', 'project'],
|
package/dist/help.js
CHANGED
|
@@ -83,11 +83,13 @@ ${(0, ui_1.bold)('Functions')}
|
|
|
83
83
|
|
|
84
84
|
${(0, ui_1.bold)('Database')}
|
|
85
85
|
xflow db status which migrations are applied and which are waiting
|
|
86
|
-
xflow db list databases of the organization
|
|
86
|
+
xflow db list databases of the organization, servers, projects
|
|
87
87
|
xflow db schema [table] tables of the project, or the columns of one
|
|
88
88
|
xflow db query "select ..." read data, in a read-only transaction
|
|
89
89
|
xflow db migrate [--dry-run] [--allow-destructive]
|
|
90
90
|
apply the migrations from migrations/*.sql
|
|
91
|
+
xflow db move --to <server> --confirm <database>
|
|
92
|
+
move the project database to another server
|
|
91
93
|
|
|
92
94
|
${(0, ui_1.bold)('Pages')}
|
|
93
95
|
xflow pages public sites of the organization at <name>.xflowpage.ru
|
|
@@ -162,7 +164,7 @@ no working copy of its own:
|
|
|
162
164
|
xflow db query --project 3f2a… "select id, title from tasks where done = false"
|
|
163
165
|
xflow functions invoke kanban --project 3f2a… --data-file card.json
|
|
164
166
|
|
|
165
|
-
Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('functions list')},
|
|
167
|
+
Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('db move')}, ${(0, ui_1.bold)('functions list')},
|
|
166
168
|
${(0, ui_1.bold)('functions invoke')}, ${(0, ui_1.bold)('functions logs')}, ${(0, ui_1.bold)('logs')}, ${(0, ui_1.bold)('env')}, ${(0, ui_1.bold)('env rm')},
|
|
167
169
|
${(0, ui_1.bold)('connections')}, ${(0, ui_1.bold)('connections link')}, ${(0, ui_1.bold)('connections unlink')}, ${(0, ui_1.bold)('schedules')},
|
|
168
170
|
${(0, ui_1.bold)('schedules set')}, ${(0, ui_1.bold)('schedules rm')}, ${(0, ui_1.bold)('storage ls')}, ${(0, ui_1.bold)('storage push')},
|
|
@@ -288,17 +290,36 @@ is a hard gate on a changed contract, and until you run this command nothing els
|
|
|
288
290
|
will work.`,
|
|
289
291
|
db: `${(0, ui_1.bold)('xflow db')}: schema, data, migrations
|
|
290
292
|
|
|
291
|
-
xflow db status what is applied
|
|
292
|
-
xflow db list databases of the organization,
|
|
293
|
+
xflow db status what is applied, what is waiting, the server
|
|
294
|
+
xflow db list databases of the organization: servers, projects
|
|
293
295
|
xflow db schema [table] tables of the project, or the columns of one
|
|
294
296
|
xflow db query "select ..." read data (--limit N: 50 by default, 300 at most;
|
|
295
297
|
--json prints the values whole)
|
|
296
298
|
xflow db query <file>.sql the same, the query read from a file
|
|
297
299
|
xflow db migrate apply migrations/*.sql
|
|
300
|
+
xflow db move --to <server> --confirm <database>
|
|
301
|
+
move the project database to another server
|
|
298
302
|
|
|
299
303
|
${(0, ui_1.bold)('list')} is about the organization, not about this project: it names the logical
|
|
300
304
|
databases, the identifier ${(0, ui_1.bold)('xflow init --database <id>')} asks for, and the projects
|
|
301
|
-
attached to each one. That last column is the one to read before a migration.
|
|
305
|
+
attached to each one. That last column is the one to read before a migration. The server
|
|
306
|
+
column says where a database lives: ${(0, ui_1.bold)('platform')} is the common one, ${(0, ui_1.bold)('own "<name>"')} the
|
|
307
|
+
organization's own server. While an own server is not running, its databases do not
|
|
308
|
+
answer, and that is not fixable from the code: its state is in the web interface. An
|
|
309
|
+
organization with own servers also gets them listed, with the room used on each and the
|
|
310
|
+
one new databases go to: that is the choice for ${(0, ui_1.bold)('xflow init --server')} and ${(0, ui_1.bold)('db move')}.
|
|
311
|
+
|
|
312
|
+
${(0, ui_1.bold)('move')} takes the database of this project to another server of the organization:
|
|
313
|
+
${(0, ui_1.bold)('platform')} or an own server by name (or id, when two share a name). The platform copies
|
|
314
|
+
the tables, switches the functions of every project on the database and removes the old
|
|
315
|
+
copy; nothing needs a deploy. While it copies, the applications read data but cannot save
|
|
316
|
+
anything new, usually for a couple of minutes, and that goes for every project on the
|
|
317
|
+
database, which is why ${(0, ui_1.bold)('--confirm')} takes the database name. It also needs the right
|
|
318
|
+
to move databases on the key: off by default, only its owner turns it on, in the platform
|
|
319
|
+
settings where the key permissions live. The command follows the move to the end;
|
|
320
|
+
stopping it does not stop the move, and ${(0, ui_1.bold)('xflow db status')} shows where it stands. A move
|
|
321
|
+
that fails is rolled back: the database stays where it was and works as before. Buying,
|
|
322
|
+
paying for and deleting servers stay with the person, in the web interface.
|
|
302
323
|
|
|
303
324
|
${(0, ui_1.bold)('schema')} answers a different question than the ${(0, ui_1.bold)('migrations/')} directory: one logical
|
|
304
325
|
database is shared by several projects, so the files say what you did, and the schema
|
|
@@ -697,6 +718,12 @@ is not a formality: your own palette on top of them looks foreign inside the pla
|
|
|
697
718
|
--name <name> project name (the folder name by default)
|
|
698
719
|
--template <id> a different template (the list: xflow templates)
|
|
699
720
|
--database <id> attach a logical database of the organization (the list: xflow db list)
|
|
721
|
+
--server <name> a new database for the project, right away on that server:
|
|
722
|
+
platform, or an own server of the organization (xflow db list)
|
|
723
|
+
|
|
724
|
+
Without either flag the project gets its database with the first migration, on the
|
|
725
|
+
server new databases of the organization go to. ${(0, ui_1.bold)('--server')} decides it up front: the
|
|
726
|
+
server has to be running, and the database counts against the plan at once.
|
|
700
727
|
|
|
701
728
|
The order: the project is created on the platform first, because without it there is
|
|
702
729
|
nowhere to get the token for .env. If writing the files fails, the project stays empty
|
package/dist/ui.js
CHANGED
|
@@ -96,7 +96,9 @@ function formatBytes(bytes) {
|
|
|
96
96
|
return `${bytes} B`;
|
|
97
97
|
if (bytes < 1024 * 1024)
|
|
98
98
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
99
|
-
|
|
99
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
100
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
101
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
100
102
|
}
|
|
101
103
|
function formatAge(iso) {
|
|
102
104
|
if (!iso)
|
package/dist/version.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
|
|
4
4
|
/** Keep in sync with cli/package.json. */
|
|
5
|
-
exports.CLI_VERSION = '0.
|
|
5
|
+
exports.CLI_VERSION = '0.29.0';
|
|
6
6
|
/** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
|
|
7
7
|
exports.DEFAULT_API_URL = 'https://app.getxflow.com';
|
package/package.json
CHANGED
package/skills/xflow/SKILL.md
CHANGED
|
@@ -22,7 +22,7 @@ contains the fix.
|
|
|
22
22
|
|
|
23
23
|
## Keeping these instructions current
|
|
24
24
|
|
|
25
|
-
These instructions ship with xflow CLI 0.
|
|
25
|
+
These instructions ship with xflow CLI 0.29.0, so the copy you are reading can be older than
|
|
26
26
|
the CLI answering your commands.
|
|
27
27
|
|
|
28
28
|
When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
|
|
@@ -435,6 +435,25 @@ transaction (50 rows by default, `--limit` up to 300; `--json` prints long value
|
|
|
435
435
|
with double quotes goes in a `.sql` file outside the project, `xflow db query ../q.sql`: PowerShell
|
|
436
436
|
strips them from arguments. Look before you write a migration against a shared database.
|
|
437
437
|
|
|
438
|
+
An organization can have own database servers, several of them for different jobs. `xflow db
|
|
439
|
+
list` names the server of each database (`platform` is the common one) and lists the own servers
|
|
440
|
+
with the room used and the one new databases go to; `xflow db status` names the one under this
|
|
441
|
+
project. The server belongs to the database: projects sharing a database share its server.
|
|
442
|
+
- A new project with its database on a chosen server: `xflow init --server <name>` (`platform`
|
|
443
|
+
for the common one). Without it the first migration creates the database where new ones go.
|
|
444
|
+
- An existing database: `xflow db move --to <name> --confirm <database name>`. Writes to every
|
|
445
|
+
project on it close while it copies, usually a couple of minutes; the platform switches the
|
|
446
|
+
functions itself, no deploy. It needs the right to move databases, off on the key by default:
|
|
447
|
+
refused for the right, say what should move where and why, and the person turns it on or
|
|
448
|
+
moves it in the "Data" section.
|
|
449
|
+
- Buying, paying for and deleting servers are the person's, in the web interface.
|
|
450
|
+
|
|
451
|
+
Code stays the same: `DATABASE_URL` arrives as before, pass it as is, without an `ssl` option (an
|
|
452
|
+
own server is reached over a closed network, and `ssl` makes the connection fail). While an own
|
|
453
|
+
server is not running, migrations and queries say so and functions that use the database time
|
|
454
|
+
out. That is not your code and not yours to fix: tell the person, who sees the server state in
|
|
455
|
+
the "Data" section of the web interface, instead of retrying.
|
|
456
|
+
|
|
438
457
|
## File storage
|
|
439
458
|
|
|
440
459
|
Two kinds of files live here, and they arrive through different doors.
|