@getxflow/cli 0.18.0 → 0.20.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.
@@ -229,8 +229,18 @@ async function deployments(args) {
229
229
  (0, ui_1.formatAge)(d.deployed_at ?? d.created_at),
230
230
  // Plain text only: a padded cell with ANSI codes would skew every column after it.
231
231
  d.comment && d.comment.length > 60 ? `${d.comment.slice(0, 59)}…` : (d.comment ?? ''),
232
- [d.is_dev ? (0, ui_1.bold)('dev') : '', d.is_live ? (0, ui_1.bold)('live') : ''].filter(Boolean).join(' '),
232
+ // pages_kept === false marks a build the storage cleanup has stripped:
233
+ // rollback refuses it, so saying so here saves the agent a call. Older
234
+ // platforms omit the field, and then the column stays as it was.
235
+ [
236
+ d.is_dev ? (0, ui_1.bold)('dev') : '',
237
+ d.is_live ? (0, ui_1.bold)('live') : '',
238
+ d.pages_kept === false ? 'no pages' : '',
239
+ ]
240
+ .filter(Boolean)
241
+ .join(' '),
233
242
  ]));
234
243
  (0, ui_1.note)((0, ui_1.dim)(' Serve the pages of an earlier build: xflow rollback <number>'));
244
+ (0, ui_1.note)((0, ui_1.dim)(' Pages live for the three latest builds; "no pages" marks the rest'));
235
245
  (0, ui_1.note)((0, ui_1.dim)(' Fetch the code of a build: xflow pull --revision <its revision> --into ../copy'));
236
246
  }
package/dist/help.js CHANGED
@@ -593,9 +593,10 @@ old copy.`,
593
593
  init: `${(0, ui_1.bold)('xflow init')} [dir]: a new project
594
594
 
595
595
  Lays down the platform template: React on Vite, Tailwind, a set of components
596
- (${(0, ui_1.bold)('src/components/ui')}) and ready-made blocks (${(0, ui_1.bold)('src/components/blocks')}): table,
597
- form, filters, kanban, charts. Projects created in the web get the same template, which
598
- is what makes the applications look alike.
596
+ (${(0, ui_1.bold)('src/components/ui')}) and ready-made blocks (${(0, ui_1.bold)('src/components/blocks')}): app
597
+ shell, page header, section, toolbar, list, empty state, stats, detail card, form rows,
598
+ card grid, timeline, table, charts. Projects created in the web get the same template,
599
+ which is what makes the applications look alike.
599
600
 
600
601
  Building the interface on top of these components and the tokens in ${(0, ui_1.bold)('src/index.css')}
601
602
  is not a formality: your own palette on top of them looks foreign inside the platform.
package/dist/session.js CHANGED
@@ -31,7 +31,11 @@ function connect(config, options) {
31
31
  ...(0, credentials_1.storedKeyInputs)(apiUrl),
32
32
  });
33
33
  if (!picked) {
34
- throw new errors_1.CliError(`No access key for ${apiUrl}`, 'Sign in: xflow login. In CI pass the key in the XFLOW_TOKEN variable');
34
+ throw new errors_1.CliError(`No access key for ${apiUrl}`,
35
+ // Who does it, not where the key comes from: the key is issued by the sign-in
36
+ // itself and by nothing else, so an agent reading this has to hand the step over
37
+ // instead of hunting for a value to paste.
38
+ 'Sign in: xflow login (needs a person with a browser). In CI pass the key in XFLOW_TOKEN');
35
39
  }
36
40
  if ('missing' in picked) {
37
41
  throw new errors_1.CliError(`This folder is bound to the organization ${picked.organizationId}, and there is no stored key for it`, 'Sign in to that organization: xflow login. The stored ones: xflow org');
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.18.0';
5
+ exports.CLI_VERSION = '0.20.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.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -5,15 +5,13 @@ description: Build, deploy and publish apps on the XFlow platform with the xflow
5
5
 
6
6
  # XFlow
7
7
 
8
- Hosting for web apps. The code lives in an ordinary repository on the developer
9
- machine, `xflow deploy` sends the sources, and the platform builds them in a clean
10
- sandbox and serves the result as static files. A platform project is recognized by
11
- the `xflow.json` file in its root.
8
+ Hosting for web apps. The code lives in an ordinary repository on the developer machine,
9
+ `xflow deploy` sends the sources, and the platform builds them in a clean sandbox and serves
10
+ the result as static files. A project is recognized by `xflow.json` in its root.
12
11
 
13
- No `xflow.json` means the folder is not bound to a project yet. `xflow projects list`
14
- names the projects of the organization with their ids, `xflow link <id>` binds the
15
- folder to one of them (an empty folder also gets the sources), and `xflow init` starts
16
- a new project from the platform template.
12
+ No `xflow.json` means the folder is not bound to a project yet: `xflow projects list` names the
13
+ projects of the organization with their ids, `xflow link <id>` binds the folder to one (an empty
14
+ folder also gets the sources), and `xflow init` starts a new project from the template.
17
15
 
18
16
  ## First rule
19
17
 
@@ -24,21 +22,17 @@ contains the fix.
24
22
 
25
23
  ## Keeping these instructions current
26
24
 
27
- These instructions ship with xflow CLI 0.18.0. They travel inside the package, so the copy
28
- you are reading can be older than the CLI answering your commands, and nothing about that
29
- is visible in the text itself.
25
+ These instructions ship with xflow CLI 0.20.0, so the copy you are reading can be older than
26
+ the CLI answering your commands.
30
27
 
31
28
  When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
32
- again the file it names**. Refreshing only writes new text to disk; until you re-read it you
33
- are still working from the old rules, and the command will look like it did nothing. This
34
- repair is yours to make, no need to ask anyone.
29
+ again the file it names**: refreshing only writes to disk, and until you re-read it you are
30
+ still working from the old rules. Do it yourself, no need to ask.
35
31
 
36
- Updating the CLI itself is a different matter and not yours by default. `xflow update`
37
- replaces a program shared by every project on the machine, and in most installations it
38
- cannot even do that: it prints the command for a person instead. So when a newer version is
39
- merely available, finish the task and offer the update in your summary. The one exception is
40
- a platform refusing to work with the CLI as too old: nothing runs until it is updated, so say
41
- what you are doing and run it.
32
+ Updating the CLI is not yours. `xflow update` replaces a program shared by every project on
33
+ the machine, and most installations only print the command for a person: offer the update in
34
+ your summary. The exception is a platform refusing the CLI as too old: nothing runs until it
35
+ is updated, so say what you are doing and run it.
42
36
 
43
37
  ## Hard rules
44
38
 
@@ -56,8 +50,9 @@ the error points away from the cause. The sections below carry the details.
56
50
  schedules, and a function created again later gets a different address.
57
51
  4. An already applied migration is never re-run, so editing its file changes
58
52
  nothing. A schema change is always a new file.
59
- 5. When inserts start failing with `db_write_locked`, the database is over its plan
60
- size. The fix is a migration that deletes data, never a rewrite of the failing SQL.
53
+ 5. When inserts start failing with `db_write_locked`, the database is over its plan size
54
+ and Postgres itself refuses. Reads and deletes keep working, so the fix is a migration
55
+ that deletes data, never a rewrite of the SQL. Write comes back within the hour.
61
56
  6. In file storage, call `confirm` only after the PUT has finished: the platform reads
62
57
  the real size from storage, so an early call answers that the file is not there.
63
58
  7. Heavy media does not travel with the sources. Photos, video and PDFs go up with
@@ -65,41 +60,31 @@ the error points away from the cause. The sections below carry the details.
65
60
 
66
61
  ## Plan limits
67
62
 
68
- The organization runs on a plan with finite limits: projects, databases, cloud functions,
69
- active connector connections, developer and staff seats, database and file storage,
70
- function minutes and builds per month, builds running at once, plus schedules and how
71
- often they may run. `xflow whoami` prints every one of them next to what is already
72
- used, and reading it before a long task is cheaper than hitting a wall mid-way.
73
-
74
- Two of them deserve a word. A database outlives its project (deleting a project only
75
- detaches it), so a refused database on a small plan usually means an orphan: the fix is
76
- deleting the unused database in the "Data" section of the web interface, not renaming
77
- anything. Connector connections are counted while active: switching an unused connection
78
- off in the web interface frees the seat without deleting it.
79
-
80
- The plan also caps how long one call of a cloud function may run. The cap is written
81
- into the function when it is deployed, so a plan change reaches the cloud only with the
82
- next `xflow deploy` of each project, in both directions. `xflow whoami` names the plan's
83
- cap; what a deployed function actually carries is what it was last deployed with.
84
-
85
- The same output names the rights of your own key, which is the other half of the answer:
86
- destroying data in a migration, deleting files from storage and linking a connected
87
- account each need a right that is off by default. Reading that line first turns a refusal
88
- halfway through the work into a sentence said before it starts.
89
-
90
- A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`,
91
- the API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`), and
92
- MCP tools carry the same field. Retrying the command, renaming things or rewriting the
93
- code changes nothing: tell the user what ran out and stop. Only the owner or an admin
94
- lifts it, in the web interface, by freeing the resource or moving to a bigger plan.
95
-
96
- A schedule refused for running too often is one of these too, not a bad cron string: the
97
- minimum interval comes from the plan, so widen the interval or say what the plan allows.
98
- The number is in `xflow whoami`, never guess it.
99
-
100
- On `db_write_locked` (hard rule 5) it is Postgres itself refusing, not the platform. Reads
101
- and deletes keep working, which is what makes a deleting migration the way out. Write comes
102
- back within an hour of the data going under the limit.
63
+ The organization runs on a plan with finite limits: projects, databases, functions, active
64
+ connections, seats, database and file storage, function minutes, builds per month and at
65
+ once, schedules and how often they may run. `xflow whoami` prints every one of them next to
66
+ what is used, and the rights of your own key with them: destroying data in a migration,
67
+ deleting files from storage and linking a connected account each need a right that is off by
68
+ default. Reading it before a long task turns a refusal halfway through the work into a
69
+ sentence said before it starts.
70
+
71
+ Three of them behave unlike the rest:
72
+
73
+ - A database outlives its project (deleting a project only detaches it), so a refused
74
+ database usually means an orphan: delete the unused one in the "Data" section of the web
75
+ interface.
76
+ - Connections are counted while active: switching an unused one off frees the seat without
77
+ deleting it.
78
+ - The cap on how long one function call may run is written into the function when it is
79
+ deployed, so a plan change reaches the cloud only with the next `xflow deploy` of each
80
+ project, in both directions.
81
+
82
+ A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`, the
83
+ API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`), MCP tools
84
+ carry the same field. Retrying, renaming or rewriting the code changes nothing: tell the user
85
+ what ran out and stop, only the owner or an admin lifts it. A schedule refused for running too
86
+ often is one of these too, not a bad cron string: the minimum interval comes from the plan,
87
+ never guess it.
103
88
 
104
89
  ## Workflow
105
90
 
@@ -116,16 +101,13 @@ back within an hour of the data going under the limit.
116
101
  browser for them.
117
102
  6. `xflow publish` makes that same version visible to visitors.
118
103
 
119
- The split is deliberate: shipping a build and showing it are two separate decisions.
120
- Until `publish` runs, visitors keep seeing the previous pages. The one exception is
121
- the very first version of a project: it publishes automatically, since there is no
122
- live version to protect yet.
104
+ The split is deliberate: until `publish` runs, visitors keep seeing the previous pages. The
105
+ one exception is the very first version of a project, which publishes automatically.
123
106
 
124
- `xflow status` says how the local copy differs from the server revision. When a deploy is
125
- rejected before the build starts, the server holds a version this folder has never seen:
126
- somebody deployed first, or the folder was linked to a different project along the way.
127
- Fetch the server work next to yours (`xflow pull --into ./server-copy`), merge it in git,
128
- deploy again. `--force` destroys that work, a last resort rather than a way around the error.
107
+ `xflow status` says how the local copy differs from the server revision. A deploy rejected
108
+ before the build starts means the server holds a version this folder has never seen: somebody
109
+ deployed first, or the folder was linked elsewhere. Fetch it next to yours (`xflow pull --into
110
+ ./server-copy`), merge in git, deploy again. `--force` destroys that work.
129
111
 
130
112
  **Only pages are versioned.** Cloud functions and the database are one per project: they
131
113
  are not versioned, and dev and live share them. So `xflow deploy` changes the running
@@ -133,31 +115,29 @@ application the moment it finishes, before any `publish`, and `publish` moves th
133
115
  only. Tell the user this when a deploy touches `functions/`: there is no staging step for
134
116
  server code.
135
117
 
136
- Rolling back: `xflow deployments` lists the history, `xflow rollback <id>` points the project
137
- at the pages of an earlier build. Only the pages come back, and they then talk to today's
138
- functions. The code of that build is a separate fetch (`xflow pull --revision N --into
139
- ../old-version`, kept outside the project folder); building it over the current one needs
140
- `--force` and a human typing the project name, so you cannot do it on your own.
118
+ Rolling back: `xflow deployments` lists the history, `xflow rollback <id>` points the project at
119
+ the pages of an earlier build. Only the pages come back and they talk to today's functions. The
120
+ code of that build is a separate fetch (`xflow pull --revision N --into ../old-version`, kept
121
+ outside the project folder); building it over the current one needs a human.
141
122
 
142
- The platform keeps the **last 30 successful builds** and the **last 100 revisions** (a
143
- revision a live version was built from is never deleted). That is rollback depth, not a
144
- backup: keep anything you must not lose in your own git repository.
123
+ Pages are kept for the **three latest successful builds** plus whatever dev and live show;
124
+ older versions stay in the history with `pages_kept` false, readable but not openable. The
125
+ history holds the **last 30 builds**, the sources the **last 50 revisions** (a revision a live
126
+ version was built from is never deleted). Three builds is the whole rollback depth, hours on an
127
+ active project: keep anything you must not lose in your own git repository.
145
128
 
146
129
  **The only link you give a person is the project page**, `https://app.getxflow.com/projects/<id>`,
147
- which the CLI prints for you. Refer to builds by their number ("version 481203 is built,
148
- 092399 is what visitors see"), never by address. The platform does not hand out build
149
- addresses and neither should you: a build address has the version number baked into it,
150
- and after the next publish it does not break, it keeps answering with the old copy. Anyone
151
- holding that link then stares at a frozen app and concludes the changes never shipped. The
152
- project page always shows the current state, and every version is reachable from it.
130
+ which the CLI prints for you. Refer to builds by their number ("version 481203 is built, 092399
131
+ is what visitors see"), never by address: a build address has the version baked into it and
132
+ after the next publish keeps answering with the old copy, so whoever holds it stares at a
133
+ frozen app and concludes nothing shipped. The project page always shows the current state.
153
134
 
154
135
  ## Build gate
155
136
 
156
- Before the sandbox starts, the platform checks the sources against the template. Every
157
- rule below blocks the build, and violations come back together with files and line
158
- numbers rather than one per attempt: up to fifty of them, and a count when there are
159
- more. Nothing is charged for a rejected attempt, the sandbox never starts. Write code
160
- that already satisfies these rules instead of learning them from rejections.
137
+ Before the sandbox starts, the platform checks the sources against the template. Every rule
138
+ below blocks the build, and violations come back together with files and line numbers, up to
139
+ fifty at a time. A rejected attempt costs nothing, the sandbox never starts. Write code that
140
+ satisfies these rules instead of learning them from rejections.
161
141
 
162
142
  Build setup:
163
143
 
@@ -194,9 +174,9 @@ Interface rules, checked outside `src/components/ui` and `src/components/blocks`
194
174
  `react-hot-toast`, `react-dnd`) must have it mounted in `App.tsx`. Missing providers
195
175
  build fine and give visitors a white screen.
196
176
 
197
- Template integrity. The app grows out of the platform template, and part of that template
198
- is not yours to change. The reference is a snapshot of the project itself, taken when the
199
- platform first looked at it, so these rules never argue with work that was already there:
177
+ Template integrity. Part of the platform template is not yours to change. The reference is a
178
+ snapshot of the project itself, taken when the platform first looked at it, so these rules
179
+ never argue with work that was already there:
200
180
 
201
181
  - Platform files must stay byte for byte as they arrived: `src/lib/theme-sync.ts`,
202
182
  `src/lib/platform-auth.ts`, `src/contexts/platform-auth-context.tsx`,
@@ -221,14 +201,13 @@ A rejection names the file and the revision to take the original from:
221
201
  ## Cloud functions
222
202
 
223
203
  Server-side code lives in `functions/<name>/index.ts` and exports `handler` (`index.js` and
224
- `index.mjs` are read too). The directory name is the name of the function, and the build gate
225
- is strict about it: lowercase latin letters, digits and hyphens, starting with a letter or a
226
- digit, up to 41 characters. `send-mail` passes, `sendMail` and `send_mail` do not, and the
227
- build stops before the sandbox rather than halfway through it.
204
+ `index.mjs` are read too). The directory name is the name of the function, and the build gate is
205
+ strict about it: lowercase latin letters, digits and hyphens, starting with a letter or a digit,
206
+ up to 41 characters. `send-mail` passes, `sendMail` and `send_mail` do not.
228
207
 
229
208
  There is no separate deploy command: `xflow deploy` ships the functions and then builds the
230
- application, in that order. List what is live with `xflow functions list`. The handler
231
- returns `{ statusCode, body }` where `body` is a JSON string.
209
+ application, in that order. `xflow functions list` shows what is live. The handler returns
210
+ `{ statusCode, body }` where `body` is a JSON string.
232
211
 
233
212
  Keep one shape inside that string across the whole project: `{ success: true, data }`
234
213
  when it worked, `{ success: false, error: { message, code } }` when it did not. Nothing
@@ -236,44 +215,32 @@ enforces this, but a project where every function answers its own way costs an a
236
215
  on every call. Branch the frontend on `error.code`, never on `error.message`: wording
237
216
  gets rewritten on any edit, a code does not.
238
217
 
239
- How a non-2xx answer reaches the page depends on the generation of `src/lib/xflow.ts`
240
- in the project, so look at that file before writing the catch: a copy that declares
241
- `XFlowError` throws it carrying `code` and `status`, and a copy that says nothing about
242
- errors is older and throws a plain `Error` whose message is all there is. In an older
243
- project, a function that wants its error text seen on the page answers `200` with
244
- `{ success: false, error }` in the body.
245
-
246
- The sources are the whole truth about which functions exist. Delete the directory and the
247
- next deploy would delete the function from the cloud, schedules included, and that cannot be
248
- undone: a function created again later gets a different address. So never remove a function
249
- directory to "clean up" unless the user asked for the function to go.
250
-
251
- Such a deploy does not start on its own: the platform names the functions it would remove and
252
- refuses until somebody agrees. Under an agent there is no terminal to ask in, so the refusal
253
- reaches you, and `--allow-removals` is the only way past it. Adding that flag to get the
254
- build running is exactly the wrong move: it means you deleted something the user did not ask
255
- you to delete. Put the directories back instead, and if the removal really is intended, say
256
- which functions are about to go and let the user answer.
257
-
258
- Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
259
- the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body,
260
- `--data-file body.json` sends the same from a file, `--json` prints the body alone), and
261
- `xflow functions logs <name>` shows the failures, each with its stack and the console
262
- output of that call. Only failed calls are
263
- logged, so an empty output means the function never crashed, not that logging is broken.
264
-
265
- The body has to be JSON and the CLI checks it before calling. In PowerShell the double
266
- quotes never reach the CLI: `--data '{"a":1}'` arrives as `{a:1}`, so escape them
267
- (`--data '{\"a\":1}'`) or pass a file. This is a shell habit, not a bug in the function, and
268
- it bites `--payload` of a schedule the same way. A command that worked writes nothing to
269
- stderr, so a red line in PowerShell means it really failed; `--json` prints the payload and
270
- nothing else.
218
+ How a non-2xx answer reaches the page depends on the generation of `src/lib/xflow.ts`, so read
219
+ that file before writing the catch: a copy declaring `XFlowError` throws it with `code` and
220
+ `status`, an older copy throws a plain `Error` whose message is all there is. In an older
221
+ project, a function whose error text must be seen answers `200` with `{ success: false, error }`.
222
+
223
+ The sources are the whole truth about which functions exist: a deleted directory takes the
224
+ function and its schedules out of the cloud, undoably, and a function created again later gets
225
+ a different address. Such a deploy refuses until somebody agrees, and `--allow-removals` is the
226
+ only way past it. Reaching for that flag to get a build running means you deleted something the
227
+ user never asked you to delete: put the directories back. If the removal is intended, name the
228
+ functions about to go and let the user answer.
229
+
230
+ Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it the way
231
+ the app does and prints status, timing and body (`--data '{"a":1}'`, `--data-file body.json`,
232
+ `--json` for the body alone), and `xflow functions logs <name>` shows the failures with stacks
233
+ and console output. Only failed calls are logged, so empty output means it never crashed.
234
+
235
+ The body has to be JSON and the CLI checks it before calling. In PowerShell the double quotes
236
+ never reach the CLI (`--data '{"a":1}'` arrives as `{a:1}`): escape them (`--data '{\"a\":1}'`)
237
+ or pass a file. It bites `--payload` of a schedule the same way. A command that worked writes
238
+ nothing to stderr, so a red line there means it really failed.
271
239
 
272
240
  From the app, call a function through `src/lib/xflow.ts`:
273
- `await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the credentials
274
- for you. Addresses are baked into the build, which is why the functions go out first:
275
- by the time the bundle is built they already exist, and a new function is never missing
276
- from the application that calls it.
241
+ `await xflow.functions.invoke('send-mail', { body: { to } })`, which carries the credentials for
242
+ you. Addresses are baked into the build, which is why functions go out first: by the time the
243
+ bundle is built they already exist.
277
244
 
278
245
  ### Who is calling
279
246
 
@@ -314,13 +281,12 @@ and `X-Server-Key` with `process.env.XFLOW_SERVER_KEY`. The token is the ticket
314
281
  project and the key is the identity; the wrapper checks the ticket first, so the key alone
315
282
  answers 401.
316
283
 
317
- An outside service (a webhook from a payment provider, a bot, a CRM) has no person behind it
318
- and needs a key of that one function. Keys are not issued by default and the CLI cannot
319
- create one: a human issues it in the project settings: «Облачные функции» → the function →
320
- «Настройки». Ask the user to do that and to paste the address back to you — never invent
321
- another way in. Send it in the `X-Function-Key` header. A function holds at most two keys,
322
- and the second one exists to replace the first without downtime, not to serve a second
323
- consumer.
284
+ An outside service (a payment webhook, a bot, a CRM) has no person behind it and needs a key of
285
+ that one function. Keys are not issued by default and the CLI cannot create one: a human issues
286
+ it in the project settings, «Облачные функции» → the function → «Настройки». Ask the user to do
287
+ that and to paste the address back — never invent another way in. Send it in the
288
+ `X-Function-Key` header. A function holds at most two keys, the second one to replace the first
289
+ without downtime, not to serve a second consumer.
324
290
 
325
291
  `xflow functions list` shows who can reach each function: `in-app only` (no keys, answers
326
292
  only inside the application) or `external (N keys)` (a human issued external access). Key
@@ -329,33 +295,29 @@ values are never shown there.
329
295
  ## Secrets and connected accounts
330
296
 
331
297
  Keys and passwords live on the platform, not in the repository: `xflow env set SMTP_PASSWORD=…`
332
- writes one, `xflow env` lists the names, `xflow env check` tells you which variables your
333
- functions read but the platform does not have. Values never come back out the only place
334
- they exist is inside the running function.
298
+ writes one, `xflow env` lists the names, `xflow env check` names the variables your functions
299
+ read but the platform does not have. Values never come back out: the only place they exist is
300
+ inside the running function.
335
301
 
336
302
  A variable goes to the whole organization unless you say otherwise: `--scope project` keeps it
337
303
  to this project alone, `--scope organization` is the default, and a third word is refused
338
304
  rather than read as the default. Pick the narrow one for anything that belongs to one
339
305
  application.
340
306
 
341
- These commands see only what this project can see: variables shared across the organization
342
- and the ones bound to this project. A variable bound to another project is invisible here, so
343
- `env rm` reports it as missing even though names are unique within the organization.
344
-
345
- Why hard rules 1 and 2 are hard rules: a function receives only the variables it mentions by
346
- name, and the platform reads those names out of the text of the code, so an opaque read ships
347
- a function whose secret silently never arrives. The build stops on one instead. And a value
348
- written today reaches the running function on the next `xflow deploy`, not before.
349
-
350
- Some variables come from a connected account instead of from you. When someone connects an
351
- advertising cabinet or another external service in the platform settings and links it to the
352
- project, its credentials show up in `xflow env` marked read-only, under a prefix chosen at
353
- link time: `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_CLIENT_LOGIN`. Read them like any other
354
- variable. Do not try to `xflow env set` those names: the platform keeps the values in sync
355
- and refuses. If a call to that service starts failing with an authorisation error, run
356
- `xflow status`: it says whether the token is merely expiring (any build renews it) or the
357
- account was disconnected on the provider's side, which only a human can fix by reconnecting
358
- it in the platform settings.
307
+ These commands see only what this project can see: organization-wide variables and the ones
308
+ bound to this project. One bound to another project is invisible here, so `env rm` reports it
309
+ missing even though names are unique within the organization.
310
+
311
+ Hence hard rules 1 and 2: a function receives only the variables it mentions by name, and the
312
+ platform reads those names out of the text of the code, so an opaque read would ship a function
313
+ whose secret never arrives. A value written today rides the next `xflow deploy`.
314
+
315
+ Some variables come from a connected account instead of from you. Credentials of an account
316
+ linked to the project show up in `xflow env` marked read-only, under a prefix chosen at link
317
+ time (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_CLIENT_LOGIN`): read them like any other variable,
318
+ `xflow env set` on those names is refused. If calls to that service start failing on
319
+ authorisation, `xflow status` says whether the token is merely expiring (any build renews it)
320
+ or the account was disconnected on the provider's side, which only a human can reconnect.
359
321
 
360
322
  `xflow connections` lists those accounts: the ones already linked to this project, with the
361
323
  alias and the state of the access, and the ones the organization has but this project does
@@ -369,11 +331,10 @@ while a function still reads one of the variables, naming those functions, so re
369
331
  list before reaching for `--force`. Under `--project` there is no folder to read: it
370
332
  prints the variables that go and removes the link.
371
333
 
372
- Linking needs the `connections:link` right on the key, and only accounts granted to the
373
- owner of the key personally can be linked at all; the same right covers calling an unlinked
374
- account. If the right was taken away, say so and
375
- ask the person to turn it back on where the key permissions live in the platform settings: a key cannot
376
- grant it to itself. Connecting a new account and switching one off stay with a person too.
334
+ Linking needs the `connections:link` right on the key, and only accounts granted to the owner
335
+ of the key personally can be linked at all; the same right covers calling an unlinked account.
336
+ A key cannot grant it to itself, so ask the person to turn it back on where the key permissions
337
+ live. Connecting a new account and switching one off stay with a person too.
377
338
 
378
339
  ### Asking an API what it returns
379
340
 
@@ -402,10 +363,9 @@ linked or not: an invented name is left in the text as you typed it and the requ
402
363
  anyway, into somebody's live account. `xflow env` is not that list. Only the hosts of that
403
364
  connector are reachable and a refusal lists them.
404
365
 
405
- **Ask for the shape, not the volume.** One day and `limit=1` is enough to learn what the
406
- fields are called; the answer is printed head first and says when there is more. The whole
407
- dataset belongs in a cloud function, which is where you were going anyway. `--json | jq
408
- '.response.body | keys'` gives the field names without the body entering your context at all.
366
+ **Ask for the shape, not the volume.** One day and `limit=1` is enough to learn what the fields
367
+ are called, and the whole dataset belongs in a cloud function anyway. `--json | jq
368
+ '.response.body | keys'` gives the field names without the body entering your context.
409
369
 
410
370
  The call goes out for real, against a live account: `POST` and `DELETE` change things there,
411
371
  and nothing undoes that. Read the method of the endpoint before calling it, and ask the
@@ -453,23 +413,19 @@ You never write that variable yourself: `xflow env set DATABASE_URL=...` is refu
453
413
  fills those in itself, and your value under one of them would shadow the real one.
454
414
 
455
415
  The platform keeps no database history and no backups, so anything that destroys data
456
- (`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) needs two
457
- things at once: the `--allow-destructive` flag, and the right to destroy data on the key.
458
- That right is off by default and only its owner turns it on, where the key permissions live in
459
- the platform settings. When the refusal is about the right rather than the flag, adding the flag
460
- changes nothing: say what needs deleting and why, and let the person decide. With both in
461
- place the affected tables are dumped first and kept for 7 days. Check with `--dry-run` first.
462
-
463
- One logical database can be shared by several projects, so your migration can break an app
464
- you do not see, and `xflow db status` lists applied migrations that have no file in your
465
- repository: that is somebody else's project. `xflow db list` names the databases of the
466
- organization and the projects on each, which is where you find out who else is on yours.
467
- For the same reason `migrations/` is not the
468
- schema. It says what you did; `xflow db schema [table]` says what is in the database right
469
- now, and `xflow db query "select ..."` reads it inside a READ ONLY transaction (50 rows by
470
- default, `--limit` raises that to 300). The table cuts long values to keep the columns
471
- readable; `--json` prints them whole. Look before
472
- you write a migration against a shared database.
416
+ (`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) needs both the
417
+ `--allow-destructive` flag and the right to destroy data on the key, which is off by default and
418
+ only its owner turns on. When the refusal is about the right, the flag changes nothing: say what
419
+ needs deleting and why, and let the person decide. With both in place the affected tables are
420
+ dumped first and kept for 7 days. Check with `--dry-run`.
421
+
422
+ One logical database can be shared by several projects, so your migration can break an app you
423
+ do not see: `xflow db status` lists applied migrations with no file in your repository (that is
424
+ somebody else's project), and `xflow db list` names who else is on yours. For the same reason
425
+ `migrations/` is not the schema. It says what you did; `xflow db schema [table]` says what is in
426
+ the database right now, and `xflow db query "select ..."` reads it inside a READ ONLY
427
+ transaction (50 rows by default, `--limit` up to 300; `--json` prints long values whole). Look
428
+ before you write a migration against a shared database.
473
429
 
474
430
  ## File storage
475
431
 
@@ -485,29 +441,28 @@ xflow storage ls --json # the same list, machine-readable
485
441
  ```
486
442
 
487
443
  The caps that send them here (10 MB of sources, 15 MB unpacked) are about heavy media, not
488
- about code: icons, fonts and small artwork the build needs stay in the repository as usual.
489
- Everything heavy goes to storage, where it is metered against the organization plan and is
490
- not re-uploaded on every deploy.
444
+ about code: icons, fonts and small artwork stay in the repository as usual. Everything heavy
445
+ goes to storage, metered against the organization plan and not re-uploaded on every deploy.
491
446
 
492
- `push` prints the address of every file and skips what is already stored under the same path
493
- and size, so a repeat run after a broken connection is cheap; `--replace` overwrites a file
494
- whose size differs. An address belongs to the record rather than to the bytes, so a
495
- replacement keeps it and the links in your code and tables keep working. Put those addresses
496
- into the code or into a table: there is no command that fetches files back to a machine.
447
+ `push` prints the address of every file and skips what is already stored under the same path and
448
+ size, so a repeat run after a broken connection is cheap; `--replace` overwrites a file whose
449
+ size differs. An address belongs to the record rather than to the bytes, so a replacement keeps
450
+ it and existing links keep working. Put those addresses into the code or into a table: nothing
451
+ fetches files back to a machine.
497
452
 
498
453
  Deleting needs a right of its own, off by default (**Delete files**, where the key permissions
499
- live in the platform settings), so `xflow storage rm` may answer that the key was not granted it.
500
- Ask the person to switch it on, and do not look for a way around: whatever the users of the
501
- application uploaded lives in the same folders, and there is no undo. A folder first tells
502
- you how many files it holds and deletes them only with `--yes`.
454
+ live), so `xflow storage rm` may answer that the key was not granted it. Ask the person to switch
455
+ it on and do not look for a way around: whatever the users of the application uploaded lives in
456
+ the same folders, and there is no undo. A folder first says how many files it holds and deletes
457
+ them only with `--yes`.
503
458
 
504
459
  **Files that users of the application upload** go through a cloud function of your own. The
505
- browser cannot reach storage directly: those endpoints take only the server key of the
506
- project, and the platform puts it into the environment of your cloud functions as
507
- `XFLOW_SERVER_KEY`. Nothing else holds it: not the bundle, not `.env`, not `xflow env`.
460
+ browser cannot reach storage directly: those endpoints take only the server key of the project,
461
+ which the platform puts into the environment of your functions as `XFLOW_SERVER_KEY` and
462
+ nothing else holds not the bundle, not `.env`, not `xflow env`.
508
463
 
509
- So uploading is a function of your own. It asks the platform for a one-time link, the browser
510
- then sends the bytes straight to storage, and a second call records the file:
464
+ So uploading is a function of your own: it asks the platform for a one-time link, the browser
465
+ sends the bytes straight to storage, and a second call records the file:
511
466
 
512
467
  ```js
513
468
  const link = await fetch(`${process.env.XFLOW_API_URL}/api/storage/project/upload-url`, {
@@ -523,14 +478,12 @@ const link = await fetch(`${process.env.XFLOW_API_URL}/api/storage/project/uploa
523
478
  `confirm` takes the returned `s3Key` and answers with the file and its address; `delete`
524
479
  takes that same `url`. Never pipe the bytes through the function itself.
525
480
 
526
- There is no endpoint that lists files back to a function, so the address that `confirm`
527
- returns is the only copy this door gives you: write it into a table of your own in the same
528
- call, and the application reads its files from there.
481
+ No endpoint lists files back to a function, so the address `confirm` returns is the only copy
482
+ this door gives you: write it into a table of your own in the same call.
529
483
 
530
- That address never expires and is safe to store, but it is not a public link. It opens only
531
- for a visitor who is signed in and has access to this project, the same rule that guards the
532
- application itself, so it works on your pages and does nothing in an email or on a page
533
- anyone can open.
484
+ That address never expires and is safe to store, but it is not public: it opens only for a
485
+ signed-in visitor with access to this project, the same rule that guards the application itself,
486
+ so it works on your pages and does nothing in an email.
534
487
 
535
488
  Four things bite an upload that otherwise looks right:
536
489
 
@@ -555,28 +508,27 @@ A refusal comes back as `{ error, code }`. Branch on `code` (`invalid_name`, `fi
555
508
  `quota_exceeded`, `not_uploaded`, `duplicate_name`, `forbidden_type`, `not_found`, …) and never on the text:
556
509
  the wording is free to change, the code is not.
557
510
 
558
- What the app may do with files is decided inside that function, because the page in the
559
- browser can be edited by whoever opened it. Never write the key into the sources and never
560
- send it to the frontend: the build gate stops on a key found in the application code, and a
561
- key that reached a visitor lets them delete every file of the project.
511
+ What the app may do with files is decided inside that function, because the page can be edited
512
+ by whoever opened it. Never send the key to the frontend: the build gate stops on a key found in
513
+ the sources, and a key that reached a visitor deletes every file of the project.
562
514
 
563
515
  ## Organizations and keys
564
516
 
565
- One key per organization, stored side by side rather than replacing each other:
566
- `xflow org` lists them, `xflow org switch <name|id>` changes the active one, and
567
- `xflow help org` has the precedence rules. Two things are worth knowing without asking.
517
+ One key per organization, stored side by side rather than replacing each other: `xflow org`
518
+ lists them, `xflow org switch <name|id>` changes the active one, `xflow help org` has the
519
+ precedence rules.
568
520
 
569
- Inside a project folder there is nothing to switch: commands follow the organization the
570
- folder is bound to, whatever the active one is. That is what lets two projects of two
571
- organizations work side by side. The exception is `--project <id>`, which every command
572
- that can answer without the working copy takes (`xflow help project` lists them): it ignores
573
- the folder whole, so a second project needs no second checkout. `projects get` and `link`
574
- take no flag and name the project by its id as an argument instead.
521
+ Inside a project folder there is nothing to switch: commands follow the organization the folder
522
+ is bound to, whatever the active one is, which is what lets projects of two organizations work
523
+ side by side. The exception is `--project <id>`, taken by every command that can answer without
524
+ the working copy (`xflow help project` lists them): it ignores the folder whole, so a second
525
+ project needs no second checkout. `projects get` and `link` name the project by id as an
526
+ argument instead.
575
527
 
576
528
  A "not found" on a project you know exists usually means the key belongs to another
577
- organization, not that the project is gone: ids are unique across the platform, so a
578
- wrong key can never reach somebody else's project. Check `xflow org`. Signing in to a
579
- missing organization is `xflow login`, and that needs a person with a browser.
529
+ organization: ids are unique across the platform, so a wrong key can never reach somebody
530
+ else's project. Check `xflow org`; signing in to a missing organization is `xflow login`, and
531
+ that needs a person with a browser.
580
532
 
581
533
  ## Direct access without the terminal
582
534
 
@@ -601,18 +553,28 @@ through tool calls also burns the user's tokens for nothing.
601
553
 
602
554
  ## App design
603
555
 
604
- The platform design system already ships inside the project, and the app is supposed
605
- to look like a part of the platform:
556
+ The platform design system ships inside the project, and the app should look like a part
557
+ of the platform:
558
+
559
+ - `src/components/ui` — primitives: buttons, inputs, dialogs, tables, menus
560
+ - `src/components/blocks` — composed blocks: app shell, page header, section, toolbar with
561
+ filters, list, empty state, stats, detail card, form rows, card grid, timeline, table,
562
+ charts. Props are typed next to each block
563
+ - `src/index.css` — colour tokens
564
+
565
+ Take a block before writing your own layout, and colours from tokens (`bg-card`,
566
+ `text-muted-foreground`, `bg-success-soft`): a custom hex palette makes the app look
567
+ foreign inside the platform.
606
568
 
607
- - `src/components/ui` for primitives: buttons, inputs, dialogs, tables, menus
608
- - `src/components/blocks` for composed blocks: `data-table` and `charts`
609
- - `src/index.css` for color tokens
569
+ Screens that read as a product, not as a form:
610
570
 
611
- Before writing your own component, check whether a block already covers it: props are
612
- typed next to each block, larger ones keep a separate `types.ts`. Take colors from
613
- tokens (`bg-card`, `text-muted-foreground`, `bg-success-soft` and the like). A custom
614
- hex palette makes the app look foreign inside the platform, which is the whole reason
615
- the design system sits in the project.
571
+ - Every screen starts with `Page` and `PageHeader`; header, filters and content in one column.
572
+ - Draw all four states: loading, error, empty, data. An empty screen says what appears here and offers an action.
573
+ - Colour only when it communicates: status, action, error, success. Grey carries the structure.
574
+ - Density over air: people scan and compare. Numbers in a column, `tabular-nums`.
575
+ - Name the shape first: list, list with detail, dashboard, form, board.
576
+ - Seed 5-10 believable rows in the first migration: a screen with zero rows looks unfinished.
577
+ - Squint at the result: if the hierarchy blurs away, it is decoration, not structure.
616
578
 
617
579
  The app runs inside the platform in an iframe and receives the theme and the current
618
580
  user from it. The `usePlatformAuth()` hook gives the name, role, permissions and the