@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.
@@ -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) {
@@ -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 will not be applied again: write a new migration');
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) {
@@ -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
  };
@@ -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: the comparison is by name, not by
358
- content. ${(0, ui_1.bold)('xflow db status')} lists such a file separately, so write a new migration
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
- 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
565
566
 
566
- Calls it exactly the way the application does: the project token plus a visitor pass
567
- for the person who owns the key, so the function sees a real caller and its role.
568
- 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.
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).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.32.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
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.32.0, so the copy you are reading can be older than
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, so editing its file changes
52
- nothing. A schema change is always a new file.
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 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
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, so an already applied file is never re-run and
454
- editing it changes nothing: write a new migration instead.
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