@getxflow/cli 1.3.0 → 1.5.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 CHANGED
@@ -10,7 +10,7 @@ cd my-app/apps/my-app && npm install && xflow dev
10
10
  xflow deploy -m "First version"
11
11
  ```
12
12
 
13
- Requires Node 20+. The package has no dependencies: it holds an access key, and nobody
13
+ Requires Node 22+. The package has no dependencies: it holds an access key, and nobody
14
14
  else's code belongs next to it.
15
15
 
16
16
  | Command | What it does |
package/dist/bin.js CHANGED
@@ -180,15 +180,11 @@ async function run(args) {
180
180
  await (0, db_1.dbExec)({ ...args, words: args.words.slice(2) });
181
181
  return;
182
182
  }
183
- if (second === 'move') {
184
- await (0, db_1.dbMove)(rest);
185
- return;
186
- }
187
183
  if (second === undefined || second === 'status') {
188
184
  await (0, db_1.dbStatus)({ ...args, words: args.words.slice(2) });
189
185
  return;
190
186
  }
191
- throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query, exec, migrate and move');
187
+ throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query, exec and migrate');
192
188
  case 'storage':
193
189
  if (second === 'push') {
194
190
  await (0, storage_1.storagePush)(rest);
@@ -5,7 +5,6 @@ exports.dbList = dbList;
5
5
  exports.dbSchema = dbSchema;
6
6
  exports.dbQuery = dbQuery;
7
7
  exports.dbExec = dbExec;
8
- exports.dbMove = dbMove;
9
8
  exports.dbMigrate = dbMigrate;
10
9
  exports.migrateBeforeDeploy = migrateBeforeDeploy;
11
10
  const node_fs_1 = require("node:fs");
@@ -20,41 +19,6 @@ const ui_1 = require("../ui");
20
19
  const sources_1 = require("./sources");
21
20
  /** Migration files live in the repo; the applied history lives in the database. */
22
21
  const MIGRATIONS_DIR = 'migrations';
23
- function serverLabel(server) {
24
- if (server === undefined)
25
- return '-';
26
- if (server === null)
27
- return 'platform';
28
- return server.status === 'active' ? `own "${server.name}"` : `own "${server.name}", ${server.status}`;
29
- }
30
- const moving = (move) => move?.status === 'copying' || move?.status === 'switching';
31
- /** Asked apart from the history: during a move the database does not answer, this does. */
32
- async function projectDatabase(client, projectId) {
33
- try {
34
- return await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/move`);
35
- }
36
- catch (e) {
37
- if (e instanceof api_1.ApiError && e.code === 'not_deployed')
38
- return null;
39
- throw e;
40
- }
41
- }
42
- function printPlace(history, place) {
43
- (0, ui_1.out)(`Schema: ${(0, ui_1.bold)(history.schema)}`);
44
- if (history.server !== undefined)
45
- (0, ui_1.out)(`Server: ${serverLabel(history.server)}`);
46
- if (place?.move)
47
- (0, ui_1.out)(`Move: ${moveLine(place.move)}`);
48
- (0, ui_1.out)('');
49
- }
50
- /** Mid-move the history cannot be read, so the answer is the move itself. */
51
- function printMoving(place) {
52
- if (place.database)
53
- (0, ui_1.out)(`Database: ${(0, ui_1.bold)(place.database.name)}`);
54
- (0, ui_1.out)(`Server: ${serverLabel(place.server)}`);
55
- (0, ui_1.out)(`Move: ${place.move?.note ?? ''}`);
56
- (0, ui_1.note)((0, ui_1.dim)(' Migrations and queries wait until it is over; the platform finishes it on its own'));
57
- }
58
22
  const STATE_LABEL = {
59
23
  pending: 'waiting',
60
24
  applied: 'applied',
@@ -86,11 +50,9 @@ async function dbStatus(args) {
86
50
  // is the applied history alone, which does not depend on any working copy.
87
51
  if ((0, args_1.flagString)(args, 'project') !== undefined) {
88
52
  const { projectId, client } = await (0, session_1.projectTarget)(args);
89
- const place = await projectDatabase(client, projectId);
90
- if (place && moving(place.move))
91
- return printMoving(place);
92
53
  const history = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/migrate`);
93
- printPlace(history, place);
54
+ (0, ui_1.out)(`Schema: ${(0, ui_1.bold)(history.schema)}`);
55
+ (0, ui_1.out)('');
94
56
  if (history.applied.length === 0) {
95
57
  (0, ui_1.note)('No migrations have been applied yet');
96
58
  return;
@@ -102,15 +64,13 @@ async function dbStatus(args) {
102
64
  const { root, config } = (0, config_1.requireProject)();
103
65
  const client = await (0, session_1.connectProject)(config.projectId, root, config);
104
66
  const local = readMigrations(root);
105
- const place = await projectDatabase(client, config.projectId);
106
- if (place && moving(place.move))
107
- return printMoving(place);
108
67
  const status = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/status`, {
109
68
  method: 'POST',
110
69
  body: { migrations: local },
111
70
  timeoutMs: 60_000,
112
71
  });
113
- printPlace({ schema: status.schema, server: status.server, applied: [] }, place);
72
+ (0, ui_1.out)(`Schema: ${(0, ui_1.bold)(status.schema)}`);
73
+ (0, ui_1.out)('');
114
74
  if (status.migrations.length === 0) {
115
75
  (0, ui_1.note)(`No migrations. The first one: ${MIGRATIONS_DIR}/0001_init.sql`);
116
76
  return;
@@ -121,65 +81,23 @@ async function dbStatus(args) {
121
81
  (0, ui_1.warn)('A changed file is never applied again and db migrate refuses it: write a new migration');
122
82
  }
123
83
  }
124
- function serverRow(server) {
125
- const room = server.used_bytes !== null && server.space_bytes !== null
126
- ? `${(0, ui_1.formatBytes)(server.used_bytes)} of ${(0, ui_1.formatBytes)(server.space_bytes)} used`
127
- : '';
128
- return [
129
- server.id === null ? 'platform' : `own "${server.name}"`,
130
- server.id === null ? 'plan space' : server.tier ?? '',
131
- server.status === 'active' ? '' : server.status,
132
- room,
133
- `${server.databases} ${server.databases === 1 ? 'database' : 'databases'}`,
134
- ];
135
- }
136
- /** The databases of the projects of the organization, and the servers they can live on. */
84
+ /** The databases of the projects of the organization, largest first. */
137
85
  async function dbList() {
138
86
  const client = (0, session_1.connect)((0, config_1.localConfig)());
139
87
  const data = await (0, api_1.apiJson)(client, '/api/v1/org/databases');
140
88
  if (data.databases.length === 0) {
141
89
  (0, ui_1.note)('The organization has no databases');
142
90
  (0, ui_1.note)((0, ui_1.dim)(' One is created with the first project: xflow init'));
91
+ return;
143
92
  }
144
- else {
145
- printDatabases(data.databases, data.total, data.truncated);
146
- }
147
- printServers(data.servers);
148
- }
149
- function moveLine(move) {
150
- return move.finished_at ? `${move.note} (${(0, ui_1.formatAge)(move.finished_at)})` : move.note;
151
- }
152
- function printDatabases(databases, total, truncated) {
153
- (0, ui_1.table)(databases.map((database) => [
93
+ (0, ui_1.table)(data.databases.map((database) => [
154
94
  database.name,
155
95
  database.id,
156
- database.schema ?? '-',
157
- serverLabel(database.server),
158
- typeof database.bytes === 'number' ? (0, ui_1.formatBytes)(database.bytes) : '',
96
+ database.schema_name,
97
+ database.bytes === null ? '' : (0, ui_1.formatBytes)(database.bytes),
159
98
  ]));
160
- if (truncated)
161
- (0, ui_1.note)((0, ui_1.dim)(` Shown ${databases.length} of ${total}`));
162
- for (const database of databases) {
163
- if (database.move)
164
- (0, ui_1.note)((0, ui_1.dim)(` ${database.name}: ${moveLine(database.move)}`));
165
- }
166
- const down = new Map();
167
- for (const { server } of databases)
168
- if (server && server.status !== 'active')
169
- down.set(server.id, server);
170
- for (const server of down.values()) {
171
- (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. ` +
172
- 'Not fixable from the code: tell the person, the server state is in the «Базы данных» (Databases) section of the web interface'));
173
- }
174
- }
175
- /** Where a new or a moved database can go, with the room on each. Only the platform: no choice to show. */
176
- function printServers(servers) {
177
- if (!servers || servers.length < 2)
178
- return;
179
- (0, ui_1.out)('');
180
- (0, ui_1.out)((0, ui_1.bold)('Servers'));
181
- (0, ui_1.table)(servers.map(serverRow));
182
- (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'));
99
+ if (data.truncated)
100
+ (0, ui_1.note)((0, ui_1.dim)(` Shown ${data.databases.length} of ${data.total}`));
183
101
  }
184
102
  /** Long values would tear the columns apart; the cut is marked, never silent. */
185
103
  const CELL_LIMIT = 60;
@@ -328,64 +246,6 @@ async function dbExec(args) {
328
246
  (0, ui_1.note)((0, ui_1.dim)(` The first ${data.rows.length} returned rows: the rest are saved but not sent back`));
329
247
  }
330
248
  }
331
- const MOVE_POLL_MS = 3_000;
332
- /** Past this the command lets go: the move carries on, db status follows it. */
333
- const MOVE_FOLLOW_MS = 10 * 60_000;
334
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
335
- /**
336
- * Moving the project database to another server of the organization. The platform
337
- * runs the move by itself; this command starts it and follows it to the end.
338
- */
339
- async function dbMove(args) {
340
- const to = (0, args_1.flagString)(args, 'to');
341
- if (!to) {
342
- throw new errors_1.CliError('Name the server: --to <name>', 'The servers and their names: xflow db list ("platform" is the common one)');
343
- }
344
- const { projectId, client } = await (0, session_1.projectTarget)(args);
345
- const path = `/api/v1/projects/${projectId}/db/move`;
346
- let place;
347
- // The platform takes the project name as the yes; --yes looks it up.
348
- const confirm = (0, args_1.flagBool)(args, 'yes')
349
- ? (await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`)).name
350
- : undefined;
351
- try {
352
- place = await (0, api_1.apiJson)(client, path, {
353
- method: 'POST',
354
- body: { to, confirm },
355
- timeoutMs: 60_000,
356
- });
357
- }
358
- catch (e) {
359
- // Said in the words of this command: the flag, and the name that goes in it.
360
- if (e instanceof api_1.ApiError && e.code === 'confirm_required') {
361
- throw new errors_1.CliError(e.message, `To move it: xflow db move --to "${to}" --yes`);
362
- }
363
- throw e;
364
- }
365
- (0, ui_1.note)((0, ui_1.dim)(' The platform carries the move through by itself: stopping this command does not stop it'));
366
- const deadline = Date.now() + MOVE_FOLLOW_MS;
367
- let shown = '';
368
- while (moving(place.move)) {
369
- const line = place.move?.note ?? '';
370
- if (line !== shown)
371
- (0, ui_1.step)(line);
372
- shown = line;
373
- if (Date.now() > deadline) {
374
- (0, ui_1.note)('Still going: xflow db status follows it');
375
- return;
376
- }
377
- await sleep(MOVE_POLL_MS);
378
- place = await (0, api_1.apiJson)(client, path);
379
- }
380
- const { move } = place;
381
- if (move?.status === 'failed')
382
- throw new errors_1.CliError(move.note);
383
- if (move && (move.functions_done ?? 0) < (move.functions_total ?? 0)) {
384
- (0, ui_1.warn)(move.note);
385
- return;
386
- }
387
- (0, ui_1.ok)(move?.note ?? `The database is on ${serverLabel(place.server)}`);
388
- }
389
249
  async function dbMigrate(args) {
390
250
  const { root, config } = (0, config_1.requireProject)();
391
251
  const client = await (0, session_1.connectProject)(config.projectId, root, config);
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.orgList = orgList;
4
4
  exports.orgMembers = orgMembers;
5
+ exports.findOrg = findOrg;
5
6
  exports.orgSwitch = orgSwitch;
6
7
  const api_1 = require("../api");
7
8
  const config_1 = require("../config");
@@ -47,6 +48,19 @@ async function orgMembers() {
47
48
  const count = data.truncated ? `${data.members.length} of ${data.total}` : String(data.total);
48
49
  (0, ui_1.note)((0, ui_1.dim)(` ${count} in "${org ? label(org) : 'the organization'}". Inviting and removing people happens in the platform interface`));
49
50
  }
51
+ /** An organization by id, or by name ignoring case; a name shared by several asks for the id. */
52
+ function findOrg(orgs, wanted) {
53
+ const byId = orgs.find((org) => org.organizationId === wanted);
54
+ if (byId)
55
+ return byId;
56
+ const byName = orgs.filter((org) => (org.name ?? '').toLowerCase() === wanted.toLowerCase());
57
+ if (byName.length > 1) {
58
+ throw new errors_1.CliError(`Several organizations are named "${wanted}"`, `Use the id instead: ${byName.map((org) => org.organizationId).join(', ')}`);
59
+ }
60
+ if (!byName[0])
61
+ throw new errors_1.CliError(`No stored key for "${wanted}"`, 'The list: xflow org. To add one: xflow login');
62
+ return byName[0];
63
+ }
50
64
  /** Local switch of the active organization: a pointer move, no browser. */
51
65
  function orgSwitch(args) {
52
66
  const wanted = args.words[0];
@@ -57,15 +71,7 @@ function orgSwitch(args) {
57
71
  if (orgs.length === 0) {
58
72
  throw new errors_1.CliError(`No keys for ${apiUrl}`, 'Sign in: xflow login');
59
73
  }
60
- const byId = orgs.find((org) => org.organizationId === wanted);
61
- const byName = orgs.filter((org) => (org.name ?? '').toLowerCase() === wanted.toLowerCase());
62
- if (!byId && byName.length > 1) {
63
- throw new errors_1.CliError(`Several organizations are named "${wanted}"`, `Use the id instead: ${byName.map((org) => org.organizationId).join(', ')}`);
64
- }
65
- const target = byId ?? byName[0];
66
- if (!target) {
67
- throw new errors_1.CliError(`No stored key for "${wanted}"`, 'The list: xflow org. To add one: xflow login');
68
- }
74
+ const target = findOrg(orgs, wanted);
69
75
  if (target.active) {
70
76
  (0, ui_1.ok)(`"${label(target)}" is already the active organization`);
71
77
  return;
@@ -13,6 +13,7 @@ const args_1 = require("../args");
13
13
  const config_1 = require("../config");
14
14
  const credentials_1 = require("../credentials");
15
15
  const errors_1 = require("../errors");
16
+ const org_1 = require("./org");
16
17
  const session_1 = require("../session");
17
18
  const skills_1 = require("./skills");
18
19
  const sources_1 = require("./sources");
@@ -61,15 +62,28 @@ async function init(args) {
61
62
  const target = (0, node_path_1.resolve)(args.words[0] ?? '.');
62
63
  const name = (0, args_1.flagString)(args, 'name') ?? (0, node_path_1.basename)(target);
63
64
  const noApp = (0, args_1.flagBool)(args, 'no-app');
64
- const client = (0, session_1.connect)();
65
+ let client = (0, session_1.connect)();
66
+ // A person in several organizations names the one that gets the project: the active one may be a client's.
67
+ const orgs = client.source === 'env' ? [] : (0, credentials_1.listOrgs)(client.apiUrl);
68
+ const wanted = (0, args_1.flagString)(args, 'org');
69
+ if (wanted) {
70
+ if (client.source === 'env')
71
+ throw new errors_1.CliError('--org does not apply to a key from XFLOW_TOKEN', 'That key belongs to one organization: drop --org');
72
+ const org = (0, org_1.findOrg)(orgs, wanted);
73
+ client = { apiUrl: client.apiUrl, token: org.token, source: 'folder', organizationId: org.organizationId };
74
+ }
75
+ else if (orgs.length > 1) {
76
+ throw new errors_1.CliError(`You are signed in to ${orgs.length} organizations: name the one that gets the project`, `xflow init ${args.words[0] ?? '.'} --org <name>. The list: xflow org`);
77
+ }
78
+ const orgName = orgs.find((org) => org.organizationId === client.organizationId)?.name;
65
79
  if (!isEmptyEnough(target)) {
66
80
  throw new errors_1.CliError(`The directory ${target} is not empty`, 'Create the project in an empty folder: xflow init my-project. To link an existing folder to a project: xflow link <id>');
67
81
  }
68
82
  const folder = noApp ? null : checkFolder((0, args_1.flagString)(args, 'app') ?? (0, sources_1.appFolderName)(name, new Set(), 'app'));
69
- (0, ui_1.step)('Creating the project on the platform');
83
+ (0, ui_1.step)(orgName ? `Creating the project in "${orgName}"` : 'Creating the project on the platform');
70
84
  const project = await (0, api_1.apiJson)(client, '/api/v1/projects', {
71
85
  method: 'POST',
72
- body: { name, app: folder ? { name } : null, server: (0, args_1.flagString)(args, 'server') ?? null },
86
+ body: { name, app: folder ? { name } : null },
73
87
  timeoutMs: 60_000,
74
88
  });
75
89
  try {
@@ -91,8 +105,6 @@ async function init(args) {
91
105
  }
92
106
  (0, skills_1.installSkillQuietly)(target);
93
107
  (0, ui_1.ok)(`Project "${project.name}" created`);
94
- if (project.server)
95
- (0, ui_1.note)((0, ui_1.dim)(` Its database goes to the own server "${project.server.name}"`));
96
108
  (0, ui_1.out)('');
97
109
  (0, ui_1.out)(` ${(0, ui_1.bold)('Next:')}`);
98
110
  (0, ui_1.out)(` cd ${(0, node_path_1.basename)(target)}`);
@@ -251,7 +263,7 @@ async function get(args) {
251
263
  if (card.description)
252
264
  (0, ui_1.out)(card.description);
253
265
  (0, ui_1.out)('');
254
- (0, ui_1.out)(`Database: ${card.database.schema}${card.database.server_id ? ' (own server)' : ''}`);
266
+ (0, ui_1.out)(`Database: ${card.database.schema}`);
255
267
  (0, ui_1.out)('');
256
268
  (0, ui_1.out)('Apps:');
257
269
  if (card.apps.length === 0)
package/dist/flags.js CHANGED
@@ -44,7 +44,7 @@ exports.KNOWN = {
44
44
  'org list': [],
45
45
  'org members': [],
46
46
  'org switch': [],
47
- init: ['name', 'app', 'no-app', 'server'],
47
+ init: ['name', 'app', 'no-app', 'org'],
48
48
  link: [],
49
49
  projects: [],
50
50
  'projects list': [],
@@ -81,7 +81,6 @@ exports.KNOWN = {
81
81
  'db query': ['json', 'project'],
82
82
  'db exec': ['allow-destructive', 'json', 'project'],
83
83
  'db migrate': ['dry-run', 'allow-destructive'],
84
- 'db move': ['to', 'yes', 'project'],
85
84
  storage: ['json', 'project'],
86
85
  'storage ls': ['json', 'project'],
87
86
  'storage list': ['json', 'project'],
@@ -118,7 +117,7 @@ exports.KNOWN = {
118
117
  * line here, or a refusal nobody prints any more stays behind as a lie.
119
118
  */
120
119
  exports.WORKING_COPY = {
121
- pull: 'it writes the sources next to it',
120
+ pull: 'it writes the sources next to it. A project without a folder comes down whole with xflow link <id> in an empty one',
122
121
  status: 'it compares the folder with the revision on the platform',
123
122
  dev: 'it runs the dev server of the app folder it stands in',
124
123
  'apps add': 'it lays the template into apps/',
package/dist/help.js CHANGED
@@ -91,14 +91,12 @@ ${(0, ui_1.bold)('Functions')}
91
91
 
92
92
  ${(0, ui_1.bold)('Database')}
93
93
  xflow db status which migrations are applied and which are waiting
94
- xflow db list databases of the projects and the servers of the organization
94
+ xflow db list databases of the projects of the organization, with sizes
95
95
  xflow db schema [table] tables of the project, or the columns of one
96
96
  xflow db query 'select ...' read data, in a read-only transaction
97
97
  xflow db exec 'update ...' a one-off data change, outside the migration history
98
98
  xflow db migrate [--dry-run] [--allow-destructive]
99
99
  apply the migrations from migrations/*.sql
100
- xflow db move --to <server> --yes
101
- move the project database to another server
102
100
 
103
101
  ${(0, ui_1.bold)('Pages')}
104
102
  xflow pages public sites of the organization at <name>.xflowpage.ru
@@ -173,7 +171,7 @@ no working copy of its own:
173
171
  xflow db query --project 3f2a… 'select id, title from tasks where done = false'
174
172
  xflow functions invoke kanban --project 3f2a… --data-file card.json
175
173
 
176
- Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db exec')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('db move')}, ${(0, ui_1.bold)('functions list')},
174
+ Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db exec')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('functions list')},
177
175
  ${(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')},
178
176
  ${(0, ui_1.bold)('connections')}, ${(0, ui_1.bold)('connections link')}, ${(0, ui_1.bold)('connections unlink')}, ${(0, ui_1.bold)('schedules')},
179
177
  ${(0, ui_1.bold)('schedules set')}, ${(0, ui_1.bold)('schedules rm')}, ${(0, ui_1.bold)('storage ls')}, ${(0, ui_1.bold)('storage push')},
@@ -336,8 +334,8 @@ is a hard gate on a changed contract, and until you run this command nothing els
336
334
  will work.`,
337
335
  db: `${(0, ui_1.bold)('xflow db')}: schema, data, migrations
338
336
 
339
- xflow db status what is applied, what is waiting, the server
340
- xflow db list databases of the projects, the servers of the organization
337
+ xflow db status what is applied, what is waiting
338
+ xflow db list databases of the projects of the organization, with sizes
341
339
  xflow db schema [table] tables of the project, or the columns of one
342
340
  xflow db query 'select ...' read data (rows: the query's own limit, 50 without
343
341
  one, 300 at most; --json prints the values whole)
@@ -345,26 +343,10 @@ will work.`,
345
343
  xflow db exec 'update ...' a one-off data change: one INSERT, UPDATE, DELETE or MERGE
346
344
  (or <file>.sql; --allow-destructive, --json)
347
345
  xflow db migrate store the project root, then apply migrations/*.sql
348
- xflow db move --to <server> --yes
349
- move the project database to another server
350
346
 
351
347
  Every project has exactly one database of its own, shared by all of its apps and functions,
352
348
  and every command here works from any folder of the project. ${(0, ui_1.bold)('list')} is about the
353
- organization: one database per project, and where each lives: ${(0, ui_1.bold)('platform')} is the common
354
- server, ${(0, ui_1.bold)('own "<name>"')} an own server of the organization. While an own server is not
355
- running, its databases do not answer, and that is not fixable from the code. An organization
356
- with own servers also gets them listed, with the room used on each: that is the choice for
357
- ${(0, ui_1.bold)('xflow init --server')} and ${(0, ui_1.bold)('db move')}.
358
-
359
- ${(0, ui_1.bold)('move')} takes the database of this project to another server of the organization:
360
- ${(0, ui_1.bold)('platform')} or an own server by name (or id, when two share a name). The platform copies
361
- the tables, switches the functions of the project and removes the old copy; nothing needs a
362
- deploy. While it copies, the apps read data but cannot save anything new, usually for a
363
- couple of minutes, which is why it asks for ${(0, ui_1.bold)('--yes')}. It also needs the
364
- right to move databases on the key: off by default, only its owner turns it on, in the
365
- platform settings where the key permissions live. The command follows the move to the end;
366
- stopping it does not stop the move, and ${(0, ui_1.bold)('xflow db status')} shows where it stands. A move
367
- that fails is rolled back. Buying, paying for and deleting servers stay with the person.
349
+ organization: one database per project, largest first, to see which project takes the room.
368
350
 
369
351
  ${(0, ui_1.bold)('schema')} answers a different question than the ${(0, ui_1.bold)('migrations/')} directory: the files say
370
352
  what you did, the schema says what is actually in there. ${(0, ui_1.bold)('query')} runs inside a READ ONLY transaction, so
@@ -776,8 +758,8 @@ change them, restyle them or delete what you do not use.
776
758
  --name <name> project name (the folder name by default)
777
759
  --app <folder> the folder of the first app (made from the name by default)
778
760
  --no-app a project without apps: its functions are workflows
779
- --server <name> the database of the project, right away on that server:
780
- platform, or an own server of the organization (xflow db list)
761
+ --org <name> the organization that gets the project; required when you are
762
+ signed in to several (xflow org)
781
763
 
782
764
  More apps later: ${(0, ui_1.bold)('xflow apps add <folder>')}. If writing the files fails, the project stays
783
765
  on the platform and the CLI says how to pick it up with the link command.`,
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 = '1.3.0';
5
+ exports.CLI_VERSION = '1.5.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
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
7
- "node": ">=20"
7
+ "node": ">=22"
8
8
  },
9
9
  "bin": {
10
10
  "xflow": "dist/bin.js"
@@ -12,7 +12,8 @@ address and runs its backend: one database and cloud functions per project.
12
12
  No `xflow.json` in the folder or above it means it is not bound to a project yet:
13
13
  `xflow projects list` names the projects of the organization with their ids, `xflow link <id>`
14
14
  binds the folder to one (an empty folder gets the whole project), and `xflow init` starts a new
15
- project with its first app.
15
+ project with its first app. A person signed in to several organizations names the one that gets it:
16
+ `xflow init <dir> --org <name>` (ask them which one if unsure; `xflow org` lists them).
16
17
 
17
18
  ## First rule
18
19
 
@@ -27,7 +28,7 @@ instead of guessing; give the person https://docs.getxflow.com when they ask how
27
28
 
28
29
  ## Keeping these instructions current
29
30
 
30
- These instructions ship with xflow CLI 1.3.0, so the copy you are reading can be older than
31
+ These instructions ship with xflow CLI 1.5.0, so the copy you are reading can be older than
31
32
  the CLI answering your commands.
32
33
 
33
34
  When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
@@ -61,8 +62,8 @@ the error points away from the cause. The sections below carry the details.
61
62
  6. An already applied migration is never re-run, and `xflow db migrate` refuses a
62
63
  name it already holds with different content. A schema change is always a new file.
63
64
  7. When `xflow db migrate` answers `db_write_locked`, or an INSERT or CREATE TABLE in a
64
- function fails with `permission denied` (42501), the database is over its plan size and
65
- Postgres itself refuses. Reads, updates and deletes keep working, so the fix is deleting data
65
+ function fails with `database size limit ... reached` (53100), the database is over its plan
66
+ size and Postgres itself refuses. Reads, updates and deletes keep working, so the fix is deleting data
66
67
  (`xflow db exec` or a migration), never a rewrite of the SQL. Write comes back within the hour. The limit
67
68
  is shared by the organization: `xflow db list` shows the size of each project database.
68
69
  8. Heavy media does not travel with the sources. Photos, video and PDFs go up with
@@ -566,30 +567,20 @@ needs deleting and why, and let the person decide. With both in place the affect
566
567
  dumped first (up to 50 000 rows each) and kept for 7 days; getting a dump back is a request to
567
568
  the person, no command does it. Check with `--dry-run`.
568
569
 
569
- An organization can have own database servers. `xflow db list` names the server of each project
570
- database (`platform` is the common one) and the own servers with the room used.
571
- - A new project with its database on a chosen server: `xflow init --server <name>`.
572
- - An existing one: `xflow db move --to <name> --yes`. Writes close while it
573
- copies, usually a couple of minutes; the platform switches the functions itself. It needs the
574
- right to move databases, off on the key by default: refused for the right, say what should move
575
- where and why, and the person turns it on or moves it in the «Базы данных» (Databases) section.
576
- - Buying, paying for and deleting servers are the person's, in the web interface.
577
-
578
- Code stays the same on an own server: pass `DATABASE_URL` as is, without an `ssl` option. While
579
- an own server is not running, migrations and queries say so and functions that use the database
580
- time out: tell the person instead of retrying.
581
-
582
- An own server comes tuned and guarded, so do not tune it from migrations:
583
- - Installed: `pgcrypto`, `pg_trgm`, `unaccent`, `citext`, `btree_gin`, `btree_gist`,
584
- `pg_stat_statements`. `gen_random_uuid()` is built in everywhere, `uuidv7()` on an own server.
585
- Never `uuid-ossp`. Anything else (`postgis`, `pgvector`) is a request to the person.
586
- - A statement is cut after 30 seconds, waiting for a lock after 3, a transaction left open after
587
- 30. A query that needs longer is a wrong query (a missing index, or a report that belongs in a
588
- scheduled function writing a summary table). A scheduled job that genuinely needs more runs
589
- `SET statement_timeout = '5min'` on its own connection. A database that used `citext` or
590
- another extension type cannot move back to `platform`.
591
- - A restart on a plan change takes about a minute. Create the `Client` inside the handler, or
592
- keep a `Pool` at module level: a `Client` created at module level dies with the restart.
570
+ Pass `DATABASE_URL` as is, without an `ssl` option. The database is PostgreSQL 18, so do not
571
+ tune it from migrations:
572
+ - Installed: `pg_trgm`. `gen_random_uuid()` and `uuidv7()` are built in. Never `uuid-ossp`.
573
+ Anything else (`postgis`, `pgvector`) is a request to the person.
574
+ - A statement is cut after 30 seconds, waiting for a lock after 10, a transaction left open after
575
+ 60. A query that needs longer is a wrong query (a missing index, or a report that belongs in a
576
+ scheduled function writing a summary table).
577
+
578
+ Calls of one project run at the same time in one process. A `Client`, even one made at module
579
+ level, gets its own pooled connection for each call, so `connect()` twice is fine. Keep
580
+ per-request data out of module-level variables. Session state (`SET` without `LOCAL`, temp tables,
581
+ advisory locks) lasts until the call ends, and a transaction left open is rolled back: set what
582
+ the call needs inside the handler. A `Client` that listens for `notification` keeps its own
583
+ connection.
593
584
 
594
585
  ## File storage
595
586