@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.
@@ -66,7 +66,7 @@ function readMigrations(root) {
66
66
  .sort()
67
67
  .map((file) => ({
68
68
  name: file.replace(/\.sql$/i, ''),
69
- sql: (0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, file), 'utf-8'),
69
+ sql: (0, text_file_1.readExactTextFile)((0, node_path_1.join)(dir, file)),
70
70
  }));
71
71
  }
72
72
  function requireMigrations(root) {
@@ -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).map((fn) => fn.name);
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
- (0, ui_1.table)(data.functions.map((fn) => [
28
- fn.name,
29
- fn.status === 'deployed' ? 'deployed' : fn.status === 'failed' ? 'failed' : fn.status,
30
- accessLabel(fn),
31
- fn.last_deployed_at ? (0, ui_1.formatAge)(fn.last_deployed_at) : '-',
32
- // The address is not a secret (the app bundle carries it): a page or a
33
- // webhook needs it, and asking the user to copy it out of the UI was a detour.
34
- fn.invoke_url ?? '',
35
- fn.error_message ?? '',
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
- functions: 'invoke',
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
- invoke: `${(0, ui_1.bold)('xflow functions invoke')} <name>: call a function
565
+ functions: `${(0, ui_1.bold)('xflow functions')}: what is deployed, and calling it
566
566
 
567
- Calls it exactly the way the application does: the project token plus a visitor pass
568
- for the person who owns the key, so the function sees a real caller and its role.
569
- Both are taken from the platform, no local .env is needed.
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).trim();
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.33.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.33.0",
3
+ "version": "0.34.1",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -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.33.0, so the copy you are reading can be older than
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
- Three of them behave unlike the rest:
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 only to a member of the organization who has access to that project.
284
- The platform issues a short-lived pass when it opens the application, the wrapper checks it
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