@getxflow/cli 0.33.0 → 0.34.1
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/commands/db.js +1 -1
- package/dist/commands/deploy.js +11 -2
- package/dist/commands/functions.js +25 -11
- package/dist/help.js +29 -5
- package/dist/text-file.js +13 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +15 -7
package/dist/commands/db.js
CHANGED
|
@@ -66,7 +66,7 @@ function readMigrations(root) {
|
|
|
66
66
|
.sort()
|
|
67
67
|
.map((file) => ({
|
|
68
68
|
name: file.replace(/\.sql$/i, ''),
|
|
69
|
-
sql: (0,
|
|
69
|
+
sql: (0, text_file_1.readExactTextFile)((0, node_path_1.join)(dir, file)),
|
|
70
70
|
}));
|
|
71
71
|
}
|
|
72
72
|
function requireMigrations(root) {
|
package/dist/commands/deploy.js
CHANGED
|
@@ -58,7 +58,7 @@ async function waitForBuild(client, projectId, started) {
|
|
|
58
58
|
async function refreshPlatformEnv(root, client, projectId) {
|
|
59
59
|
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
60
60
|
(0, template_1.writePlatformEnv)(root, card.project_token, client.apiUrl, card.functions, card.function_map);
|
|
61
|
-
return card.functions.filter((fn) => fn.invoke_url)
|
|
61
|
+
return card.functions.filter((fn) => fn.invoke_url);
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Start the build, asking before anything is destroyed.
|
|
@@ -191,10 +191,19 @@ async function finishBuild(client, projectId, started, root) {
|
|
|
191
191
|
(0, ui_1.note)((0, ui_1.dim)(' Publishing does not hold them back'));
|
|
192
192
|
}
|
|
193
193
|
else if (functions.length > 0) {
|
|
194
|
-
(0, ui_1.note)((0, ui_1.dim)(` Cloud functions live now: ${functions.join(', ')}`));
|
|
194
|
+
(0, ui_1.note)((0, ui_1.dim)(` Cloud functions live now: ${functions.map((fn) => fn.name).join(', ')}`));
|
|
195
195
|
if (hasSite)
|
|
196
196
|
(0, ui_1.note)((0, ui_1.dim)(' There is one copy per project, publishing does not hold them back'));
|
|
197
197
|
}
|
|
198
|
+
// An open function is called by its address and by nothing else: a page holds it
|
|
199
|
+
// in its own files. Printing it here saves the xflow functions list that every
|
|
200
|
+
// page needed anyway, and says out loud which functions this build left open.
|
|
201
|
+
const open = functions?.filter((fn) => fn.is_public) ?? [];
|
|
202
|
+
if (open.length > 0) {
|
|
203
|
+
(0, ui_1.note)((0, ui_1.dim)(' Open to anyone, no pass and no key, called at this address:'));
|
|
204
|
+
for (const fn of open)
|
|
205
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${fn.name} ${fn.invoke_url}`));
|
|
206
|
+
}
|
|
198
207
|
if (hasSite)
|
|
199
208
|
(0, ui_1.note)((0, ui_1.dim)(' Show the new pages to visitors: xflow publish'));
|
|
200
209
|
}
|
|
@@ -20,20 +20,34 @@ const FUNCTIONS_DIR = 'functions';
|
|
|
20
20
|
async function functionsList(args) {
|
|
21
21
|
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
22
22
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/functions`);
|
|
23
|
-
if (data.functions.length === 0) {
|
|
23
|
+
if (data.functions.length === 0 && !data.shared?.length) {
|
|
24
24
|
(0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow deploy`);
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
fn
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
if (data.functions.length > 0) {
|
|
28
|
+
(0, ui_1.table)(data.functions.map((fn) => [
|
|
29
|
+
fn.name,
|
|
30
|
+
fn.status === 'deployed' ? 'deployed' : fn.status === 'failed' ? 'failed' : fn.status,
|
|
31
|
+
accessLabel(fn),
|
|
32
|
+
fn.last_deployed_at ? (0, ui_1.formatAge)(fn.last_deployed_at) : '-',
|
|
33
|
+
// The address is not a secret (the app bundle carries it): a page or a
|
|
34
|
+
// webhook needs it, and asking the user to copy it out of the UI was a detour.
|
|
35
|
+
fn.invoke_url ?? '',
|
|
36
|
+
fn.error_message ?? '',
|
|
37
|
+
]));
|
|
38
|
+
}
|
|
39
|
+
// Not rows of the table above: these are not this project's to deploy, rename or
|
|
40
|
+
// delete, and invoke and logs do not reach them from here either. Under the table
|
|
41
|
+
// with the id they need, so calling one is the same command plus --project.
|
|
42
|
+
if (data.shared?.length) {
|
|
43
|
+
if (data.functions.length > 0)
|
|
44
|
+
(0, ui_1.note)();
|
|
45
|
+
(0, ui_1.note)((0, ui_1.dim)(' Also callable from this project by name, code in another project of the organization:'));
|
|
46
|
+
const width = Math.max(...data.shared.map((fn) => fn.name.length));
|
|
47
|
+
for (const fn of data.shared) {
|
|
48
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${fn.name.padEnd(width)} --project ${fn.project_id}`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
37
51
|
}
|
|
38
52
|
function prettyBody(text) {
|
|
39
53
|
try {
|
package/dist/help.js
CHANGED
|
@@ -10,7 +10,7 @@ const version_1 = require("./version");
|
|
|
10
10
|
* covers a neighbour answers for it too.
|
|
11
11
|
*/
|
|
12
12
|
const ALIASES = {
|
|
13
|
-
|
|
13
|
+
invoke: 'functions',
|
|
14
14
|
publish: 'deploy',
|
|
15
15
|
deployments: 'rollback',
|
|
16
16
|
};
|
|
@@ -562,11 +562,35 @@ has to be deployed already, since the schedule points at it and not the other wa
|
|
|
562
562
|
|
|
563
563
|
A failed run shows up in ${(0, ui_1.bold)('xflow functions logs')}: nobody is watching a scheduled
|
|
564
564
|
function, so the wrapper reports a crash the same way it does on an ordinary call.`,
|
|
565
|
-
|
|
565
|
+
functions: `${(0, ui_1.bold)('xflow functions')}: what is deployed, and calling it
|
|
566
566
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
567
|
+
xflow functions list name, state, who may call it, age, address
|
|
568
|
+
xflow functions invoke <name> call one the way the application does
|
|
569
|
+
xflow functions logs [name] crashes: stack and console output
|
|
570
|
+
|
|
571
|
+
There is no deploy command of their own: ${(0, ui_1.bold)('xflow deploy')} ships everything in
|
|
572
|
+
${(0, ui_1.bold)('functions/<name>/index.ts')} and then builds the application.
|
|
573
|
+
|
|
574
|
+
${(0, ui_1.bold)('list')} answers who may reach each function, and the code decides it:
|
|
575
|
+
|
|
576
|
+
in-app only a visitor of the application, with a pass the platform issues
|
|
577
|
+
external (N keys) plus an outside service holding a key a human issued in the interface
|
|
578
|
+
public plus anyone at all: the code says ${(0, ui_1.bold)("exports.access = 'public'")}
|
|
579
|
+
|
|
580
|
+
An open function is how a page on xflowpage.ru gets a backend: it answers without a pass
|
|
581
|
+
and without a key, so the page calls the printed address directly and bakes no key into
|
|
582
|
+
its files. Remove the line and deploy again to close it. The function that stays inside
|
|
583
|
+
the application declares nothing, and ${(0, ui_1.bold)("exports.minRole = 'developer'")} raises the
|
|
584
|
+
role the visitor needs. The address is printed for every deployed function; it is not a
|
|
585
|
+
secret, the application bundle carries it.
|
|
586
|
+
|
|
587
|
+
Under the table, when there are any: the standalone functions of the organization this
|
|
588
|
+
project calls by the same name. Their code lives elsewhere, so reaching them from here
|
|
589
|
+
takes ${(0, ui_1.bold)('--project <id>')}, which is printed next to each.
|
|
590
|
+
|
|
591
|
+
${(0, ui_1.bold)('invoke')} calls it exactly the way the application does: the project token plus a
|
|
592
|
+
visitor pass for the person who owns the key, so the function sees a real caller and its
|
|
593
|
+
role. Both are taken from the platform, no local .env is needed.
|
|
570
594
|
|
|
571
595
|
--data '{"a":1}' request body (the method becomes POST by default)
|
|
572
596
|
--data-file body.json the same body from a file
|
package/dist/text-file.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.readTextFile = readTextFile;
|
|
4
|
+
exports.readExactTextFile = readExactTextFile;
|
|
4
5
|
const node_fs_1 = require("node:fs");
|
|
5
6
|
const node_util_1 = require("node:util");
|
|
6
7
|
const errors_1 = require("./errors");
|
|
@@ -14,8 +15,19 @@ function readTextFile(path, missingHint) {
|
|
|
14
15
|
throw new errors_1.CliError(`No file ${path}`, missingHint);
|
|
15
16
|
const bytes = (0, node_fs_1.readFileSync)(path);
|
|
16
17
|
const encoding = bytes[0] === 0xff && bytes[1] === 0xfe ? 'utf-16le' : 'utf-8';
|
|
18
|
+
// trim() also drops the BOM kept by ignoreBOM: U+FEFF is whitespace in JS.
|
|
19
|
+
return decode(bytes, encoding, path).trim();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A file hashed by its exact text, so nothing may be stripped: a dropped BOM or trailing
|
|
23
|
+
* newline changes the checksum of a migration that is already applied.
|
|
24
|
+
*/
|
|
25
|
+
function readExactTextFile(path) {
|
|
26
|
+
return decode((0, node_fs_1.readFileSync)(path), 'utf-8', path);
|
|
27
|
+
}
|
|
28
|
+
function decode(bytes, encoding, path) {
|
|
17
29
|
try {
|
|
18
|
-
return new node_util_1.TextDecoder(encoding, { fatal: true }).decode(bytes)
|
|
30
|
+
return new node_util_1.TextDecoder(encoding, { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
19
31
|
}
|
|
20
32
|
catch {
|
|
21
33
|
throw new errors_1.CliError(`${path} is not UTF-8`, 'Save it as UTF-8, in Windows PowerShell: Set-Content -Encoding utf8');
|
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.
|
|
5
|
+
exports.CLI_VERSION = '0.34.1';
|
|
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
|
@@ -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
|
+
These instructions ship with xflow CLI 0.34.1, 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
|
|
@@ -69,11 +69,8 @@ deleting files from storage and linking a connected account each need a right th
|
|
|
69
69
|
default. Reading it before a long task turns a refusal halfway through the work into a
|
|
70
70
|
sentence said before it starts.
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Two of them behave unlike the rest:
|
|
73
73
|
|
|
74
|
-
- A database outlives its project (deleting a project only detaches it), so a refused
|
|
75
|
-
database usually means an orphan: delete the unused one in the "Data" section of the web
|
|
76
|
-
interface.
|
|
77
74
|
- Connections are counted while active: switching an unused one off frees the seat without
|
|
78
75
|
deleting it.
|
|
79
76
|
- The cap on how long one function call may run is written into the function when it is
|
|
@@ -280,8 +277,9 @@ bundle is built they already exist.
|
|
|
280
277
|
|
|
281
278
|
### Who is calling
|
|
282
279
|
|
|
283
|
-
A function answers
|
|
284
|
-
|
|
280
|
+
A function answers a member of the organization who has access to that project, an outside
|
|
281
|
+
service holding a key of that function, and, when the code opens it, anyone at all. The
|
|
282
|
+
platform issues a short-lived pass when it opens the application, the wrapper checks it
|
|
285
283
|
with the platform on every call, and the handler receives the answer in `event.xflow`:
|
|
286
284
|
|
|
287
285
|
```js
|
|
@@ -346,6 +344,10 @@ in the interface, and removing the line and deploying closes it again.
|
|
|
346
344
|
`xflow functions list` shows who can reach each function: `in-app only` (answers only inside
|
|
347
345
|
the application), `external (N keys)` (a human issued external access), `public` (the code
|
|
348
346
|
opened it), and the address of every deployed function. Key values are never shown there.
|
|
347
|
+
Under the table it names the standalone functions of the organization this project calls by
|
|
348
|
+
the same name, each with the `--project` id its code lives in: that list and the table
|
|
349
|
+
together are everything `xflow.functions.invoke` reaches from here. A deploy prints the
|
|
350
|
+
address of every function it left open, so a page needs no second command to find it.
|
|
349
351
|
|
|
350
352
|
## Secrets and connected accounts
|
|
351
353
|
|
|
@@ -625,6 +627,10 @@ platform: pick a page when the audience is outside the organization.
|
|
|
625
627
|
- Static files only: HTML, CSS, JS, images, fonts, PDF, audio, video. No server code, database
|
|
626
628
|
or functions on a page. Upload what the build writes (`dist`), never a folder with
|
|
627
629
|
`package.json`. At most 5 MB zipped, 25 MB unpacked, 1000 files.
|
|
630
|
+
- Everything a page shows travels inside the page. File storage of a project is not an
|
|
631
|
+
option: its addresses open only for a signed-in member with access to that project, so in a
|
|
632
|
+
page they give a visitor nothing. Heavy media has to fit those caps or be hosted elsewhere,
|
|
633
|
+
and that is worth saying before a landing page is designed around a video.
|
|
628
634
|
- A page that needs a backend (a form, a lookup) calls a cloud function of a project that
|
|
629
635
|
declares `exports.access = 'public'` (see «Who is calling»): the page fetches the address
|
|
630
636
|
from `xflow functions list` directly, with no key and no header. The function lives in a
|
|
@@ -632,6 +638,8 @@ platform: pick a page when the audience is outside the organization.
|
|
|
632
638
|
- Everything in a page is readable by every visitor. Never put a key, token or password into
|
|
633
639
|
it: a known key format gets the upload refused, anything else simply leaks.
|
|
634
640
|
- Search engines are asked not to index pages: do not promise SEO.
|
|
641
|
+
- The tab icon is the platform's on every page: the upload drops any `<link rel="…icon…">` the
|
|
642
|
+
files carry and puts the XFlow one in. Do not spend a request on a favicon of your own.
|
|
635
643
|
- The MCP `pages` tool uploads one self-contained HTML document up to 150 KB.
|
|
636
644
|
- Pages take the key of the whole organization, the one `xflow login` issues; a key scoped to
|
|
637
645
|
projects is refused, and that refusal is not yours to fix. Switching a page off and deleting it
|