@getxflow/cli 1.5.0 → 1.6.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/bin.js +1 -28
- package/dist/commands/deploy.js +1 -2
- package/dist/commands/env.js +4 -9
- package/dist/commands/storage.js +1 -2
- package/dist/flags.js +1 -10
- package/dist/help.js +2 -64
- package/dist/limits.js +3 -5
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +23 -61
- package/dist/commands/pages.js +0 -257
package/dist/bin.js
CHANGED
|
@@ -25,7 +25,6 @@ const schedules_1 = require("./commands/schedules");
|
|
|
25
25
|
const skills_1 = require("./commands/skills");
|
|
26
26
|
const sources_1 = require("./commands/sources");
|
|
27
27
|
const storage_1 = require("./commands/storage");
|
|
28
|
-
const pages_1 = require("./commands/pages");
|
|
29
28
|
const deploy_1 = require("./commands/deploy");
|
|
30
29
|
const dev_1 = require("./commands/dev");
|
|
31
30
|
const update_1 = require("./commands/update");
|
|
@@ -199,32 +198,6 @@ async function run(args) {
|
|
|
199
198
|
return;
|
|
200
199
|
}
|
|
201
200
|
throw new errors_1.CliError(`Unknown command: storage ${second}`, 'Available: ls, push and rm');
|
|
202
|
-
case 'pages':
|
|
203
|
-
if (second === 'create') {
|
|
204
|
-
await (0, pages_1.pagesCreate)(rest);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (second === 'deploy') {
|
|
208
|
-
await (0, pages_1.pagesDeploy)(rest);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (second === 'status') {
|
|
212
|
-
await (0, pages_1.pagesStatus)(rest);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (second === 'rollback') {
|
|
216
|
-
await (0, pages_1.pagesRollback)(rest);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (second === 'rm' || second === 'remove') {
|
|
220
|
-
await (0, pages_1.pagesRemove)(rest);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (second === undefined || second === 'list') {
|
|
224
|
-
await (0, pages_1.pagesList)(rest);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
throw new errors_1.CliError(`Unknown command: pages ${second}`, 'Available: list, create, deploy, status, rollback and rm');
|
|
228
201
|
case 'status':
|
|
229
202
|
await (0, sources_1.status)();
|
|
230
203
|
return;
|
|
@@ -334,7 +307,7 @@ async function main() {
|
|
|
334
307
|
// XFLOW_TOKEN is taken as given and no organization is looked for, so a
|
|
335
308
|
// project of another one answers "not found" with nothing else to say.
|
|
336
309
|
if (process.env.XFLOW_TOKEN?.trim()) {
|
|
337
|
-
(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
|
|
310
|
+
(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 of another one is not found'));
|
|
338
311
|
}
|
|
339
312
|
else if (e.apiUrl && (0, credentials_1.listOrgs)(e.apiUrl).length > 1) {
|
|
340
313
|
(0, ui_1.fatalNote)((0, ui_1.dim)(' Several organizations are signed in: the answer may live in another one, see xflow org'));
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.versionComment = versionComment;
|
|
4
3
|
exports.statedRevision = statedRevision;
|
|
5
4
|
exports.deploy = deploy;
|
|
6
5
|
exports.publish = publish;
|
|
@@ -70,7 +69,7 @@ async function startBuild(client, base, revision, allowRemovals, comment) {
|
|
|
70
69
|
const MAX_COMMENT_LENGTH = 200;
|
|
71
70
|
/** Control and bidi-override characters: they spoof terminals, not comments. */
|
|
72
71
|
const COMMENT_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
|
|
73
|
-
/** The version comment as typed after -m, cleaned; empty when there is none.
|
|
72
|
+
/** The version comment as typed after -m, cleaned; empty when there is none. */
|
|
74
73
|
function versionComment(raw) {
|
|
75
74
|
const comment = (raw ?? '').replace(/\s+/g, ' ').replace(COMMENT_CONTROL_CHARS, '').trim();
|
|
76
75
|
if (comment.length > MAX_COMMENT_LENGTH) {
|
package/dist/commands/env.js
CHANGED
|
@@ -8,14 +8,12 @@ exports.envRemove = envRemove;
|
|
|
8
8
|
const node_fs_1 = require("node:fs");
|
|
9
9
|
const node_path_1 = require("node:path");
|
|
10
10
|
const api_1 = require("../api");
|
|
11
|
-
const args_1 = require("../args");
|
|
12
11
|
const config_1 = require("../config");
|
|
13
12
|
const errors_1 = require("../errors");
|
|
14
13
|
const session_1 = require("../session");
|
|
15
14
|
const ui_1 = require("../ui");
|
|
16
15
|
const SCOPE_LABEL = {
|
|
17
|
-
|
|
18
|
-
project: 'this project only',
|
|
16
|
+
project: 'this project',
|
|
19
17
|
connection: 'from a connected service, read-only',
|
|
20
18
|
};
|
|
21
19
|
const FUNCTIONS_DIR = 'functions';
|
|
@@ -101,7 +99,7 @@ async function envList(args) {
|
|
|
101
99
|
(0, ui_1.note)((0, ui_1.dim)(' To store one: xflow env set SMTP_PASSWORD=secret'));
|
|
102
100
|
return;
|
|
103
101
|
}
|
|
104
|
-
(0, ui_1.table)(rows.map((row) => [row.name, SCOPE_LABEL[row.scope]
|
|
102
|
+
(0, ui_1.table)(rows.map((row) => [row.name, SCOPE_LABEL[row.scope]]));
|
|
105
103
|
(0, ui_1.note)((0, ui_1.dim)(' The platform never returns values: they are visible only inside the function'));
|
|
106
104
|
}
|
|
107
105
|
/** Check that every referenced variable is stored. */
|
|
@@ -172,12 +170,9 @@ async function envSet(args) {
|
|
|
172
170
|
const value = pair.slice(pair.indexOf('=') + 1);
|
|
173
171
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env`, {
|
|
174
172
|
method: 'POST',
|
|
175
|
-
|
|
176
|
-
// refuses a third. Guessing here would have made --scope proj mean the
|
|
177
|
-
// whole organization, which is the opposite of what such a typo means.
|
|
178
|
-
body: { name, value, scope: (0, args_1.flagString)(args, 'scope') },
|
|
173
|
+
body: { name, value },
|
|
179
174
|
});
|
|
180
|
-
(0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} stored
|
|
175
|
+
(0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} stored`);
|
|
181
176
|
// Values reach a function on its next deploy.
|
|
182
177
|
const users = referencedByFunctions(root).needed.get(name);
|
|
183
178
|
if (users && users.length > 0) {
|
package/dist/commands/storage.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.contentTypeFor = contentTypeFor;
|
|
4
|
-
exports.collectMedia = collectMedia;
|
|
5
4
|
exports.planBatches = planBatches;
|
|
6
5
|
exports.caseCollisions = caseCollisions;
|
|
7
6
|
exports.storageList = storageList;
|
|
@@ -75,7 +74,7 @@ function contentTypeFor(name) {
|
|
|
75
74
|
* what one is asked to put there so it stays out of the 10 MB archive.
|
|
76
75
|
*
|
|
77
76
|
* Dot entries and symlinks are skipped: pointing push at a working copy should
|
|
78
|
-
* not upload .git, and a link is not ours to follow.
|
|
77
|
+
* not upload .git, and a link is not ours to follow.
|
|
79
78
|
*/
|
|
80
79
|
function collectMedia(root, prefix = '') {
|
|
81
80
|
const found = [];
|
package/dist/flags.js
CHANGED
|
@@ -60,7 +60,7 @@ exports.KNOWN = {
|
|
|
60
60
|
logs: ['limit', 'project', 'app'],
|
|
61
61
|
env: ['project'],
|
|
62
62
|
'env list': ['project'],
|
|
63
|
-
'env set': [
|
|
63
|
+
'env set': [],
|
|
64
64
|
'env rm': ['project'],
|
|
65
65
|
'env remove': ['project'],
|
|
66
66
|
'env check': [],
|
|
@@ -87,14 +87,6 @@ exports.KNOWN = {
|
|
|
87
87
|
'storage push': ['to', 'replace', 'json', 'project'],
|
|
88
88
|
'storage rm': ['folder', 'yes', 'project'],
|
|
89
89
|
'storage remove': ['folder', 'yes', 'project'],
|
|
90
|
-
pages: ['json'],
|
|
91
|
-
'pages list': ['json'],
|
|
92
|
-
'pages create': ['slug'],
|
|
93
|
-
'pages deploy': ['message'],
|
|
94
|
-
'pages status': ['json'],
|
|
95
|
-
'pages rollback': [],
|
|
96
|
-
'pages rm': ['yes'],
|
|
97
|
-
'pages remove': ['yes'],
|
|
98
90
|
status: [],
|
|
99
91
|
dev: [],
|
|
100
92
|
pull: ['into', 'force', 'revision'],
|
|
@@ -144,7 +136,6 @@ const NAMESPACES = new Set([
|
|
|
144
136
|
'schedules',
|
|
145
137
|
'db',
|
|
146
138
|
'storage',
|
|
147
|
-
'pages',
|
|
148
139
|
]);
|
|
149
140
|
/** The command these words name, as far as the table knows it. */
|
|
150
141
|
function commandOf(words) {
|
package/dist/help.js
CHANGED
|
@@ -98,16 +98,6 @@ ${(0, ui_1.bold)('Database')}
|
|
|
98
98
|
xflow db migrate [--dry-run] [--allow-destructive]
|
|
99
99
|
apply the migrations from migrations/*.sql
|
|
100
100
|
|
|
101
|
-
${(0, ui_1.bold)('Pages')}
|
|
102
|
-
xflow pages public sites of the organization at <name>.xflowpage.ru
|
|
103
|
-
xflow pages create <title> <folder|.zip|.html> [--slug name]
|
|
104
|
-
a new page with the site on it, live at once
|
|
105
|
-
xflow pages deploy <page> <folder|.zip|.html> [-m "what changed"]
|
|
106
|
-
upload the site: live at once, there is no publish step
|
|
107
|
-
xflow pages status <page> the page and its versions
|
|
108
|
-
xflow pages rollback <page> <N> put an earlier version back on the address
|
|
109
|
-
xflow pages rm <page> --yes delete the page for good: the address empties
|
|
110
|
-
|
|
111
101
|
${(0, ui_1.bold)('Reference')}
|
|
112
102
|
xflow logs [--limit N] browser errors of the published apps
|
|
113
103
|
xflow projects list projects of the organization
|
|
@@ -262,53 +252,6 @@ how they differ, ${(0, ui_1.bold)('xflow pull --into ./server-copy')} fetches th
|
|
|
262
252
|
|
|
263
253
|
In a folder that is already linked, the command points xflow.json at the other project and
|
|
264
254
|
keeps the rest of the file. A new project instead of an existing one: ${(0, ui_1.bold)('xflow init')}.`,
|
|
265
|
-
pages: `${(0, ui_1.bold)('xflow pages')}: public sites of the organization
|
|
266
|
-
|
|
267
|
-
A page is a static site at https://<name>.xflowpage.ru: a landing page, a presentation, an
|
|
268
|
-
invoice for people outside the organization. It opens by its address for anyone, without
|
|
269
|
-
signing in, and has no build, no publish step and no database. An app is the opposite: code
|
|
270
|
-
the platform builds, with versions, access levels and a database through functions.
|
|
271
|
-
|
|
272
|
-
xflow pages the pages, their state and address
|
|
273
|
-
xflow pages create <title> <path> a new page with its first version: there are no
|
|
274
|
-
empty pages
|
|
275
|
-
--slug <name> the name in the address; without it the name is made
|
|
276
|
-
from the title, with a random tail
|
|
277
|
-
xflow pages deploy <page> <path> upload a new version
|
|
278
|
-
-m <text> what changed, one short line: it names the version
|
|
279
|
-
xflow pages status <page> the page and its versions
|
|
280
|
-
xflow pages rollback <page> <N> put version N back on the address
|
|
281
|
-
xflow pages rm <page> delete the page
|
|
282
|
-
--yes without it the command names what goes and stops
|
|
283
|
-
|
|
284
|
-
${(0, ui_1.bold)('<path>')} is a folder with index.html at the top, its .zip, or one .html file.
|
|
285
|
-
${(0, ui_1.bold)('<page>')} is the name from the address or the id. ${(0, ui_1.bold)('--json')} on list and status
|
|
286
|
-
prints the answer as JSON. A page belongs to the organization, not to a project, so there is
|
|
287
|
-
no ${(0, ui_1.bold)('--project')}: the key is picked as for every other command (${(0, ui_1.bold)('xflow help org')}).
|
|
288
|
-
|
|
289
|
-
${(0, ui_1.bold)('An upload is live at once.')} There is no draft and no publish step: anyone with the
|
|
290
|
-
address sees the new version within seconds. Uploading the same files again changes nothing.
|
|
291
|
-
The files of the last 5 versions are kept, so a rollback takes seconds and needs no upload.
|
|
292
|
-
|
|
293
|
-
What goes up is the built site: HTML, CSS, JS, images, fonts, PDF, audio and video. No code
|
|
294
|
-
runs on the platform side, and a folder with package.json is refused as the sources of a site:
|
|
295
|
-
build it and upload what the build writes. Dot entries are skipped. At most 5 MB zipped, 25 MB
|
|
296
|
-
unpacked and 1000 files.
|
|
297
|
-
|
|
298
|
-
The page is public, so a file holding a secret key is refused whole: every visitor would read
|
|
299
|
-
it. A page has no backend: a form or a lookup belongs in an app with access "anyone". The one
|
|
300
|
-
key a page may hold is a function key, and it opens that function to every visitor: an HTML
|
|
301
|
-
form can post to its webhook address with ${(0, ui_1.bold)('?key=')}, while fetch cannot read the answer
|
|
302
|
-
(no CORS). Search engines are asked not to index pages, so do not promise SEO.
|
|
303
|
-
|
|
304
|
-
${(0, ui_1.bold)('Deleting is final.')} The address goes empty and every version of the page is gone: there
|
|
305
|
-
is no undo and no copy on the platform side. The name stays reserved for the organization, so
|
|
306
|
-
the same ${(0, ui_1.bold)('--slug')} is free for the next page of yours and taken for everyone else. A
|
|
307
|
-
throwaway page put up to try something is worth deleting the same way it was made.
|
|
308
|
-
|
|
309
|
-
A page takes the key of the whole organization, the one ${(0, ui_1.bold)('xflow login')} issues: a key
|
|
310
|
-
scoped to projects is refused. A page is deleted by whoever made it, or by the owner of the
|
|
311
|
-
organization. Switching a page off for a while is for a person, in the platform interface.`,
|
|
312
255
|
update: `${(0, ui_1.bold)('xflow update')}: bring the CLI up to date
|
|
313
256
|
|
|
314
257
|
Installs the published version and, if anything changed, rewrites the platform
|
|
@@ -463,11 +406,7 @@ the function. So keep your own copy wherever you got it from.
|
|
|
463
406
|
xflow env set SMTP_PASSWORD=… store
|
|
464
407
|
xflow env rm SMTP_PASSWORD delete
|
|
465
408
|
|
|
466
|
-
|
|
467
|
-
--scope organization visible to every project of the organization (the default)
|
|
468
|
-
|
|
469
|
-
Those two words are the whole list: a third is refused rather than read as the default,
|
|
470
|
-
because a value meant for one project would have gone out to all of them.
|
|
409
|
+
A variable belongs to this project: functions of other projects never see it.
|
|
471
410
|
|
|
472
411
|
${(0, ui_1.bold)('check')} reads the sources in ${(0, ui_1.bold)('functions/')} and looks for ${(0, ui_1.bold)('process.env.NAME')}
|
|
473
412
|
references. The same rule applies on deploy: a function receives only the variables it
|
|
@@ -609,8 +548,7 @@ Who may call a function is its callers, in the root xflow.json:
|
|
|
609
548
|
|
|
610
549
|
{ "projectId": "…", "functions": { "book-slot": { "callers": ["zapis"] } } }
|
|
611
550
|
|
|
612
|
-
A caller is an app folder of this project (apps/zapis is "zapis")
|
|
613
|
-
nothing. A function nobody lists has no callers: a workflow, woken by a schedule, by a service
|
|
551
|
+
A caller is an app folder of this project (apps/zapis is "zapis"). A function nobody lists has no callers: a workflow, woken by a schedule, by a service
|
|
614
552
|
holding a key a person issued, or by ${(0, ui_1.bold)('invoke')}. ${(0, ui_1.bold)('open')} in the list means a caller is an app
|
|
615
553
|
anyone with the link opens, so anyone on the internet can call it. The address in the note
|
|
616
554
|
under the table is for webhooks and other functions, with a key; apps call ${(0, ui_1.bold)('/_fn/<name>')} on
|
package/dist/limits.js
CHANGED
|
@@ -4,12 +4,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.limitLine = limitLine;
|
|
5
5
|
exports.quotaRows = quotaRows;
|
|
6
6
|
const DENIAL_LABELS = {
|
|
7
|
-
limit_projects: 'projects, the platform cap',
|
|
7
|
+
limit_projects: 'new projects today, the platform cap (resets at 00:00 UTC)',
|
|
8
8
|
limit_apps: 'apps',
|
|
9
9
|
plan_app_access: 'access levels of apps',
|
|
10
10
|
functions_stopped: 'function minutes, the functions of the organization are stopped',
|
|
11
11
|
limit_connections: 'active connector connections',
|
|
12
|
-
limit_pages: 'public pages',
|
|
13
12
|
limit_functions: 'cloud functions',
|
|
14
13
|
limit_builds: 'builds this month',
|
|
15
14
|
limit_builds_concurrent: 'builds running at once',
|
|
@@ -32,12 +31,12 @@ function limitLine(detail) {
|
|
|
32
31
|
: detail.code === 'limit_schedule_interval'
|
|
33
32
|
? `, every ${detail.used} min against a ${detail.limit} min minimum`
|
|
34
33
|
: `, ${detail.used} of ${detail.limit} used`;
|
|
35
|
-
|
|
34
|
+
const tail = detail.code === 'limit_projects' ? 'Repeating it before then will not help.' : 'Repeating the same command will not help.';
|
|
35
|
+
return `Plan limit: ${label}${numbers}. ${tail}`;
|
|
36
36
|
}
|
|
37
37
|
const QUOTA_ORDER = [
|
|
38
38
|
'apps',
|
|
39
39
|
'connections',
|
|
40
|
-
'pages',
|
|
41
40
|
'functions',
|
|
42
41
|
'schedules',
|
|
43
42
|
'people',
|
|
@@ -49,7 +48,6 @@ const QUOTA_ORDER = [
|
|
|
49
48
|
const QUOTA_LABELS = {
|
|
50
49
|
apps: 'Apps',
|
|
51
50
|
connections: 'Active connections',
|
|
52
|
-
pages: 'Public pages',
|
|
53
51
|
functions: 'Cloud functions',
|
|
54
52
|
schedules: 'Schedules',
|
|
55
53
|
people: 'People',
|
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.
|
|
5
|
+
exports.CLI_VERSION = '1.6.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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xflow
|
|
3
|
-
description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use whenever a folder has xflow.json (the project root or an app folder in apps/), and for any task that touches a project and its apps, deployment, publishing, rollback, source sync, who may open an app or call a function, sign-in inside a public app, the project database or SQL migrations, cloud functions, schedules, environment variables and secrets, file storage and uploads of photos, video or PDFs, connected accounts of external services, production errors and logs, UI built on the template components, or a public
|
|
3
|
+
description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use whenever a folder has xflow.json (the project root or an app folder in apps/), and for any task that touches a project and its apps, deployment, publishing, rollback, source sync, who may open an app or call a function, sign-in inside a public app, the project database or SQL migrations, cloud functions, schedules, environment variables and secrets, file storage and uploads of photos, video or PDFs, connected accounts of external services, production errors and logs, UI built on the template components, or a public site for people outside the organization (a landing page, presentation or invoice).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# XFlow
|
|
@@ -28,7 +28,7 @@ instead of guessing; give the person https://docs.getxflow.com when they ask how
|
|
|
28
28
|
|
|
29
29
|
## Keeping these instructions current
|
|
30
30
|
|
|
31
|
-
These instructions ship with xflow CLI 1.
|
|
31
|
+
These instructions ship with xflow CLI 1.6.0, so the copy you are reading can be older than
|
|
32
32
|
the CLI answering your commands.
|
|
33
33
|
|
|
34
34
|
When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
|
|
@@ -113,9 +113,10 @@ One folder per project, apps in subfolders:
|
|
|
113
113
|
|
|
114
114
|
## Plan limits
|
|
115
115
|
|
|
116
|
-
The organization runs on a plan with limits: apps,
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
The organization runs on a plan with limits: apps, active connections, people in the
|
|
117
|
+
organization, database and file storage, function minutes, builds per month and at once, how often
|
|
118
|
+
schedules may run, and which access levels an app may have. The number of functions, schedules
|
|
119
|
+
and projects is not capped; at most 15 new projects a day (UTC) per organization, deleted ones count. Most are finite; a plan
|
|
119
120
|
may leave one uncapped, and then `whoami` prints `no limit` instead of a ceiling.
|
|
120
121
|
`xflow whoami` prints every one of them next to what is used, and the rights of your own key
|
|
121
122
|
with them: destroying data (a migration or `db exec`), deleting files from storage, moving a database and
|
|
@@ -124,18 +125,19 @@ halfway through the work into a sentence said before it starts.
|
|
|
124
125
|
|
|
125
126
|
- Connections are counted while active: switching an unused one off frees the seat without
|
|
126
127
|
deleting it.
|
|
127
|
-
- Function minutes are shared by the whole organization: every app
|
|
128
|
+
- Function minutes are shared by the whole organization: every app and schedule spends
|
|
128
129
|
them. When they run out, every function of the organization stops (`functions_stopped`).
|
|
129
130
|
- An unpaid plan stops the functions at once, and five days later freezes the organization:
|
|
130
131
|
every command except `xflow whoami` answers `subscription_blocked`,
|
|
131
|
-
apps
|
|
132
|
+
apps show a "suspended" page. Nothing is deleted and no code fixes it: tell the
|
|
132
133
|
person that the owner pays the invoice in the organization settings, section «Тариф» (Plan).
|
|
133
134
|
- The cap on how long one function call may run is written into the function when it is
|
|
134
135
|
deployed, so a plan change reaches the cloud only with the next `xflow deploy functions` of
|
|
135
136
|
each project, in both directions.
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
- The functions of one project take a plan-set rate: Free 5 calls a second (a burst of 60),
|
|
138
|
+
Start 20 (300), Business 50 (750). A call over the rate gets 429 at once. Calls at the same time:
|
|
139
|
+
Free 4, Start 10, Business 25, the rest wait in a queue. Fan out in batches of that size, never
|
|
140
|
+
all items of a list in parallel.
|
|
139
141
|
|
|
140
142
|
A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`, the
|
|
141
143
|
API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`). Retrying, renaming or rewriting the code changes nothing: tell the user
|
|
@@ -274,9 +276,8 @@ The root `xflow.json` names the callers of each function:
|
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
|
|
277
|
-
- A caller is an app folder of this project (`apps/zapis` is `"zapis"`)
|
|
278
|
-
|
|
279
|
-
the same thing.
|
|
279
|
+
- A caller is an app folder of this project (`apps/zapis` is `"zapis"`). A function without
|
|
280
|
+
callers is left out of `functions` or given `"callers": []`, the same thing.
|
|
280
281
|
- A function nobody lists has no callers: no app reaches it. That is a workflow,
|
|
281
282
|
run by a schedule, a key or `xflow functions invoke`.
|
|
282
283
|
- Callers ship with `xflow deploy functions` and come back with its `--revision N`.
|
|
@@ -345,19 +346,11 @@ At the closed levels do none of this: the platform already knows the person.
|
|
|
345
346
|
- `xflow dev` does not carry `X-Session` and calls functions as you (a member).
|
|
346
347
|
Try the sign-in on the app address.
|
|
347
348
|
|
|
348
|
-
##
|
|
349
|
+
## A site for people outside the organization
|
|
349
350
|
|
|
350
|
-
- A page is
|
|
351
|
-
|
|
352
|
-
-
|
|
353
|
-
functions. Anything people sign in to, enter data into or come back to.
|
|
354
|
-
- Keep the sources of a page outside the project folder, or list them in `.xflowignore`: the
|
|
355
|
-
root travels with `deploy functions`.
|
|
356
|
-
- A page has no backend. A form or a lookup for strangers is an app at "Anyone with the link"
|
|
357
|
-
(a paid plan): the platform checks every call to its functions.
|
|
358
|
-
- A function key on a page is allowed, and it opens that function to every visitor: an HTML
|
|
359
|
-
form posts to `https://fn.xflowpage.ru/<projectId>/<name>?key=...`, fetch cannot read the
|
|
360
|
-
answer (no CORS). Nothing guards it but the function's own checks: say so to the user.
|
|
351
|
+
- A landing page, a presentation, an invoice is an app at "Anyone with the link" (a paid plan):
|
|
352
|
+
built, published and rolled back like any app, opened by its address without signing in.
|
|
353
|
+
- Search engines are asked not to index apps: do not promise SEO.
|
|
361
354
|
|
|
362
355
|
## Cloud functions
|
|
363
356
|
|
|
@@ -457,11 +450,8 @@ writes one, `xflow env` lists the names, `xflow env check` names the variables y
|
|
|
457
450
|
read but the platform does not have. Values never come back out: the only place they exist is
|
|
458
451
|
inside the running function. Apps get no variables at all.
|
|
459
452
|
|
|
460
|
-
A variable
|
|
461
|
-
|
|
462
|
-
Pick the narrow one for anything that belongs to one project. These commands see only what
|
|
463
|
-
this project can see, so `env rm` reports a variable of another project missing even though
|
|
464
|
-
names are unique within the organization.
|
|
453
|
+
A variable belongs to one project: functions of other projects never see it, and each project
|
|
454
|
+
keeps its own value under the same name.
|
|
465
455
|
|
|
466
456
|
`DATABASE_URL` and every name starting with `XFLOW` are filled in by the platform: writing them
|
|
467
457
|
is refused.
|
|
@@ -571,8 +561,9 @@ Pass `DATABASE_URL` as is, without an `ssl` option. The database is PostgreSQL 1
|
|
|
571
561
|
tune it from migrations:
|
|
572
562
|
- Installed: `pg_trgm`. `gen_random_uuid()` and `uuidv7()` are built in. Never `uuid-ossp`.
|
|
573
563
|
Anything else (`postgis`, `pgvector`) is a request to the person.
|
|
574
|
-
- A statement is cut after
|
|
575
|
-
|
|
564
|
+
- A statement is cut after 10 seconds (in a migration through `xflow db migrate`, after 2 minutes),
|
|
565
|
+
waiting for a lock after 10, a transaction left open after 60. Connections to the database of
|
|
566
|
+
one project: Free 10, Start 25, Business 50. A query that needs longer is a wrong query (a missing index, or a report that belongs in a
|
|
576
567
|
scheduled function writing a summary table).
|
|
577
568
|
|
|
578
569
|
Calls of one project run at the same time in one process. A `Client`, even one made at module
|
|
@@ -633,35 +624,6 @@ key to the frontend.
|
|
|
633
624
|
A refusal comes back as `{ error, code }` (`invalid_name`, `file_too_large`, `quota_exceeded`,
|
|
634
625
|
`not_uploaded`, `duplicate_name`, `forbidden_type`, `not_found`, …): branch on `code`.
|
|
635
626
|
|
|
636
|
-
## Pages
|
|
637
|
-
|
|
638
|
-
A page is a public static site at `https://<name>.xflowpage.ru`, outside projects. Anyone with
|
|
639
|
-
the address opens it, without signing in.
|
|
640
|
-
|
|
641
|
-
- `xflow pages create "Price list" ./dist` makes a page with the built site on it and prints its
|
|
642
|
-
address: there are no empty pages. `--slug price-list` picks the name, otherwise it comes from
|
|
643
|
-
the title with a random tail.
|
|
644
|
-
- `xflow pages deploy <page> <folder|.zip|.html> -m "what changed"` uploads a new version.
|
|
645
|
-
- **Both are public the moment the command finishes**: no draft, no publish step.
|
|
646
|
-
- `xflow pages status <page>` lists the versions, `xflow pages rollback <page> <N>` puts one back
|
|
647
|
-
in seconds; the files of the last 5 are kept.
|
|
648
|
-
- `xflow pages rm <page> --yes` deletes the page: the address goes empty and every version is
|
|
649
|
-
gone for good, without `--yes` the command names what goes and stops. The name stays reserved
|
|
650
|
-
for the organization. A page put up to try something out is yours to take down when you are
|
|
651
|
-
done: do not leave it standing.
|
|
652
|
-
- Names are 3-40 characters. At most 100 pages per organization, 20 new and 200 uploads a day,
|
|
653
|
-
10 seconds between uploads of one page: batch changes into one upload.
|
|
654
|
-
- Static files only, what the build writes (`dist`), never a folder with `package.json`. At most
|
|
655
|
-
5 MB zipped, 25 MB unpacked, 1000 files. Everything a page shows travels inside it: project
|
|
656
|
-
file storage does not serve pages.
|
|
657
|
-
- A backend for a page: see **Page or app**.
|
|
658
|
-
- Everything in a page is readable by every visitor. A known key format gets the upload refused,
|
|
659
|
-
anything else simply leaks.
|
|
660
|
-
- Search engines are asked not to index pages or apps: do not promise SEO.
|
|
661
|
-
- The tab icon is the platform's on every page: do not spend a request on a favicon of your own.
|
|
662
|
-
- A page is deleted by whoever made it, or by the owner of the organization. Switching a page
|
|
663
|
-
off for a while is for a person, in the platform interface.
|
|
664
|
-
|
|
665
627
|
## Organizations and keys
|
|
666
628
|
|
|
667
629
|
One key per organization, stored side by side: `xflow org` lists them, `xflow org switch
|
package/dist/commands/pages.js
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pagesList = pagesList;
|
|
4
|
-
exports.pagesCreate = pagesCreate;
|
|
5
|
-
exports.pagesDeploy = pagesDeploy;
|
|
6
|
-
exports.pagesStatus = pagesStatus;
|
|
7
|
-
exports.pagesRollback = pagesRollback;
|
|
8
|
-
exports.pagesRemove = pagesRemove;
|
|
9
|
-
const node_fs_1 = require("node:fs");
|
|
10
|
-
const node_path_1 = require("node:path");
|
|
11
|
-
const api_1 = require("../api");
|
|
12
|
-
const args_1 = require("../args");
|
|
13
|
-
const config_1 = require("../config");
|
|
14
|
-
const credentials_1 = require("../credentials");
|
|
15
|
-
const errors_1 = require("../errors");
|
|
16
|
-
const session_1 = require("../session");
|
|
17
|
-
const ui_1 = require("../ui");
|
|
18
|
-
const zip_1 = require("../zip");
|
|
19
|
-
const deploy_1 = require("./deploy");
|
|
20
|
-
const storage_1 = require("./storage");
|
|
21
|
-
/**
|
|
22
|
-
* Pages: public sites of the organization at <name>.xflowpage.ru. They open without
|
|
23
|
-
* signing in and outside the platform, and an upload is live at once.
|
|
24
|
-
*/
|
|
25
|
-
/** Keep in sync with the platform: a refusal here saves the upload. */
|
|
26
|
-
const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
27
|
-
const MAX_UNPACKED_BYTES = 25 * 1024 * 1024;
|
|
28
|
-
const MAX_FILES = 1000;
|
|
29
|
-
/** Where build tools write the site: named when the folder turns out to be the sources. */
|
|
30
|
-
const BUILD_DIRS = ['dist', 'build', 'out'];
|
|
31
|
-
/** A page belongs to the organization, not to a project: the key is picked as for xflow org members. */
|
|
32
|
-
function pagesClient() {
|
|
33
|
-
return (0, session_1.connect)((0, config_1.localConfig)());
|
|
34
|
-
}
|
|
35
|
-
function pagePath(ref) {
|
|
36
|
-
return `/api/v1/pages/${encodeURIComponent(ref)}`;
|
|
37
|
-
}
|
|
38
|
-
function clip(text, max) {
|
|
39
|
-
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
40
|
-
}
|
|
41
|
-
function stateOf(page) {
|
|
42
|
-
if (page.status === 'blocked')
|
|
43
|
-
return 'taken down';
|
|
44
|
-
if (page.status === 'disabled')
|
|
45
|
-
return 'switched off';
|
|
46
|
-
return page.current_revision === null ? 'empty' : 'live';
|
|
47
|
-
}
|
|
48
|
-
function describeState(page) {
|
|
49
|
-
if (page.status === 'blocked') {
|
|
50
|
-
const reason = page.blocked_reason ? ': ' + page.blocked_reason : '';
|
|
51
|
-
return `Taken down by the platform${reason}. Write to team@getxflow.com if it is a mistake`;
|
|
52
|
-
}
|
|
53
|
-
if (page.status === 'disabled')
|
|
54
|
-
return 'Switched off: the address shows a placeholder until a person switches it on';
|
|
55
|
-
if (page.current_revision === null)
|
|
56
|
-
return 'Empty: nothing is uploaded yet';
|
|
57
|
-
return `Live: version ${page.current_revision}, put there ${(0, ui_1.formatAge)(page.deployed_at)}`;
|
|
58
|
-
}
|
|
59
|
-
async function pagesList(args) {
|
|
60
|
-
const client = pagesClient();
|
|
61
|
-
const { pages } = await (0, api_1.apiJson)(client, '/api/v1/pages');
|
|
62
|
-
if ((0, args_1.flagBool)(args, 'json')) {
|
|
63
|
-
(0, ui_1.out)(JSON.stringify({ pages }, null, 2));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (pages.length === 0) {
|
|
67
|
-
(0, ui_1.note)('The organization has no pages yet');
|
|
68
|
-
(0, ui_1.note)((0, ui_1.dim)(' A new one: xflow pages create "Price list" ./dist'));
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
(0, ui_1.table)(pages.map((page) => [
|
|
72
|
-
page.slug,
|
|
73
|
-
clip(page.title, 40),
|
|
74
|
-
stateOf(page),
|
|
75
|
-
(0, ui_1.formatAge)(page.deployed_at ?? page.created_at),
|
|
76
|
-
page.url,
|
|
77
|
-
]));
|
|
78
|
-
}
|
|
79
|
-
const orgs = (0, credentials_1.listOrgs)(client.apiUrl);
|
|
80
|
-
if (client.source === 'active' && orgs.length > 1) {
|
|
81
|
-
const name = orgs.find((org) => org.active)?.name;
|
|
82
|
-
const whose = name ? `"${name}"` : 'the active organization';
|
|
83
|
-
(0, ui_1.note)((0, ui_1.dim)(` Pages of ${whose} only. For another organization: xflow org switch <name or id>`));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
async function pagesCreate(args) {
|
|
87
|
-
const source = args.words.length > 2 ? args.words[args.words.length - 1] : undefined;
|
|
88
|
-
const title = args.words.slice(1, -1).join(' ').trim();
|
|
89
|
-
if (!source || !title) {
|
|
90
|
-
throw new errors_1.CliError('A title and what to put on the page are required', 'A page is born with its first version: xflow pages create "Price list" ./dist --slug price-list');
|
|
91
|
-
}
|
|
92
|
-
const upload = readUpload(source);
|
|
93
|
-
const slug = (0, args_1.flagString)(args, 'slug');
|
|
94
|
-
const query = new URLSearchParams(slug ? { title, slug } : { title });
|
|
95
|
-
(0, ui_1.step)(`Uploading ${upload.label}`);
|
|
96
|
-
const result = await (0, api_1.apiUpload)(pagesClient(), `/api/v1/pages?${query}`, upload.body, {
|
|
97
|
-
'Content-Type': contentTypeOf(upload),
|
|
98
|
-
});
|
|
99
|
-
(0, ui_1.ok)(`Page "${result.page.title}" is live: ${result.file_count} file(s), ${(0, ui_1.formatBytes)(result.size_bytes)}`);
|
|
100
|
-
(0, ui_1.out)(result.page.url);
|
|
101
|
-
(0, ui_1.note)((0, ui_1.dim)(` Anyone with the address sees it within seconds. The next version: xflow pages deploy ${result.page.slug} <path>`));
|
|
102
|
-
}
|
|
103
|
-
function contentTypeOf(upload) {
|
|
104
|
-
return upload.kind === 'zip' ? 'application/zip' : 'text/html';
|
|
105
|
-
}
|
|
106
|
-
function assertUploadSize(bytes) {
|
|
107
|
-
if (bytes <= MAX_UPLOAD_BYTES)
|
|
108
|
-
return;
|
|
109
|
-
throw new errors_1.CliError(`The upload is ${(0, ui_1.formatBytes)(bytes)}, a page takes ${MAX_UPLOAD_BYTES / 1024 / 1024} MB at most`, 'Pages are for sites, presentations and invoices: compress the images and drop the sourcemaps');
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* The folder of a built site, zipped here. The sources of a site are refused before the
|
|
113
|
-
* walk: the platform would reject them anyway, and node_modules is a long way down.
|
|
114
|
-
*/
|
|
115
|
-
function zipSite(absolute, source) {
|
|
116
|
-
const built = BUILD_DIRS.find((dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(absolute, dir, 'index.html')));
|
|
117
|
-
const pointAtBuild = built
|
|
118
|
-
? `The built site is in ${(0, node_path_1.join)(source, built)}: upload that folder`
|
|
119
|
-
: 'Build it and upload the folder the build writes';
|
|
120
|
-
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(absolute, 'package.json'))) {
|
|
121
|
-
throw new errors_1.CliError(`${source} holds the sources of a site, not a built one`, pointAtBuild);
|
|
122
|
-
}
|
|
123
|
-
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(absolute, 'index.html'))) {
|
|
124
|
-
throw new errors_1.CliError(`There is no index.html at the top of ${source}`, built ? pointAtBuild : 'The start page of a site is index.html at the top of its folder');
|
|
125
|
-
}
|
|
126
|
-
const files = (0, storage_1.collectMedia)(absolute);
|
|
127
|
-
if (files.length > MAX_FILES) {
|
|
128
|
-
throw new errors_1.CliError(`${source} holds ${files.length} files, a page takes ${MAX_FILES} at most`, 'Drop what the site never loads');
|
|
129
|
-
}
|
|
130
|
-
const bytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
131
|
-
if (bytes > MAX_UNPACKED_BYTES) {
|
|
132
|
-
throw new errors_1.CliError(`${source} is ${(0, ui_1.formatBytes)(bytes)}, a page takes ${MAX_UNPACKED_BYTES / 1024 / 1024} MB at most`, 'Compress the images and drop the sourcemaps');
|
|
133
|
-
}
|
|
134
|
-
const body = (0, zip_1.zipCreate)(files.map((file) => ({ path: file.remote, content: (0, node_fs_1.readFileSync)(file.absolute) })));
|
|
135
|
-
assertUploadSize(body.length);
|
|
136
|
-
return { kind: 'zip', body, label: `${files.length} file(s), ${(0, ui_1.formatBytes)(bytes)}` };
|
|
137
|
-
}
|
|
138
|
-
function readUpload(source) {
|
|
139
|
-
const absolute = (0, node_path_1.resolve)(source);
|
|
140
|
-
let stat;
|
|
141
|
-
try {
|
|
142
|
-
stat = (0, node_fs_1.statSync)(absolute);
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
throw new errors_1.CliError(`No such file or folder: ${source}`);
|
|
146
|
-
}
|
|
147
|
-
if (stat.isDirectory())
|
|
148
|
-
return zipSite(absolute, source);
|
|
149
|
-
const extension = (0, node_path_1.extname)(absolute).toLowerCase();
|
|
150
|
-
if (extension !== '.html' && extension !== '.htm' && extension !== '.zip') {
|
|
151
|
-
throw new errors_1.CliError(`${(0, node_path_1.basename)(absolute)} cannot be a page`, 'A page goes up as a folder with index.html at the top, its .zip, or one .html file');
|
|
152
|
-
}
|
|
153
|
-
assertUploadSize(stat.size);
|
|
154
|
-
return {
|
|
155
|
-
kind: extension === '.zip' ? 'zip' : 'html',
|
|
156
|
-
body: (0, node_fs_1.readFileSync)(absolute),
|
|
157
|
-
label: `${(0, node_path_1.basename)(absolute)}, ${(0, ui_1.formatBytes)(stat.size)}`,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
async function pagesDeploy(args) {
|
|
161
|
-
const [, ref, source] = args.words;
|
|
162
|
-
if (!ref || !source) {
|
|
163
|
-
throw new errors_1.CliError('A page and what to upload are required', 'For example: xflow pages deploy price-list ./dist. The pages: xflow pages');
|
|
164
|
-
}
|
|
165
|
-
const comment = (0, deploy_1.versionComment)((0, args_1.flagString)(args, 'message'));
|
|
166
|
-
const upload = readUpload(source);
|
|
167
|
-
const client = pagesClient();
|
|
168
|
-
(0, ui_1.step)(`Uploading ${upload.label}`);
|
|
169
|
-
const query = comment ? `?comment=${encodeURIComponent(comment)}` : '';
|
|
170
|
-
const result = await (0, api_1.apiUpload)(client, `${pagePath(ref)}/deploy${query}`, upload.body, {
|
|
171
|
-
'Content-Type': contentTypeOf(upload),
|
|
172
|
-
});
|
|
173
|
-
if (result.status === 'unchanged') {
|
|
174
|
-
(0, ui_1.ok)(`Nothing changed: version ${result.revision} is already on the address`);
|
|
175
|
-
}
|
|
176
|
-
else if (result.status === 'stored') {
|
|
177
|
-
(0, ui_1.warn)(`The page is switched off: version ${result.revision} is kept, not shown`);
|
|
178
|
-
(0, ui_1.note)((0, ui_1.dim)(' It goes live when a person switches the page on'));
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
(0, ui_1.ok)(`Version ${result.revision} is live: ${result.file_count} file(s), ${(0, ui_1.formatBytes)(result.size_bytes)}`);
|
|
182
|
-
}
|
|
183
|
-
(0, ui_1.out)(result.page.url);
|
|
184
|
-
if (result.status === 'deployed') {
|
|
185
|
-
(0, ui_1.note)((0, ui_1.dim)(` Anyone with the address sees it within seconds. The versions: xflow pages status ${result.page.slug}`));
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
async function pagesStatus(args) {
|
|
189
|
-
const ref = args.words[1];
|
|
190
|
-
if (!ref)
|
|
191
|
-
throw new errors_1.CliError('A page is required', 'The name from its address, or its id. The pages: xflow pages');
|
|
192
|
-
const data = await (0, api_1.apiJson)(pagesClient(), `${pagePath(ref)}/revisions`);
|
|
193
|
-
if ((0, args_1.flagBool)(args, 'json')) {
|
|
194
|
-
(0, ui_1.out)(JSON.stringify(data, null, 2));
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const { page, revisions } = data;
|
|
198
|
-
(0, ui_1.out)(`${(0, ui_1.bold)(page.title)} ${page.url}`);
|
|
199
|
-
(0, ui_1.out)(describeState(page));
|
|
200
|
-
if (revisions.length === 0) {
|
|
201
|
-
(0, ui_1.note)((0, ui_1.dim)(` Upload: xflow pages deploy ${page.slug} <folder, .zip or .html>`));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
(0, ui_1.out)();
|
|
205
|
-
(0, ui_1.table)(revisions.map((revision) => [
|
|
206
|
-
String(revision.revision),
|
|
207
|
-
(0, ui_1.formatAge)(revision.created_at),
|
|
208
|
-
`${revision.file_count} file(s)`,
|
|
209
|
-
(0, ui_1.formatBytes)(revision.size_bytes),
|
|
210
|
-
clip(revision.comment ?? '', 60),
|
|
211
|
-
revision.current ? 'current' : revision.restorable ? '' : 'files removed',
|
|
212
|
-
]));
|
|
213
|
-
(0, ui_1.note)((0, ui_1.dim)(` Put a version back: xflow pages rollback ${page.slug} <number>. The files of the last 5 are kept`));
|
|
214
|
-
}
|
|
215
|
-
async function pagesRollback(args) {
|
|
216
|
-
const [, ref, raw] = args.words;
|
|
217
|
-
if (!ref || !raw) {
|
|
218
|
-
throw new errors_1.CliError('A page and a version number are required', 'For example: xflow pages rollback price-list 2. The versions: xflow pages status <page>');
|
|
219
|
-
}
|
|
220
|
-
const revision = Number(raw);
|
|
221
|
-
if (!Number.isInteger(revision) || revision < 1) {
|
|
222
|
-
throw new errors_1.CliError(`A version number is a positive integer, got "${raw}"`, `The versions: xflow pages status ${ref}`);
|
|
223
|
-
}
|
|
224
|
-
const { page } = await (0, api_1.apiJson)(pagesClient(), `${pagePath(ref)}/rollback`, {
|
|
225
|
-
method: 'POST',
|
|
226
|
-
body: { revision },
|
|
227
|
-
});
|
|
228
|
-
if (page.status === 'active')
|
|
229
|
-
(0, ui_1.ok)(`Version ${revision} is back on the address`);
|
|
230
|
-
else
|
|
231
|
-
(0, ui_1.warn)(`The page is switched off: version ${revision} is current and goes live when it is switched on`);
|
|
232
|
-
(0, ui_1.out)(page.url);
|
|
233
|
-
}
|
|
234
|
-
async function pagesRemove(args) {
|
|
235
|
-
const ref = args.words[1];
|
|
236
|
-
if (!ref) {
|
|
237
|
-
throw new errors_1.CliError('A page is required', 'The name from its address, or its id. The pages: xflow pages');
|
|
238
|
-
}
|
|
239
|
-
// The page is read first: deleting is confirmed by the name in the address, and the
|
|
240
|
-
// page can be named here by its id.
|
|
241
|
-
const client = pagesClient();
|
|
242
|
-
const { page } = await (0, api_1.apiJson)(client, pagePath(ref));
|
|
243
|
-
const query = (0, args_1.flagBool)(args, 'yes') ? `?confirm=${encodeURIComponent(page.slug)}` : '';
|
|
244
|
-
try {
|
|
245
|
-
await (0, api_1.apiJson)(client, `${pagePath(page.id)}${query}`, { method: 'DELETE' });
|
|
246
|
-
}
|
|
247
|
-
catch (e) {
|
|
248
|
-
// The platform names what goes and refuses until the answer is seen. Said in the
|
|
249
|
-
// words of this command, not of the API.
|
|
250
|
-
if (e instanceof api_1.ApiError && e.code === 'confirm_required') {
|
|
251
|
-
throw new errors_1.CliError(e.message, `To delete it anyway: xflow pages rm ${page.slug} --yes`);
|
|
252
|
-
}
|
|
253
|
-
throw e;
|
|
254
|
-
}
|
|
255
|
-
(0, ui_1.ok)(`The page is deleted: ${page.url} shows nothing now`);
|
|
256
|
-
(0, ui_1.note)((0, ui_1.dim)(` Its versions are gone for good, the name ${page.slug} stays reserved for the organization`));
|
|
257
|
-
}
|