@getxflow/cli 0.32.0 → 0.34.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/commands/db.js +2 -2
- package/dist/commands/deploy.js +11 -2
- package/dist/commands/functions.js +25 -11
- package/dist/help.js +34 -9
- package/dist/text-file.js +13 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +20 -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) {
|
|
@@ -130,7 +130,7 @@ async function dbStatus(args) {
|
|
|
130
130
|
(0, ui_1.table)(rows);
|
|
131
131
|
if (rows.some((row) => row[1] === 'applied, file changed')) {
|
|
132
132
|
(0, ui_1.out)('');
|
|
133
|
-
(0, ui_1.warn)('A changed file
|
|
133
|
+
(0, ui_1.warn)('A changed file is never applied again and db migrate refuses it: write a new migration');
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
function serverRow(server) {
|
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
|
};
|
|
@@ -354,13 +354,14 @@ kept for 7 days: that is a "caught it right away" safety net, not a backup.
|
|
|
354
354
|
${(0, ui_1.bold)('DROP DATABASE')} is never allowed, because the database is shared across the
|
|
355
355
|
organization.
|
|
356
356
|
|
|
357
|
-
Editing an already applied file achieves nothing:
|
|
358
|
-
content
|
|
359
|
-
instead.
|
|
357
|
+
Editing an already applied file achieves nothing: ${(0, ui_1.bold)('migrate')} refuses a name that is
|
|
358
|
+
already applied with different content, and ${(0, ui_1.bold)('xflow db status')} lists such a file
|
|
359
|
+
separately. Write a new migration instead.
|
|
360
360
|
|
|
361
361
|
One logical database can be attached to several projects, so the history may hold
|
|
362
362
|
migrations that are not in your repository. That is normal, but it also means your
|
|
363
|
-
migration can break somebody else's application
|
|
363
|
+
migration can break somebody else's application, and that a name already in that history
|
|
364
|
+
is taken for you too: ${(0, ui_1.bold)('0001_init')} of a neighbour leaves yours no room.`,
|
|
364
365
|
logs: `${(0, ui_1.bold)('xflow logs')} and ${(0, ui_1.bold)('xflow functions logs')}: project errors
|
|
365
366
|
|
|
366
367
|
One stream per project: crashes of the released application in the browser and
|
|
@@ -561,11 +562,35 @@ has to be deployed already, since the schedule points at it and not the other wa
|
|
|
561
562
|
|
|
562
563
|
A failed run shows up in ${(0, ui_1.bold)('xflow functions logs')}: nobody is watching a scheduled
|
|
563
564
|
function, so the wrapper reports a crash the same way it does on an ordinary call.`,
|
|
564
|
-
|
|
565
|
+
functions: `${(0, ui_1.bold)('xflow functions')}: what is deployed, and calling it
|
|
565
566
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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.
|
|
569
594
|
|
|
570
595
|
--data '{"a":1}' request body (the method becomes POST by default)
|
|
571
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.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
|
@@ -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.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
|
|
@@ -48,8 +48,8 @@ the error points away from the cause. The sections below carry the details.
|
|
|
48
48
|
3. Never delete a `functions/<name>/` directory unless the user asked for that
|
|
49
49
|
function to go. The next deploy removes it from the cloud together with its
|
|
50
50
|
schedules, and a function created again later gets a different address.
|
|
51
|
-
4. An already applied migration is never re-run,
|
|
52
|
-
|
|
51
|
+
4. An already applied migration is never re-run, and `xflow db migrate` refuses a
|
|
52
|
+
name it already holds with different content. A schema change is always a new file.
|
|
53
53
|
5. When inserts start failing with `db_write_locked`, the database is over its plan size
|
|
54
54
|
and Postgres itself refuses. Reads and deletes keep working, so the fix is a migration
|
|
55
55
|
that deletes data, never a rewrite of the SQL. Write comes back within the hour.
|
|
@@ -280,8 +280,9 @@ bundle is built they already exist.
|
|
|
280
280
|
|
|
281
281
|
### Who is calling
|
|
282
282
|
|
|
283
|
-
A function answers
|
|
284
|
-
|
|
283
|
+
A function answers a member of the organization who has access to that project, an outside
|
|
284
|
+
service holding a key of that function, and, when the code opens it, anyone at all. The
|
|
285
|
+
platform issues a short-lived pass when it opens the application, the wrapper checks it
|
|
285
286
|
with the platform on every call, and the handler receives the answer in `event.xflow`:
|
|
286
287
|
|
|
287
288
|
```js
|
|
@@ -346,6 +347,10 @@ in the interface, and removing the line and deploying closes it again.
|
|
|
346
347
|
`xflow functions list` shows who can reach each function: `in-app only` (answers only inside
|
|
347
348
|
the application), `external (N keys)` (a human issued external access), `public` (the code
|
|
348
349
|
opened it), and the address of every deployed function. Key values are never shown there.
|
|
350
|
+
Under the table it names the standalone functions of the organization this project calls by
|
|
351
|
+
the same name, each with the `--project` id its code lives in: that list and the table
|
|
352
|
+
together are everything `xflow.functions.invoke` reaches from here. A deploy prints the
|
|
353
|
+
address of every function it left open, so a page needs no second command to find it.
|
|
349
354
|
|
|
350
355
|
## Secrets and connected accounts
|
|
351
356
|
|
|
@@ -450,8 +455,10 @@ xflow functions logs report # empty output means it never crashed
|
|
|
450
455
|
|
|
451
456
|
Schema changes are files: `migrations/0001_init.sql`, `migrations/0002_orders.sql`, applied
|
|
452
457
|
in filename order by `xflow db migrate`. `xflow db status` shows what is applied and what
|
|
453
|
-
waits. History lives in the database itself,
|
|
454
|
-
|
|
458
|
+
waits. History lives in the database itself, and the database can be shared with other
|
|
459
|
+
projects of the organization, so a migration name is taken once for all of them: `migrate`
|
|
460
|
+
refuses a name that is already applied with different content. Never edit an applied file,
|
|
461
|
+
write a new migration instead.
|
|
455
462
|
|
|
456
463
|
The browser never reaches the database directly. The app reads and writes through a cloud
|
|
457
464
|
function, and inside the handler the connection string is already there:
|
|
@@ -623,6 +630,10 @@ platform: pick a page when the audience is outside the organization.
|
|
|
623
630
|
- Static files only: HTML, CSS, JS, images, fonts, PDF, audio, video. No server code, database
|
|
624
631
|
or functions on a page. Upload what the build writes (`dist`), never a folder with
|
|
625
632
|
`package.json`. At most 5 MB zipped, 25 MB unpacked, 1000 files.
|
|
633
|
+
- Everything a page shows travels inside the page. File storage of a project is not an
|
|
634
|
+
option: its addresses open only for a signed-in member with access to that project, so in a
|
|
635
|
+
page they give a visitor nothing. Heavy media has to fit those caps or be hosted elsewhere,
|
|
636
|
+
and that is worth saying before a landing page is designed around a video.
|
|
626
637
|
- A page that needs a backend (a form, a lookup) calls a cloud function of a project that
|
|
627
638
|
declares `exports.access = 'public'` (see «Who is calling»): the page fetches the address
|
|
628
639
|
from `xflow functions list` directly, with no key and no header. The function lives in a
|
|
@@ -630,6 +641,8 @@ platform: pick a page when the audience is outside the organization.
|
|
|
630
641
|
- Everything in a page is readable by every visitor. Never put a key, token or password into
|
|
631
642
|
it: a known key format gets the upload refused, anything else simply leaks.
|
|
632
643
|
- Search engines are asked not to index pages: do not promise SEO.
|
|
644
|
+
- The tab icon is the platform's on every page: the upload drops any `<link rel="…icon…">` the
|
|
645
|
+
files carry and puts the XFlow one in. Do not spend a request on a favicon of your own.
|
|
633
646
|
- The MCP `pages` tool uploads one self-contained HTML document up to 150 KB.
|
|
634
647
|
- Pages take the key of the whole organization, the one `xflow login` issues; a key scoped to
|
|
635
648
|
projects is refused, and that refusal is not yours to fix. Switching a page off and deleting it
|