@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 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` / `templates` | new project, link a folder to an existing one, which templates there are |
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.apiBinary = apiBinary;
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
- async function apiBinary(client, path) {
145
- const response = await send(client, path, { timeoutMs: TRANSFER_TIMEOUT_MS });
146
- if (!response.ok)
147
- throw await readError(response, client);
148
- return Buffer.from(await response.arrayBuffer());
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
@@ -8,7 +8,7 @@ exports.flagBool = flagBool;
8
8
  exports.flagNumber = flagNumber;
9
9
  /** Flags that never take a value, so `--force ./dir` keeps the path as a word. */
10
10
  exports.BOOLEAN_FLAGS = new Set([
11
- 'function',
11
+ 'no-app',
12
12
  'force',
13
13
  'yes',
14
14
  'json',
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, any build renews it`;
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, a rebuild will not help';
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];
@@ -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
- function checksum(sql) {
57
- return (0, node_crypto_1.createHash)('sha256').update(sql).digest('hex').slice(0, 16);
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 history = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`);
104
- const applied = new Map(history.applied.map((row) => [row.name, row]));
105
- printPlace(history, place);
106
- if (local.length === 0 && applied.size === 0) {
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
- const rows = [];
111
- for (const migration of local) {
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
- function attachedProjects(database) {
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 "Data" section of the web interface'));
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 goes to the platform server. To put it on another: xflow init --server <name>, or xflow db move --to <name> for one that already exists'));
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 ?? '<database>';
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',