@getxflow/cli 0.25.0 → 0.27.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 CHANGED
@@ -26,6 +26,7 @@ const schedules_1 = require("./commands/schedules");
26
26
  const skills_1 = require("./commands/skills");
27
27
  const sources_1 = require("./commands/sources");
28
28
  const storage_1 = require("./commands/storage");
29
+ const pages_1 = require("./commands/pages");
29
30
  const deploy_1 = require("./commands/deploy");
30
31
  const dev_1 = require("./commands/dev");
31
32
  const update_1 = require("./commands/update");
@@ -194,6 +195,28 @@ async function run(args) {
194
195
  return;
195
196
  }
196
197
  throw new errors_1.CliError(`Unknown command: storage ${second}`, 'Available: ls, push and rm');
198
+ case 'pages':
199
+ if (second === 'create') {
200
+ await (0, pages_1.pagesCreate)(rest);
201
+ return;
202
+ }
203
+ if (second === 'deploy') {
204
+ await (0, pages_1.pagesDeploy)(rest);
205
+ return;
206
+ }
207
+ if (second === 'status') {
208
+ await (0, pages_1.pagesStatus)(rest);
209
+ return;
210
+ }
211
+ if (second === 'rollback') {
212
+ await (0, pages_1.pagesRollback)(rest);
213
+ return;
214
+ }
215
+ if (second === undefined || second === 'list') {
216
+ await (0, pages_1.pagesList)(rest);
217
+ return;
218
+ }
219
+ throw new errors_1.CliError(`Unknown command: pages ${second}`, 'Available: list, create, deploy, status and rollback');
197
220
  case 'status':
198
221
  await (0, sources_1.status)();
199
222
  return;
@@ -294,11 +317,15 @@ async function main() {
294
317
  (0, ui_1.fatalNote)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
295
318
  if (e.hint)
296
319
  (0, ui_1.fatalNote)((0, ui_1.dim)(` ${e.hint}`));
320
+ if (e.code === 'unauthorized' && process.env.XFLOW_TOKEN?.trim()) {
321
+ // Signing in again changes nothing here: the stored key is not the one being sent.
322
+ (0, ui_1.fatalNote)((0, ui_1.dim)(' XFLOW_TOKEN is set, so that is the key refused: put the current one there'));
323
+ }
297
324
  if (e.code === 'not_found') {
298
325
  // XFLOW_TOKEN is taken as given and no organization is looked for, so a
299
326
  // project of another one answers "not found" with nothing else to say.
300
327
  if (process.env.XFLOW_TOKEN?.trim()) {
301
- (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'));
328
+ (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'));
302
329
  }
303
330
  else if ((0, credentials_1.anyMultipleOrgs)()) {
304
331
  (0, ui_1.fatalNote)((0, ui_1.dim)(' Several organizations are signed in: the answer may live in another one, see xflow org'));
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.versionComment = versionComment;
3
4
  exports.deploy = deploy;
4
5
  exports.publish = publish;
5
6
  exports.rollback = rollback;
@@ -118,21 +119,26 @@ async function assertBuildAllowed(client, projectId) {
118
119
  const MAX_COMMENT_LENGTH = 200;
119
120
  /** Control and bidi-override characters: they spoof terminals, not comments. */
120
121
  const COMMENT_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
122
+ /** The version comment as typed after -m, cleaned; empty when there is none. Pages take the same. */
123
+ function versionComment(raw) {
124
+ const comment = (raw ?? '')
125
+ .replace(/\s+/g, ' ')
126
+ .replace(COMMENT_CONTROL_CHARS, '')
127
+ .trim();
128
+ if (comment.length > MAX_COMMENT_LENGTH) {
129
+ throw new errors_1.CliError(`The version comment is too long: ${MAX_COMMENT_LENGTH} characters at most`, 'One short line about what changed in this version is enough');
130
+ }
131
+ return comment;
132
+ }
121
133
  /**
122
134
  * The version comment, checked before anything goes up: a refusal after the push
123
135
  * would leave the project with a revision newer than its build for no good reason.
124
136
  */
125
137
  function requireComment(args) {
126
- const comment = ((0, args_1.flagString)(args, 'message') ?? '')
127
- .replace(/\s+/g, ' ')
128
- .replace(COMMENT_CONTROL_CHARS, '')
129
- .trim();
138
+ const comment = versionComment((0, args_1.flagString)(args, 'message'));
130
139
  if (!comment) {
131
140
  throw new errors_1.CliError('Every version needs a comment saying what changed in it', 'Add -m: xflow deploy -m "Cart and card payments". One short line, written in the language the user speaks: it names this version in the history');
132
141
  }
133
- if (comment.length > MAX_COMMENT_LENGTH) {
134
- throw new errors_1.CliError(`The version comment is too long: ${MAX_COMMENT_LENGTH} characters at most`, 'One short line about what changed in this version is enough');
135
- }
136
142
  return comment;
137
143
  }
138
144
  async function deploy(args) {
@@ -0,0 +1,232 @@
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
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const api_1 = require("../api");
11
+ const args_1 = require("../args");
12
+ const config_1 = require("../config");
13
+ const credentials_1 = require("../credentials");
14
+ const errors_1 = require("../errors");
15
+ const session_1 = require("../session");
16
+ const ui_1 = require("../ui");
17
+ const zip_1 = require("../zip");
18
+ const deploy_1 = require("./deploy");
19
+ const storage_1 = require("./storage");
20
+ /**
21
+ * Pages: public sites of the organization at <name>.xflowpage.ru. They open without
22
+ * signing in and outside the platform, and an upload is live at once.
23
+ */
24
+ /** Keep in sync with the platform: a refusal here saves the upload. */
25
+ const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
26
+ const MAX_UNPACKED_BYTES = 25 * 1024 * 1024;
27
+ const MAX_FILES = 1000;
28
+ /** Where build tools write the site: named when the folder turns out to be the sources. */
29
+ const BUILD_DIRS = ['dist', 'build', 'out'];
30
+ /** A page belongs to the organization, not to a project: the key is picked as for xflow org members. */
31
+ function pagesClient() {
32
+ return (0, session_1.connect)((0, config_1.localConfig)());
33
+ }
34
+ function pagePath(ref) {
35
+ return `/api/v1/pages/${encodeURIComponent(ref)}`;
36
+ }
37
+ function clip(text, max) {
38
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
39
+ }
40
+ function stateOf(page) {
41
+ if (page.status === 'blocked')
42
+ return 'taken down';
43
+ if (page.status === 'disabled')
44
+ return 'switched off';
45
+ return page.current_revision === null ? 'empty' : 'live';
46
+ }
47
+ function describeState(page) {
48
+ if (page.status === 'blocked') {
49
+ const reason = page.blocked_reason ? ': ' + page.blocked_reason : '';
50
+ return `Taken down by the platform${reason}. Write to team@getxflow.com if it is a mistake`;
51
+ }
52
+ if (page.status === 'disabled')
53
+ return 'Switched off: the address shows a placeholder until a person switches it on';
54
+ if (page.current_revision === null)
55
+ return 'Empty: nothing is uploaded yet';
56
+ return `Live: version ${page.current_revision}, put there ${(0, ui_1.formatAge)(page.deployed_at)}`;
57
+ }
58
+ async function pagesList(args) {
59
+ const client = pagesClient();
60
+ const { pages } = await (0, api_1.apiJson)(client, '/api/v1/pages');
61
+ if ((0, args_1.flagBool)(args, 'json')) {
62
+ (0, ui_1.out)(JSON.stringify({ pages }, null, 2));
63
+ return;
64
+ }
65
+ if (pages.length === 0) {
66
+ (0, ui_1.note)('The organization has no pages yet');
67
+ (0, ui_1.note)((0, ui_1.dim)(' A new one: xflow pages create "Price list" ./dist'));
68
+ }
69
+ else {
70
+ (0, ui_1.table)(pages.map((page) => [
71
+ page.slug,
72
+ clip(page.title, 40),
73
+ stateOf(page),
74
+ (0, ui_1.formatAge)(page.deployed_at ?? page.created_at),
75
+ page.url,
76
+ ]));
77
+ }
78
+ const orgs = (0, credentials_1.listOrgs)(client.apiUrl);
79
+ if (client.source === 'active' && orgs.length > 1) {
80
+ const name = orgs.find((org) => org.active)?.name;
81
+ const whose = name ? `"${name}"` : 'the active organization';
82
+ (0, ui_1.note)((0, ui_1.dim)(` Pages of ${whose} only. For another organization: xflow org switch <name or id>`));
83
+ }
84
+ }
85
+ async function pagesCreate(args) {
86
+ const source = args.words.length > 2 ? args.words[args.words.length - 1] : undefined;
87
+ const title = args.words.slice(1, -1).join(' ').trim();
88
+ if (!source || !title) {
89
+ 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');
90
+ }
91
+ const upload = readUpload(source);
92
+ const slug = (0, args_1.flagString)(args, 'slug');
93
+ const query = new URLSearchParams(slug ? { title, slug } : { title });
94
+ (0, ui_1.step)(`Uploading ${upload.label}`);
95
+ const result = await (0, api_1.apiUpload)(pagesClient(), `/api/v1/pages?${query}`, upload.body, {
96
+ 'Content-Type': contentTypeOf(upload),
97
+ });
98
+ (0, ui_1.ok)(`Page "${result.page.title}" is live: ${result.file_count} file(s), ${(0, ui_1.formatBytes)(result.size_bytes)}`);
99
+ (0, ui_1.out)(result.page.url);
100
+ (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>`));
101
+ }
102
+ function contentTypeOf(upload) {
103
+ return upload.kind === 'zip' ? 'application/zip' : 'text/html';
104
+ }
105
+ function assertUploadSize(bytes) {
106
+ if (bytes <= MAX_UPLOAD_BYTES)
107
+ return;
108
+ 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');
109
+ }
110
+ /**
111
+ * The folder of a built site, zipped here. The sources of a site are refused before the
112
+ * walk: the platform would reject them anyway, and node_modules is a long way down.
113
+ */
114
+ function zipSite(absolute, source) {
115
+ const built = BUILD_DIRS.find((dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(absolute, dir, 'index.html')));
116
+ const pointAtBuild = built
117
+ ? `The built site is in ${(0, node_path_1.join)(source, built)}: upload that folder`
118
+ : 'Build it and upload the folder the build writes';
119
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(absolute, 'package.json'))) {
120
+ throw new errors_1.CliError(`${source} holds the sources of a site, not a built one`, pointAtBuild);
121
+ }
122
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(absolute, 'index.html'))) {
123
+ 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');
124
+ }
125
+ const files = (0, storage_1.collectMedia)(absolute);
126
+ if (files.length > MAX_FILES) {
127
+ throw new errors_1.CliError(`${source} holds ${files.length} files, a page takes ${MAX_FILES} at most`, 'Drop what the site never loads');
128
+ }
129
+ const bytes = files.reduce((sum, file) => sum + file.size, 0);
130
+ if (bytes > MAX_UNPACKED_BYTES) {
131
+ 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');
132
+ }
133
+ const body = (0, zip_1.zipCreate)(files.map((file) => ({ path: file.remote, content: (0, node_fs_1.readFileSync)(file.absolute) })));
134
+ assertUploadSize(body.length);
135
+ return { kind: 'zip', body, label: `${files.length} file(s), ${(0, ui_1.formatBytes)(bytes)}` };
136
+ }
137
+ function readUpload(source) {
138
+ const absolute = (0, node_path_1.resolve)(source);
139
+ let stat;
140
+ try {
141
+ stat = (0, node_fs_1.statSync)(absolute);
142
+ }
143
+ catch {
144
+ throw new errors_1.CliError(`No such file or folder: ${source}`);
145
+ }
146
+ if (stat.isDirectory())
147
+ return zipSite(absolute, source);
148
+ const extension = (0, node_path_1.extname)(absolute).toLowerCase();
149
+ if (extension !== '.html' && extension !== '.htm' && extension !== '.zip') {
150
+ 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');
151
+ }
152
+ assertUploadSize(stat.size);
153
+ return {
154
+ kind: extension === '.zip' ? 'zip' : 'html',
155
+ body: (0, node_fs_1.readFileSync)(absolute),
156
+ label: `${(0, node_path_1.basename)(absolute)}, ${(0, ui_1.formatBytes)(stat.size)}`,
157
+ };
158
+ }
159
+ async function pagesDeploy(args) {
160
+ const [, ref, source] = args.words;
161
+ if (!ref || !source) {
162
+ 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');
163
+ }
164
+ const comment = (0, deploy_1.versionComment)((0, args_1.flagString)(args, 'message'));
165
+ const upload = readUpload(source);
166
+ const client = pagesClient();
167
+ (0, ui_1.step)(`Uploading ${upload.label}`);
168
+ const query = comment ? `?comment=${encodeURIComponent(comment)}` : '';
169
+ const result = await (0, api_1.apiUpload)(client, `${pagePath(ref)}/deploy${query}`, upload.body, {
170
+ 'Content-Type': contentTypeOf(upload),
171
+ });
172
+ if (result.status === 'unchanged') {
173
+ (0, ui_1.ok)(`Nothing changed: version ${result.revision} is already on the address`);
174
+ }
175
+ else if (result.status === 'stored') {
176
+ (0, ui_1.warn)(`The page is switched off: version ${result.revision} is kept, not shown`);
177
+ (0, ui_1.note)((0, ui_1.dim)(' It goes live when a person switches the page on'));
178
+ }
179
+ else {
180
+ (0, ui_1.ok)(`Version ${result.revision} is live: ${result.file_count} file(s), ${(0, ui_1.formatBytes)(result.size_bytes)}`);
181
+ }
182
+ (0, ui_1.out)(result.page.url);
183
+ if (result.status === 'deployed') {
184
+ (0, ui_1.note)((0, ui_1.dim)(` Anyone with the address sees it within seconds. The versions: xflow pages status ${result.page.slug}`));
185
+ }
186
+ }
187
+ async function pagesStatus(args) {
188
+ const ref = args.words[1];
189
+ if (!ref)
190
+ throw new errors_1.CliError('A page is required', 'The name from its address, or its id. The pages: xflow pages');
191
+ const data = await (0, api_1.apiJson)(pagesClient(), `${pagePath(ref)}/revisions`);
192
+ if ((0, args_1.flagBool)(args, 'json')) {
193
+ (0, ui_1.out)(JSON.stringify(data, null, 2));
194
+ return;
195
+ }
196
+ const { page, revisions } = data;
197
+ (0, ui_1.out)(`${(0, ui_1.bold)(page.title)} ${page.url}`);
198
+ (0, ui_1.out)(describeState(page));
199
+ if (revisions.length === 0) {
200
+ (0, ui_1.note)((0, ui_1.dim)(` Upload: xflow pages deploy ${page.slug} <folder, .zip or .html>`));
201
+ return;
202
+ }
203
+ (0, ui_1.out)();
204
+ (0, ui_1.table)(revisions.map((revision) => [
205
+ String(revision.revision),
206
+ (0, ui_1.formatAge)(revision.created_at),
207
+ `${revision.file_count} file(s)`,
208
+ (0, ui_1.formatBytes)(revision.size_bytes),
209
+ clip(revision.comment ?? '', 60),
210
+ revision.current ? 'current' : revision.restorable ? '' : 'files removed',
211
+ ]));
212
+ (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`));
213
+ }
214
+ async function pagesRollback(args) {
215
+ const [, ref, raw] = args.words;
216
+ if (!ref || !raw) {
217
+ 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>');
218
+ }
219
+ const revision = Number(raw);
220
+ if (!Number.isInteger(revision) || revision < 1) {
221
+ throw new errors_1.CliError(`A version number is a positive integer, got "${raw}"`, `The versions: xflow pages status ${ref}`);
222
+ }
223
+ const { page } = await (0, api_1.apiJson)(pagesClient(), `${pagePath(ref)}/rollback`, {
224
+ method: 'POST',
225
+ body: { revision },
226
+ });
227
+ if (page.status === 'active')
228
+ (0, ui_1.ok)(`Version ${revision} is back on the address`);
229
+ else
230
+ (0, ui_1.warn)(`The page is switched off: version ${revision} is current and goes live when it is switched on`);
231
+ (0, ui_1.out)(page.url);
232
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.contentTypeFor = contentTypeFor;
4
+ exports.collectMedia = collectMedia;
4
5
  exports.planBatches = planBatches;
5
6
  exports.caseCollisions = caseCollisions;
6
7
  exports.storageList = storageList;
@@ -74,7 +75,7 @@ function contentTypeFor(name) {
74
75
  * what one is asked to put there so it stays out of the 10 MB archive.
75
76
  *
76
77
  * Dot entries and symlinks are skipped: pointing push at a working copy should
77
- * not upload .git, and a link is not ours to follow.
78
+ * not upload .git, and a link is not ours to follow. Pages walk a site the same way.
78
79
  */
79
80
  function collectMedia(root, prefix = '') {
80
81
  const found = [];
package/dist/flags.js CHANGED
@@ -81,6 +81,12 @@ exports.KNOWN = {
81
81
  'storage push': ['to', 'replace', 'json', 'project'],
82
82
  'storage rm': ['folder', 'yes', 'project'],
83
83
  'storage remove': ['folder', 'yes', 'project'],
84
+ pages: ['json'],
85
+ 'pages list': ['json'],
86
+ 'pages create': ['slug'],
87
+ 'pages deploy': ['message'],
88
+ 'pages status': ['json'],
89
+ 'pages rollback': [],
84
90
  status: [],
85
91
  dev: [],
86
92
  pull: ['into', 'force', 'revision'],
@@ -127,6 +133,7 @@ const NAMESPACES = new Set([
127
133
  'schedules',
128
134
  'db',
129
135
  'storage',
136
+ 'pages',
130
137
  'mcp',
131
138
  ]);
132
139
  /** The command these words name, as far as the table knows it. */
package/dist/help.js CHANGED
@@ -89,6 +89,15 @@ ${(0, ui_1.bold)('Database')}
89
89
  xflow db migrate [--dry-run] [--allow-destructive]
90
90
  apply the migrations from migrations/*.sql
91
91
 
92
+ ${(0, ui_1.bold)('Pages')}
93
+ xflow pages public sites of the organization at <name>.xflowpage.ru
94
+ xflow pages create <title> <folder|.zip|.html> [--slug name]
95
+ a new page with the site on it, live at once
96
+ xflow pages deploy <page> <folder|.zip|.html> [-m "what changed"]
97
+ upload the site: live at once, there is no publish step
98
+ xflow pages status <page> the page and its versions
99
+ xflow pages rollback <page> <N> put an earlier version back on the address
100
+
92
101
  ${(0, ui_1.bold)('Reference')}
93
102
  xflow logs [--limit N] browser errors from the released application
94
103
  xflow projects list projects of the organization
@@ -202,6 +211,58 @@ active one outside. With several signed in, a line under the table says whose
202
211
  projects these are.
203
212
 
204
213
  ${(0, ui_1.bold)('xflow link <id>')} binds the current folder to a project; ${(0, ui_1.bold)('xflow init')} makes a new one.`,
214
+ link: `${(0, ui_1.bold)('xflow link')} <id>: bind this folder to a project
215
+
216
+ Writes xflow.json and .xflow/state.json, points .env at the project and lays down the
217
+ instructions for the agents. The id comes from ${(0, ui_1.bold)('xflow projects list')}; a project of
218
+ another signed-in organization is found too, and the folder is bound to that organization.
219
+
220
+ An empty folder (.git alone counts as empty) also gets the latest sources, so there is no
221
+ ${(0, ui_1.bold)('pull')} after it: pull would refuse the folder anyway, it is not empty any more. A
222
+ folder with files keeps them as they are: ${(0, ui_1.bold)('xflow status')} says how they differ from
223
+ the server revision, ${(0, ui_1.bold)('xflow pull --into ./server-copy')} fetches it next to them.
224
+
225
+ In a folder that is already linked, the command points xflow.json at the other project and
226
+ keeps the rest of the file. A new project instead of an existing one: ${(0, ui_1.bold)('xflow init')}.`,
227
+ pages: `${(0, ui_1.bold)('xflow pages')}: public sites of the organization
228
+
229
+ A page is a static site at https://<name>.xflowpage.ru: a landing page, a presentation, an
230
+ invoice for people outside the organization. It opens by its address for anyone, without
231
+ signing in and outside the platform. A project is the opposite: it opens only inside the
232
+ platform, for the members of the organization.
233
+
234
+ xflow pages the pages, their state and address
235
+ xflow pages create <title> <path> a new page with its first version: there are no
236
+ empty pages
237
+ --slug <name> the name in the address; without it the name is made
238
+ from the title, with a random tail
239
+ xflow pages deploy <page> <path> upload a new version
240
+ -m <text> what changed, one short line: it names the version
241
+ xflow pages status <page> the page and its versions
242
+ xflow pages rollback <page> <N> put version N back on the address
243
+
244
+ ${(0, ui_1.bold)('<path>')} is a folder with index.html at the top, its .zip, or one .html file.
245
+ ${(0, ui_1.bold)('<page>')} is the name from the address or the id. ${(0, ui_1.bold)('--json')} on list and status
246
+ prints the answer as JSON. A page belongs to the organization, not to a project, so there is
247
+ no ${(0, ui_1.bold)('--project')}: the key is picked as for every other command (${(0, ui_1.bold)('xflow help org')}).
248
+
249
+ ${(0, ui_1.bold)('An upload is live at once.')} There is no draft and no publish step: anyone with the
250
+ address sees the new version within seconds. Uploading the same files again changes nothing.
251
+ The files of the last 5 versions are kept, so a rollback takes seconds and needs no upload.
252
+
253
+ What goes up is the built site: HTML, CSS, JS, images, fonts, PDF, audio and video. No code
254
+ runs on the platform side, and a folder with package.json is refused as the sources of a site:
255
+ build it and upload what the build writes. Dot entries are skipped. At most 5 MB zipped, 25 MB
256
+ unpacked and 1000 files.
257
+
258
+ The page is public, so a file holding a secret key is refused whole: every visitor would read
259
+ it. A function key is the exception: it opens that one function to every visitor, which is
260
+ what a form on a landing page needs. Search engines are asked not to index pages, so do not
261
+ promise SEO.
262
+
263
+ A page takes the key of the whole organization, the one ${(0, ui_1.bold)('xflow login')} issues. The
264
+ section is in beta: XFlow support switches it on for an organization. Switching a page off
265
+ and deleting it are for a person, in the platform interface.`,
205
266
  update: `${(0, ui_1.bold)('xflow update')}: bring the CLI up to date
206
267
 
207
268
  Installs the published version and, if anything changed, rewrites the platform
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.25.0';
5
+ exports.CLI_VERSION = '0.27.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -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 the project root has xflow.json or VITE_XFLOW_* variables, and for any task that touches deployment, publishing, rollback, source sync, 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, or UI built on the platform design system.
3
+ description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use whenever the project root has xflow.json or VITE_XFLOW_* variables, and for any task that touches deployment, publishing, rollback, source sync, 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 platform design system, or a public page on xflowpage.ru (a landing page, presentation or invoice for people outside the organization).
4
4
  ---
5
5
 
6
6
  # XFlow
@@ -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.0, so the copy you are reading can be older than
25
+ These instructions ship with xflow CLI 0.27.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
@@ -125,9 +125,9 @@ outside the project folder); building it over the current one needs a human.
125
125
 
126
126
  Pages are kept for the **three latest successful builds** plus whatever dev and live show;
127
127
  older versions stay in the history with `pages_kept` false, readable but not openable. The
128
- history holds the **last 30 builds**, the sources the **last 50 revisions** (a revision a live
129
- version was built from is never deleted). Three builds is the whole rollback depth, hours on an
130
- active project: keep anything you must not lose in your own git repository.
128
+ history holds the **last 30 builds**, each keeping the revision it was built from; any other
129
+ revision is kept only while it is among the **10 latest**. Three builds is the whole rollback
130
+ depth, hours on an active project: keep anything you must not lose in your own git repository.
131
131
 
132
132
  **The only link you give a person is the project page**, `https://app.getxflow.com/projects/<id>`,
133
133
  which the CLI prints for you. Refer to builds by their number ("version 481203 is built, 092399
@@ -520,6 +520,34 @@ What the app may do with files is decided inside that function, because the page
520
520
  by whoever opened it. Never send the key to the frontend: the build gate stops on a key found in
521
521
  the sources, and a key that reached a visitor deletes every file of the project.
522
522
 
523
+ ## Pages
524
+
525
+ A page is a public static site at `https://<name>.xflowpage.ru`: a landing page, a
526
+ presentation, an invoice for people outside the organization. Anyone with the address opens it,
527
+ without signing in and outside the platform. A project is the opposite, it opens only inside the
528
+ platform: pick a page when the audience is outside the organization.
529
+
530
+ - `xflow pages create "Price list" ./dist` makes a page with the built site on it and prints its
531
+ address: there are no empty pages. `--slug price-list` picks the name, otherwise it comes from
532
+ the title with a random tail.
533
+ - `xflow pages deploy <page> <folder|.zip|.html> -m "what changed"` uploads a new version.
534
+ - **Both are public the moment the command finishes**: no draft, no publish step. Upload when the
535
+ user wants the page out, and give them the address the CLI printed.
536
+ - `xflow pages status <page>` lists the versions, `xflow pages rollback <page> <N>` puts one back
537
+ in seconds; the files of the last 5 are kept.
538
+ - Static files only: HTML, CSS, JS, images, fonts, PDF, audio, video. No server code, database
539
+ or functions on a page. Upload what the build writes (`dist`), never a folder with
540
+ `package.json`. At most 5 MB zipped, 25 MB unpacked, 1000 files.
541
+ - Everything in a page is readable by every visitor. Never put a key, token or password into
542
+ it: a known key format gets the upload refused, anything else simply leaks. A function key is
543
+ the exception: a person issues it in the settings of the function, and a form on a page may
544
+ call that function with it. So can every visitor.
545
+ - Search engines are asked not to index pages: do not promise SEO.
546
+ - The MCP `pages` tool uploads one self-contained HTML document up to 150 KB.
547
+ - Pages take the key of the whole organization, the one `xflow login` issues. The section is in
548
+ beta, switched on per organization by XFlow support: that refusal is not yours to fix. Switching
549
+ a page off and deleting it are for a person, in the platform interface.
550
+
523
551
  ## Organizations and keys
524
552
 
525
553
  One key per organization, stored side by side rather than replacing each other: `xflow org`