@getxflow/cli 1.4.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 +2 -33
- package/dist/commands/db.js +11 -151
- package/dist/commands/deploy.js +1 -2
- package/dist/commands/env.js +4 -9
- package/dist/commands/projects.js +2 -4
- package/dist/commands/storage.js +1 -2
- package/dist/flags.js +2 -12
- package/dist/help.js +7 -89
- package/dist/limits.js +3 -5
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +681 -735
- package/dist/commands/pages.js +0 -257
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
|
-
}
|