@getxflow/cli 1.2.2 → 1.3.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/help.js CHANGED
@@ -32,741 +32,753 @@ function help(topic) {
32
32
  // Falling through to the full help here is what this used to do, and it reads as an
33
33
  // answer: the agent asked about one command and got the whole screen back, with no
34
34
  // sign that the page it asked for does not exist.
35
- (0, ui_1.out)(`No page for "${topic}". Pages: ${Object.keys(TOPICS).sort().join(', ')}
35
+ (0, ui_1.out)(`No page for "${topic}". Pages: ${Object.keys(TOPICS).sort().join(', ')}
36
36
  Every command with a line of its own: xflow help`);
37
37
  return;
38
38
  }
39
- (0, ui_1.out)(`${(0, ui_1.bold)('xflow')} ${(0, ui_1.dim)(version_1.CLI_VERSION)} XFlow application hosting
40
-
41
- A project is a box: one database, cloud functions, secrets. Its apps live in apps/<folder>,
42
- each with its own versions and address. Commands follow the folder you stand in.
43
-
44
- ${(0, ui_1.bold)('Getting started')}
45
- xflow login sign in through the browser
46
- xflow init [dir] new project with its first app from the platform template
47
- xflow apps add <folder> another app of this project, in apps/<folder>
48
- xflow link <id> link this folder to a project, an empty one gets the whole project
49
- xflow dev in an app folder: run it on 127.0.0.1:3000, functions included
50
- xflow skills pick the agents that get the platform instructions
51
-
52
- ${(0, ui_1.bold)('Sources')}
53
- xflow status what is on the server and how the local copies differ
54
- xflow pull [functions|apps/<folder>] [--into dir] [--revision N]
55
- fetch the sources of the root or of one app
56
-
57
- ${(0, ui_1.bold)('Files')}
58
- xflow storage ls [folder] what the file storage of the project holds
59
- xflow storage push <dir> [--to folder] [--replace]
60
- upload heavy static assets: photos, video, PDFs
61
- xflow storage rm <address> delete one file
62
- xflow storage rm --folder <path> --yes
63
- delete a folder with everything in it
64
-
65
- ${(0, ui_1.bold)('Releasing')}
66
- xflow deploy functions -m "what changed"
67
- send the project root, ship the functions and their callers
68
- xflow deploy apps/<folder> -m "what changed"
69
- send one app and build it (a bare deploy inside its folder)
70
- xflow publish [build] point the app address at the newest build, or a named one
71
- xflow rollback <build> the same with an older build
72
- xflow deployments [functions] builds of the app, or deploys of the functions
73
-
74
- ${(0, ui_1.bold)('Functions')}
75
- xflow functions list what is deployed, its callers, open or not, last call
76
- xflow functions invoke <name> [--data '{"a":1}']
77
- call a function and print the answer
78
- xflow functions logs [name] function crashes: stack and console output
79
- xflow schedules list what runs on a timer
80
- xflow schedules set <name> "0 3 ? * * *"
81
- run a function on a schedule (UTC)
82
- xflow schedules rm <name> take a schedule off
83
- xflow env [check] function variables: what is stored, what is missing
84
- xflow env set NAME=value store a variable
85
- xflow env rm NAME delete a variable
86
- xflow connections connected accounts the project can use, and their variables
87
- xflow connections link <name> --as ALIAS
88
- give the functions the credentials of an account
89
- xflow connections unlink <ALIAS> [--yes]
90
- take them away again
91
-
92
- ${(0, ui_1.bold)('Database')}
93
- xflow db status which migrations are applied and which are waiting
94
- xflow db list databases of the projects and the servers of the organization
95
- xflow db schema [table] tables of the project, or the columns of one
96
- xflow db query 'select ...' read data, in a read-only transaction
97
- xflow db migrate [--dry-run] [--allow-destructive]
98
- apply the migrations from migrations/*.sql
99
- xflow db move --to <server> --yes
100
- move the project database to another server
101
-
102
- ${(0, ui_1.bold)('Pages')}
103
- xflow pages public sites of the organization at <name>.xflowpage.ru
104
- xflow pages create <title> <folder|.zip|.html> [--slug name]
105
- a new page with the site on it, live at once
106
- xflow pages deploy <page> <folder|.zip|.html> [-m "what changed"]
107
- upload the site: live at once, there is no publish step
108
- xflow pages status <page> the page and its versions
109
- xflow pages rollback <page> <N> put an earlier version back on the address
110
- xflow pages rm <page> --yes delete the page for good: the address empties
111
-
112
- ${(0, ui_1.bold)('Reference')}
113
- xflow logs [--limit N] browser errors of the published apps
114
- xflow projects list projects of the organization
115
- xflow projects get [id] project card: apps, database, functions
116
- xflow apps the apps of this project and their folders
117
- xflow org organizations with a stored key, the active one marked
118
- xflow org members who is in the organization, and with which role
119
- xflow org switch <name|id> make another organization the active one
120
- xflow whoami whose key this is and what it can do
121
- xflow logout [--all] forget the key of the active organization (--all: every one)
122
- xflow update update the CLI itself, and the skill that ships with it
123
- xflow --version which version is installed
124
-
125
- ${(0, ui_1.bold)('Outside a project folder')}
126
- --project <id> name the project by id, for every command that can
127
- answer without the local files (xflow help project)
128
- --app <id> name an app by id: deploy --revision, publish, deployments, logs
129
-
130
- ${(0, ui_1.bold)('Environment')}
131
- XFLOW_TOKEN access key (for CI, instead of xflow login)
132
- XFLOW_API_URL platform address when it is not app.getxflow.com
133
-
134
- More about one command: xflow help <command>
39
+ (0, ui_1.out)(`${(0, ui_1.bold)('xflow')} ${(0, ui_1.dim)(version_1.CLI_VERSION)} XFlow application hosting
40
+
41
+ A project is a box: one database, cloud functions, secrets. Its apps live in apps/<folder>,
42
+ each with its own versions and address. Commands follow the folder you stand in.
43
+
44
+ ${(0, ui_1.bold)('Getting started')}
45
+ xflow login sign in through the browser
46
+ xflow init [dir] new project with its first app from the platform template
47
+ xflow apps add <folder> another app of this project, in apps/<folder>
48
+ xflow link <id> link this folder to a project, an empty one gets the whole project
49
+ xflow dev in an app folder: run it on 127.0.0.1:3000, functions included
50
+ xflow skills pick the agents that get the platform instructions
51
+
52
+ ${(0, ui_1.bold)('Sources')}
53
+ xflow status what is on the server and how the local copies differ
54
+ xflow pull [functions|apps/<folder>] [--into dir] [--revision N]
55
+ fetch the sources of the root or of one app
56
+
57
+ ${(0, ui_1.bold)('Files')}
58
+ xflow storage ls [folder] what the file storage of the project holds
59
+ xflow storage push <dir> [--to folder] [--replace]
60
+ upload heavy static assets: photos, video, PDFs
61
+ xflow storage rm <address> delete one file
62
+ xflow storage rm --folder <path> --yes
63
+ delete a folder with everything in it
64
+
65
+ ${(0, ui_1.bold)('Releasing')}
66
+ xflow deploy functions -m "what changed"
67
+ send the project root, ship the functions and their callers
68
+ xflow deploy apps/<folder> -m "what changed"
69
+ send one app and build it (a bare deploy inside its folder)
70
+ xflow publish [build] point the app address at the newest build, or a named one
71
+ xflow rollback <build> the same with an older build
72
+ xflow deployments [functions] builds of the app, or deploys of the functions
73
+
74
+ ${(0, ui_1.bold)('Functions')}
75
+ xflow functions list what is deployed, its callers, open or not, last call
76
+ xflow functions invoke <name> [--data '{"a":1}']
77
+ call a function and print the answer
78
+ xflow functions logs [name] function crashes: stack and console output
79
+ xflow schedules list what runs on a timer
80
+ xflow schedules set <name> "0 3 ? * * *"
81
+ run a function on a schedule (UTC)
82
+ xflow schedules rm <name> take a schedule off
83
+ xflow env [check] function variables: what is stored, what is missing
84
+ xflow env set NAME=value store a variable
85
+ xflow env rm NAME delete a variable
86
+ xflow connections connected accounts the project can use, and their variables
87
+ xflow connections link <name> --as ALIAS
88
+ give the functions the credentials of an account
89
+ xflow connections unlink <ALIAS> [--yes]
90
+ take them away again
91
+
92
+ ${(0, ui_1.bold)('Database')}
93
+ xflow db status which migrations are applied and which are waiting
94
+ xflow db list databases of the projects and the servers of the organization
95
+ xflow db schema [table] tables of the project, or the columns of one
96
+ xflow db query 'select ...' read data, in a read-only transaction
97
+ xflow db exec 'update ...' a one-off data change, outside the migration history
98
+ xflow db migrate [--dry-run] [--allow-destructive]
99
+ apply the migrations from migrations/*.sql
100
+ xflow db move --to <server> --yes
101
+ move the project database to another server
102
+
103
+ ${(0, ui_1.bold)('Pages')}
104
+ xflow pages public sites of the organization at <name>.xflowpage.ru
105
+ xflow pages create <title> <folder|.zip|.html> [--slug name]
106
+ a new page with the site on it, live at once
107
+ xflow pages deploy <page> <folder|.zip|.html> [-m "what changed"]
108
+ upload the site: live at once, there is no publish step
109
+ xflow pages status <page> the page and its versions
110
+ xflow pages rollback <page> <N> put an earlier version back on the address
111
+ xflow pages rm <page> --yes delete the page for good: the address empties
112
+
113
+ ${(0, ui_1.bold)('Reference')}
114
+ xflow logs [--limit N] browser errors of the published apps
115
+ xflow projects list projects of the organization
116
+ xflow projects get [id] project card: apps, database, functions
117
+ xflow apps the apps of this project and their folders
118
+ xflow org organizations with a stored key, the active one marked
119
+ xflow org members who is in the organization, and with which role
120
+ xflow org switch <name|id> make another organization the active one
121
+ xflow whoami whose key this is and what it can do
122
+ xflow logout [--all] forget the key of the active organization (--all: every one)
123
+ xflow update update the CLI itself, and the skill that ships with it
124
+ xflow --version which version is installed
125
+
126
+ ${(0, ui_1.bold)('Outside a project folder')}
127
+ --project <id> name the project by id, for every command that can
128
+ answer without the local files (xflow help project)
129
+ --app <id> name an app by id: deploy --revision, publish, deployments, logs
130
+
131
+ ${(0, ui_1.bold)('Environment')}
132
+ XFLOW_TOKEN access key (for CI, instead of xflow login)
133
+ XFLOW_API_URL platform address when it is not app.getxflow.com
134
+
135
+ More about one command: xflow help <command>
135
136
  Documentation: https://docs.getxflow.com (for agents: https://docs.getxflow.com/llms.txt)`);
136
137
  }
137
138
  const TOPICS = {
138
- org: `${(0, ui_1.bold)('xflow org')}: several organizations, one terminal
139
-
140
- One key per organization: every ${(0, ui_1.bold)('xflow login')} stores the key of the organization
141
- chosen in the browser next to the ones already stored, it does not replace them.
142
- Which key a command then uses, in order:
143
-
144
- 1. XFLOW_TOKEN, when set (and only for the default platform address)
145
- 2. the organization this folder is bound to (${(0, ui_1.bold)('.xflow/state.json')}, written by
146
- init, link and the first successful deploy or pull)
147
- 3. the active organization
148
-
149
- xflow org the stored organizations, the active one marked
150
- xflow org members who is in this one, and with which role
151
- xflow org switch <name|id> make another one active, no browser involved
152
- xflow logout forget the key of the active organization
153
- xflow logout --all forget every key of this platform address
154
-
155
- ${(0, ui_1.bold)('members')} reads only. Inviting people, changing a role and removing somebody stay
156
- in the platform interface, with the owner: there is no command for any of them.
157
-
158
- The switch is local: it changes which stored key is used and nothing happens on
159
- the platform. A folder bound to an organization is not affected, its commands
160
- stay in its own organization: two projects of two organizations in two terminals
161
- work without switching anything.
162
-
163
- A project id is unique across the whole platform, so "project not found" under
164
- the wrong organization can never touch somebody else's project. When that error
165
- names a project you know exists, the key is simply from another organization:
139
+ org: `${(0, ui_1.bold)('xflow org')}: several organizations, one terminal
140
+
141
+ One key per organization: every ${(0, ui_1.bold)('xflow login')} stores the key of the organization
142
+ chosen in the browser next to the ones already stored, it does not replace them.
143
+ Which key a command then uses, in order:
144
+
145
+ 1. XFLOW_TOKEN, when set (and only for the default platform address)
146
+ 2. the organization this folder is bound to (${(0, ui_1.bold)('.xflow/state.json')}, written by
147
+ init, link and the first successful deploy or pull)
148
+ 3. the active organization
149
+
150
+ xflow org the stored organizations, the active one marked
151
+ xflow org members who is in this one, and with which role
152
+ xflow org switch <name|id> make another one active, no browser involved
153
+ xflow logout forget the key of the active organization
154
+ xflow logout --all forget every key of this platform address
155
+
156
+ ${(0, ui_1.bold)('members')} reads only. Inviting people, changing a role and removing somebody stay
157
+ in the platform interface, with the owner: there is no command for any of them.
158
+
159
+ The switch is local: it changes which stored key is used and nothing happens on
160
+ the platform. A folder bound to an organization is not affected, its commands
161
+ stay in its own organization: two projects of two organizations in two terminals
162
+ work without switching anything.
163
+
164
+ A project id is unique across the whole platform, so "project not found" under
165
+ the wrong organization can never touch somebody else's project. When that error
166
+ names a project you know exists, the key is simply from another organization:
166
167
  check ${(0, ui_1.bold)('xflow org')}.`,
167
- project: `${(0, ui_1.bold)('--project <id>')}: a project without its folder
168
-
169
- Every command that only talks to the platform takes it, so a service project needs
170
- no working copy of its own:
171
-
172
- xflow db query --project 3f2a… 'select id, title from tasks where done = false'
173
- xflow functions invoke kanban --project 3f2a… --data-file card.json
174
-
175
- Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('db move')}, ${(0, ui_1.bold)('functions list')},
176
- ${(0, ui_1.bold)('functions invoke')}, ${(0, ui_1.bold)('functions logs')}, ${(0, ui_1.bold)('logs')}, ${(0, ui_1.bold)('env')}, ${(0, ui_1.bold)('env rm')},
177
- ${(0, ui_1.bold)('connections')}, ${(0, ui_1.bold)('connections link')}, ${(0, ui_1.bold)('connections unlink')}, ${(0, ui_1.bold)('schedules')},
178
- ${(0, ui_1.bold)('schedules set')}, ${(0, ui_1.bold)('schedules rm')}, ${(0, ui_1.bold)('storage ls')}, ${(0, ui_1.bold)('storage push')},
179
- ${(0, ui_1.bold)('storage rm')}, ${(0, ui_1.bold)('apps')}, ${(0, ui_1.bold)('deployments')} (the functions deploys) and ${(0, ui_1.bold)('deploy')}. Three of
180
- them answer with less under the flag: ${(0, ui_1.bold)('db status')} gives the applied history alone,
181
- because comparing it against the local ${(0, ui_1.bold)('migrations/')} directory needs the folder,
182
- ${(0, ui_1.bold)('connections unlink')} removes the link without naming the functions that read
183
- its variables, for the same reason, and ${(0, ui_1.bold)('deploy')} takes the flag only together with
184
- ${(0, ui_1.bold)('--revision')}: that deploy runs a revision of the functions the platform already holds,
185
- so there is nothing to send and no folder to send it from.
186
-
187
- ${(0, ui_1.bold)('--app <id>')} does the same for one app: ${(0, ui_1.bold)('deploy --app <id> --revision N')}, ${(0, ui_1.bold)('deployments')},
188
- ${(0, ui_1.bold)('logs')}, ${(0, ui_1.bold)('publish')} and ${(0, ui_1.bold)('rollback')}. Its project comes from the platform, so the two
189
- flags never go together.
190
-
191
- The rest cannot answer without the working copy and have nothing to do with an id
192
- instead: ${(0, ui_1.bold)('pull')}, ${(0, ui_1.bold)('status')}, ${(0, ui_1.bold)('dev')}, ${(0, ui_1.bold)('apps add')}, ${(0, ui_1.bold)('db migrate')},
193
- ${(0, ui_1.bold)('env check')}, ${(0, ui_1.bold)('env set')}. They answer about the files next to them, so
194
- they keep asking for a folder. ${(0, ui_1.bold)('publish')} and ${(0, ui_1.bold)('rollback')} take ${(0, ui_1.bold)('--app')} rather than
195
- ${(0, ui_1.bold)('--project')}: they move one app. ${(0, ui_1.bold)('projects get')} and ${(0, ui_1.bold)('link')} take no flag either:
196
- they name the project by its id as an argument.
197
-
198
- The folder is then ignored whole, and that includes its ${(0, ui_1.bold)('xflow.json')}: the address
199
- is app.getxflow.com, or ${(0, ui_1.bold)('XFLOW_API_URL')} when it is set, and never the ${(0, ui_1.bold)('api')} field
200
- of a config that happens to be lying around. Standing in the folder of a self-hosted
201
- project, the flag therefore goes somewhere else than the folder does. An id handed to
202
- ${(0, ui_1.bold)('projects get')} or ${(0, ui_1.bold)('link')} behaves the same way: typed by hand, it is not redirected
203
- by a config lying around either.
204
-
205
- The id is echoed back on every run, with the organization that holds the project. It
206
- is the one place a project is named by hand, and a wrong id answers "not found" on a
207
- read but works on a write.
208
-
209
- With several organizations signed in, the one holding the project is looked for on
210
- every call: there is no folder to write the answer into, and a second place to
211
- remember it would be a second thing to keep true. When most calls go to one project,
168
+ project: `${(0, ui_1.bold)('--project <id>')}: a project without its folder
169
+
170
+ Every command that only talks to the platform takes it, so a service project needs
171
+ no working copy of its own:
172
+
173
+ xflow db query --project 3f2a… 'select id, title from tasks where done = false'
174
+ xflow functions invoke kanban --project 3f2a… --data-file card.json
175
+
176
+ Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db exec')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('db move')}, ${(0, ui_1.bold)('functions list')},
177
+ ${(0, ui_1.bold)('functions invoke')}, ${(0, ui_1.bold)('functions logs')}, ${(0, ui_1.bold)('logs')}, ${(0, ui_1.bold)('env')}, ${(0, ui_1.bold)('env rm')},
178
+ ${(0, ui_1.bold)('connections')}, ${(0, ui_1.bold)('connections link')}, ${(0, ui_1.bold)('connections unlink')}, ${(0, ui_1.bold)('schedules')},
179
+ ${(0, ui_1.bold)('schedules set')}, ${(0, ui_1.bold)('schedules rm')}, ${(0, ui_1.bold)('storage ls')}, ${(0, ui_1.bold)('storage push')},
180
+ ${(0, ui_1.bold)('storage rm')}, ${(0, ui_1.bold)('apps')}, ${(0, ui_1.bold)('deployments')} (the functions deploys) and ${(0, ui_1.bold)('deploy')}. Three of
181
+ them answer with less under the flag: ${(0, ui_1.bold)('db status')} gives the applied history alone,
182
+ because comparing it against the local ${(0, ui_1.bold)('migrations/')} directory needs the folder,
183
+ ${(0, ui_1.bold)('connections unlink')} removes the link without naming the functions that read
184
+ its variables, for the same reason, and ${(0, ui_1.bold)('deploy')} takes the flag only together with
185
+ ${(0, ui_1.bold)('--revision')}: that deploy runs a revision of the functions the platform already holds,
186
+ so there is nothing to send and no folder to send it from.
187
+
188
+ ${(0, ui_1.bold)('--app <id>')} does the same for one app: ${(0, ui_1.bold)('deploy --app <id> --revision N')}, ${(0, ui_1.bold)('deployments')},
189
+ ${(0, ui_1.bold)('logs')}, ${(0, ui_1.bold)('publish')} and ${(0, ui_1.bold)('rollback')}. Its project comes from the platform, so the two
190
+ flags never go together.
191
+
192
+ The rest cannot answer without the working copy and have nothing to do with an id
193
+ instead: ${(0, ui_1.bold)('pull')}, ${(0, ui_1.bold)('status')}, ${(0, ui_1.bold)('dev')}, ${(0, ui_1.bold)('apps add')}, ${(0, ui_1.bold)('db migrate')},
194
+ ${(0, ui_1.bold)('env check')}, ${(0, ui_1.bold)('env set')}. They answer about the files next to them, so
195
+ they keep asking for a folder. ${(0, ui_1.bold)('publish')} and ${(0, ui_1.bold)('rollback')} take ${(0, ui_1.bold)('--app')} rather than
196
+ ${(0, ui_1.bold)('--project')}: they move one app. ${(0, ui_1.bold)('projects get')} and ${(0, ui_1.bold)('link')} take no flag either:
197
+ they name the project by its id as an argument.
198
+
199
+ The folder is then ignored whole, and that includes its ${(0, ui_1.bold)('xflow.json')}: the address
200
+ is app.getxflow.com, or ${(0, ui_1.bold)('XFLOW_API_URL')} when it is set, and never the ${(0, ui_1.bold)('api')} field
201
+ of a config that happens to be lying around. Standing in the folder of a self-hosted
202
+ project, the flag therefore goes somewhere else than the folder does. An id handed to
203
+ ${(0, ui_1.bold)('projects get')} or ${(0, ui_1.bold)('link')} behaves the same way: typed by hand, it is not redirected
204
+ by a config lying around either.
205
+
206
+ The id is echoed back on every run, with the organization that holds the project. It
207
+ is the one place a project is named by hand, and a wrong id answers "not found" on a
208
+ read but works on a write.
209
+
210
+ With several organizations signed in, the one holding the project is looked for on
211
+ every call: there is no folder to write the answer into, and a second place to
212
+ remember it would be a second thing to keep true. When most calls go to one project,
212
213
  ${(0, ui_1.bold)('xflow org switch')} to its organization skips that search.`,
213
- projects: `${(0, ui_1.bold)('xflow projects')} and ${(0, ui_1.bold)('xflow apps')}: projects and their apps
214
-
215
- A project is a box: one database with its migrations, the cloud functions, secrets and
216
- members, and no code of its own. An app is code the platform builds and serves at
217
- https://<id>--<org>.xflowpage.ru, with its own versions and access level. One folder per
218
- project, apps in subfolders:
219
-
220
- xflow.json projectId, and who may call each function
221
- package.json dependencies of the functions
222
- functions/<name>/index.ts cloud functions, shared by every app
223
- migrations/0001_init.sql the one database of the project
224
- apps/<folder>/ an app: a Vite project with its own xflow.json (appId)
225
-
226
- xflow projects list every project, its apps count and age
227
- xflow projects get [id] one card: apps, database, functions and callers, schedules
228
- xflow apps the apps of this project, their folders and addresses
229
- --json as JSON, with the app ids
230
- xflow apps add <folder> a new app, laid into apps/<folder> from the template
231
- --name <name> its name (the folder by default)
232
- --color <color> its tile color (the platform picks one by default)
233
- xflow status in the root: the root and every app; in an app: that app
234
-
235
- The root and every app are separate histories: apps/** never travels with the root, the
236
- root never travels with an app. The app is its appId, so a folder can be renamed freely.
237
- Who opens an app (members of the project or anyone with the link) is set by a person
238
- in the web interface, not here.
239
-
240
- Both ${(0, ui_1.bold)('projects')} commands name the project positionally: the id is an argument, not a
241
- --project flag. Without one, ${(0, ui_1.bold)('get')} answers about the project of the folder it stands in.
242
-
243
- An id typed by hand is looked for across every signed-in organization, and the
244
- folder around the command is ignored whole, the way the ${(0, ui_1.bold)('--project')} flag does it
245
- (${(0, ui_1.bold)('xflow help project')}). So "not found" means the id is wrong, or its organization
246
- is missing from ${(0, ui_1.bold)('xflow org')}: never that the active key was simply the wrong one.
247
-
248
- ${(0, ui_1.bold)('list')} stays within one organization: the bound one inside a project folder, the
249
- active one outside. With several signed in, a line under the table says whose
250
- projects these are.
251
-
214
+ projects: `${(0, ui_1.bold)('xflow projects')} and ${(0, ui_1.bold)('xflow apps')}: projects and their apps
215
+
216
+ A project is a box: one database with its migrations, the cloud functions, secrets and
217
+ members, and no code of its own. An app is code the platform builds and serves at
218
+ https://<id>--<org>.xflowpage.ru, with its own versions and access level. One folder per
219
+ project, apps in subfolders:
220
+
221
+ xflow.json projectId, and who may call each function
222
+ package.json dependencies of the functions
223
+ functions/<name>/index.ts cloud functions, shared by every app
224
+ migrations/0001_init.sql the one database of the project
225
+ apps/<folder>/ an app: a Vite project with its own xflow.json (appId)
226
+
227
+ xflow projects list every project, its apps count and age
228
+ xflow projects get [id] one card: apps, database, functions and callers, schedules
229
+ xflow apps the apps of this project, their folders and addresses
230
+ --json as JSON, with the app ids
231
+ xflow apps add <folder> a new app, laid into apps/<folder> from the template
232
+ --name <name> its name (the folder by default)
233
+ --color <color> its tile color (the platform picks one by default)
234
+ xflow status in the root: the root and every app; in an app: that app
235
+
236
+ The root and every app are separate histories: apps/** never travels with the root, the
237
+ root never travels with an app. The app is its appId, so a folder can be renamed freely.
238
+ Who opens an app (members of the project or anyone with the link) is set by a person
239
+ in the web interface, not here.
240
+
241
+ Both ${(0, ui_1.bold)('projects')} commands name the project positionally: the id is an argument, not a
242
+ --project flag. Without one, ${(0, ui_1.bold)('get')} answers about the project of the folder it stands in.
243
+
244
+ An id typed by hand is looked for across every signed-in organization, and the
245
+ folder around the command is ignored whole, the way the ${(0, ui_1.bold)('--project')} flag does it
246
+ (${(0, ui_1.bold)('xflow help project')}). So "not found" means the id is wrong, or its organization
247
+ is missing from ${(0, ui_1.bold)('xflow org')}: never that the active key was simply the wrong one.
248
+
249
+ ${(0, ui_1.bold)('list')} stays within one organization: the bound one inside a project folder, the
250
+ active one outside. With several signed in, a line under the table says whose
251
+ projects these are.
252
+
252
253
  ${(0, ui_1.bold)('xflow link <id>')} binds the current folder to a project; ${(0, ui_1.bold)('xflow init')} makes a new one.`,
253
- link: `${(0, ui_1.bold)('xflow link')} <id>: bind this folder to a project
254
-
255
- Writes the root xflow.json and .xflow/state.json and lays down the instructions for the
256
- agents. The id comes from ${(0, ui_1.bold)('xflow projects list')}; a project of another signed-in
257
- organization is found too, and the folder is bound to that organization.
258
-
259
- An empty folder (.git alone counts as empty) gets the whole project: the latest revision
260
- of the root and of every app, each app in apps/<folder> under the folder name the project
261
- revision records. A folder with files keeps them as they are: ${(0, ui_1.bold)('xflow status')} says
262
- how they differ, ${(0, ui_1.bold)('xflow pull --into ./server-copy')} fetches the whole project next to them.
263
-
264
- In a folder that is already linked, the command points xflow.json at the other project and
254
+ link: `${(0, ui_1.bold)('xflow link')} <id>: bind this folder to a project
255
+
256
+ Writes the root xflow.json and .xflow/state.json and lays down the instructions for the
257
+ agents. The id comes from ${(0, ui_1.bold)('xflow projects list')}; a project of another signed-in
258
+ organization is found too, and the folder is bound to that organization.
259
+
260
+ An empty folder (.git alone counts as empty) gets the whole project: the latest revision
261
+ of the root and of every app, each app in apps/<folder> under the folder name the project
262
+ revision records. A folder with files keeps them as they are: ${(0, ui_1.bold)('xflow status')} says
263
+ how they differ, ${(0, ui_1.bold)('xflow pull --into ./server-copy')} fetches the whole project next to them.
264
+
265
+ In a folder that is already linked, the command points xflow.json at the other project and
265
266
  keeps the rest of the file. A new project instead of an existing one: ${(0, ui_1.bold)('xflow init')}.`,
266
- pages: `${(0, ui_1.bold)('xflow pages')}: public sites of the organization
267
-
268
- A page is a static site at https://<name>.xflowpage.ru: a landing page, a presentation, an
269
- invoice for people outside the organization. It opens by its address for anyone, without
270
- signing in, and has no build, no publish step and no database. An app is the opposite: code
271
- the platform builds, with versions, access levels and a database through functions.
272
-
273
- xflow pages the pages, their state and address
274
- xflow pages create <title> <path> a new page with its first version: there are no
275
- empty pages
276
- --slug <name> the name in the address; without it the name is made
277
- from the title, with a random tail
278
- xflow pages deploy <page> <path> upload a new version
279
- -m <text> what changed, one short line: it names the version
280
- xflow pages status <page> the page and its versions
281
- xflow pages rollback <page> <N> put version N back on the address
282
- xflow pages rm <page> delete the page
283
- --yes without it the command names what goes and stops
284
-
285
- ${(0, ui_1.bold)('<path>')} is a folder with index.html at the top, its .zip, or one .html file.
286
- ${(0, ui_1.bold)('<page>')} is the name from the address or the id. ${(0, ui_1.bold)('--json')} on list and status
287
- prints the answer as JSON. A page belongs to the organization, not to a project, so there is
288
- no ${(0, ui_1.bold)('--project')}: the key is picked as for every other command (${(0, ui_1.bold)('xflow help org')}).
289
-
290
- ${(0, ui_1.bold)('An upload is live at once.')} There is no draft and no publish step: anyone with the
291
- address sees the new version within seconds. Uploading the same files again changes nothing.
292
- The files of the last 5 versions are kept, so a rollback takes seconds and needs no upload.
293
-
294
- What goes up is the built site: HTML, CSS, JS, images, fonts, PDF, audio and video. No code
295
- runs on the platform side, and a folder with package.json is refused as the sources of a site:
296
- build it and upload what the build writes. Dot entries are skipped. At most 5 MB zipped, 25 MB
297
- unpacked and 1000 files.
298
-
299
- The page is public, so a file holding a secret key is refused whole: every visitor would read
300
- it. A page has no backend: a form or a lookup belongs in an app with access "anyone". The one
301
- key a page may hold is a function key, and it opens that function to every visitor: an HTML
302
- form can post to its webhook address with ${(0, ui_1.bold)('?key=')}, while fetch cannot read the answer
303
- (no CORS). Search engines are asked not to index pages, so do not promise SEO.
304
-
305
- ${(0, ui_1.bold)('Deleting is final.')} The address goes empty and every version of the page is gone: there
306
- is no undo and no copy on the platform side. The name stays reserved for the organization, so
307
- the same ${(0, ui_1.bold)('--slug')} is free for the next page of yours and taken for everyone else. A
308
- throwaway page put up to try something is worth deleting the same way it was made.
309
-
310
- A page takes the key of the whole organization, the one ${(0, ui_1.bold)('xflow login')} issues: a key
311
- scoped to projects is refused. A page is deleted by whoever made it, or by the owner of the
267
+ pages: `${(0, ui_1.bold)('xflow pages')}: public sites of the organization
268
+
269
+ A page is a static site at https://<name>.xflowpage.ru: a landing page, a presentation, an
270
+ invoice for people outside the organization. It opens by its address for anyone, without
271
+ signing in, and has no build, no publish step and no database. An app is the opposite: code
272
+ the platform builds, with versions, access levels and a database through functions.
273
+
274
+ xflow pages the pages, their state and address
275
+ xflow pages create <title> <path> a new page with its first version: there are no
276
+ empty pages
277
+ --slug <name> the name in the address; without it the name is made
278
+ from the title, with a random tail
279
+ xflow pages deploy <page> <path> upload a new version
280
+ -m <text> what changed, one short line: it names the version
281
+ xflow pages status <page> the page and its versions
282
+ xflow pages rollback <page> <N> put version N back on the address
283
+ xflow pages rm <page> delete the page
284
+ --yes without it the command names what goes and stops
285
+
286
+ ${(0, ui_1.bold)('<path>')} is a folder with index.html at the top, its .zip, or one .html file.
287
+ ${(0, ui_1.bold)('<page>')} is the name from the address or the id. ${(0, ui_1.bold)('--json')} on list and status
288
+ prints the answer as JSON. A page belongs to the organization, not to a project, so there is
289
+ no ${(0, ui_1.bold)('--project')}: the key is picked as for every other command (${(0, ui_1.bold)('xflow help org')}).
290
+
291
+ ${(0, ui_1.bold)('An upload is live at once.')} There is no draft and no publish step: anyone with the
292
+ address sees the new version within seconds. Uploading the same files again changes nothing.
293
+ The files of the last 5 versions are kept, so a rollback takes seconds and needs no upload.
294
+
295
+ What goes up is the built site: HTML, CSS, JS, images, fonts, PDF, audio and video. No code
296
+ runs on the platform side, and a folder with package.json is refused as the sources of a site:
297
+ build it and upload what the build writes. Dot entries are skipped. At most 5 MB zipped, 25 MB
298
+ unpacked and 1000 files.
299
+
300
+ The page is public, so a file holding a secret key is refused whole: every visitor would read
301
+ it. A page has no backend: a form or a lookup belongs in an app with access "anyone". The one
302
+ key a page may hold is a function key, and it opens that function to every visitor: an HTML
303
+ form can post to its webhook address with ${(0, ui_1.bold)('?key=')}, while fetch cannot read the answer
304
+ (no CORS). Search engines are asked not to index pages, so do not promise SEO.
305
+
306
+ ${(0, ui_1.bold)('Deleting is final.')} The address goes empty and every version of the page is gone: there
307
+ is no undo and no copy on the platform side. The name stays reserved for the organization, so
308
+ the same ${(0, ui_1.bold)('--slug')} is free for the next page of yours and taken for everyone else. A
309
+ throwaway page put up to try something is worth deleting the same way it was made.
310
+
311
+ A page takes the key of the whole organization, the one ${(0, ui_1.bold)('xflow login')} issues: a key
312
+ scoped to projects is refused. A page is deleted by whoever made it, or by the owner of the
312
313
  organization. Switching a page off for a while is for a person, in the platform interface.`,
313
- update: `${(0, ui_1.bold)('xflow update')}: bring the CLI up to date
314
-
315
- Installs the published version and, if anything changed, rewrites the platform
316
- instructions the agents already have: the skill ships inside the package and shares
317
- its version, so a fresh CLI next to a stale skill means the agent is reading last
318
- month's rules. Copies that are not installed are not created, so it is safe to run
319
- from any folder.
320
-
321
- The platform reports the published version with every answer, which is where the
322
- ${(0, ui_1.bold)('xflow N is out')} line comes from. It appears once a day at most, and never in CI,
323
- where nobody is going to update anything anyway.
324
-
325
- The command works out how this copy was installed from its own path, and refuses to
326
- cross package managers: an install made by pnpm or bun is printed as their own
327
- command rather than handed to npm, which would leave you with two copies and no way
328
- to tell which one answers. Two more cases end the same way. Under ${(0, ui_1.bold)('npx')} there is
329
- nothing to update, only a cache. And a copy inside a project's node_modules is a
330
- dependency of that project: ${(0, ui_1.bold)('npm i -D @getxflow/cli@latest')} is the fix, because
331
- inside npm scripts the local copy wins over anything installed globally.
332
-
333
- A refusal from the platform saying the CLI is too old is a different thing: that one
334
- is a hard gate on a changed contract, and until you run this command nothing else
314
+ update: `${(0, ui_1.bold)('xflow update')}: bring the CLI up to date
315
+
316
+ Installs the published version and, if anything changed, rewrites the platform
317
+ instructions the agents already have: the skill ships inside the package and shares
318
+ its version, so a fresh CLI next to a stale skill means the agent is reading last
319
+ month's rules. Copies that are not installed are not created, so it is safe to run
320
+ from any folder.
321
+
322
+ The platform reports the published version with every answer, which is where the
323
+ ${(0, ui_1.bold)('xflow N is out')} line comes from. It appears once a day at most, and never in CI,
324
+ where nobody is going to update anything anyway.
325
+
326
+ The command works out how this copy was installed from its own path, and refuses to
327
+ cross package managers: an install made by pnpm or bun is printed as their own
328
+ command rather than handed to npm, which would leave you with two copies and no way
329
+ to tell which one answers. Two more cases end the same way. Under ${(0, ui_1.bold)('npx')} there is
330
+ nothing to update, only a cache. And a copy inside a project's node_modules is a
331
+ dependency of that project: ${(0, ui_1.bold)('npm i -D @getxflow/cli@latest')} is the fix, because
332
+ inside npm scripts the local copy wins over anything installed globally.
333
+
334
+ A refusal from the platform saying the CLI is too old is a different thing: that one
335
+ is a hard gate on a changed contract, and until you run this command nothing else
335
336
  will work.`,
336
- db: `${(0, ui_1.bold)('xflow db')}: schema, data, migrations
337
-
338
- xflow db status what is applied, what is waiting, the server
339
- xflow db list databases of the projects, the servers of the organization
340
- xflow db schema [table] tables of the project, or the columns of one
341
- xflow db query 'select ...' read data (rows: the query's own limit, 50 without
342
- one, 300 at most; --json prints the values whole)
343
- xflow db query <file>.sql the same, the query read from a file
344
- xflow db migrate store the project root, then apply migrations/*.sql
345
- xflow db move --to <server> --yes
346
- move the project database to another server
347
-
348
- Every project has exactly one database of its own, shared by all of its apps and functions,
349
- and every command here works from any folder of the project. ${(0, ui_1.bold)('list')} is about the
350
- organization: one database per project, and where each lives: ${(0, ui_1.bold)('platform')} is the common
351
- server, ${(0, ui_1.bold)('own "<name>"')} an own server of the organization. While an own server is not
352
- running, its databases do not answer, and that is not fixable from the code. An organization
353
- with own servers also gets them listed, with the room used on each: that is the choice for
354
- ${(0, ui_1.bold)('xflow init --server')} and ${(0, ui_1.bold)('db move')}.
355
-
356
- ${(0, ui_1.bold)('move')} takes the database of this project to another server of the organization:
357
- ${(0, ui_1.bold)('platform')} or an own server by name (or id, when two share a name). The platform copies
358
- the tables, switches the functions of the project and removes the old copy; nothing needs a
359
- deploy. While it copies, the apps read data but cannot save anything new, usually for a
360
- couple of minutes, which is why it asks for ${(0, ui_1.bold)('--yes')}. It also needs the
361
- right to move databases on the key: off by default, only its owner turns it on, in the
362
- platform settings where the key permissions live. The command follows the move to the end;
363
- stopping it does not stop the move, and ${(0, ui_1.bold)('xflow db status')} shows where it stands. A move
364
- that fails is rolled back. Buying, paying for and deleting servers stay with the person.
365
-
366
- ${(0, ui_1.bold)('schema')} answers a different question than the ${(0, ui_1.bold)('migrations/')} directory: the files say
367
- what you did, the schema says what is actually in there. ${(0, ui_1.bold)('query')} runs inside a READ ONLY transaction, so
368
- writes are rejected by the database itself, not by us reading your SQL. Single quotes around
369
- the query hold in PowerShell, bash and zsh, while cmd.exe wants double ones; a query that
370
- needs quotes of its own goes in a .sql file outside the project, because Windows PowerShell
371
- strips double quotes from arguments and a file in migrations/ would be taken for a
372
- migration. Its table cuts
373
- long values to keep the columns readable; ${(0, ui_1.bold)('--json')} prints them whole, together with
374
- the applied limit and its ceiling. ${(0, ui_1.bold)('status')} named by id (${(0, ui_1.bold)('--project')}) answers with
375
- the applied history alone: comparing against ${(0, ui_1.bold)('migrations/')} needs the folder.
376
-
377
- ${(0, ui_1.bold)('migrate')}: the files live in the project root (${(0, ui_1.bold)('migrations/0001_init.sql')},
378
- ${(0, ui_1.bold)('migrations/0002_orders.sql')} and so on). The order comes from the file name, which
379
- is why the leading number is required. The command first stores the project root as a
380
- revision, so the migrations stay in its history and a clone gets them, then applies what is
381
- new. Every migration runs in its own transaction, the history is kept in the database
382
- itself, and anything already applied is not run again.
383
-
384
- --dry-run report what would be applied without touching the database
385
- --allow-destructive allow operations that destroy data
386
-
387
- About destructive ones. The platform keeps no database history and makes no backups,
388
- so ${(0, ui_1.bold)('DROP TABLE')}, ${(0, ui_1.bold)('DROP COLUMN')}, ${(0, ui_1.bold)('TRUNCATE')} and ${(0, ui_1.bold)('DELETE FROM')} without a
389
- condition need two things at once: this flag, and the right to destroy data on the
390
- access key. That right is off by default, and only its owner turns it on, in the
391
- platform settings, where the key permissions live: a flag is something an agent adds by itself, a
392
- right is not. With both in place, the contents of the affected tables are dumped and
393
- kept for 7 days: that is a "caught it right away" safety net, not a backup.
394
- ${(0, ui_1.bold)('DROP DATABASE')} is never allowed.
395
-
396
- Editing an already applied file achieves nothing: ${(0, ui_1.bold)('migrate')} refuses a name that is
397
- already applied with different content, and ${(0, ui_1.bold)('xflow db status')} lists such a file
337
+ db: `${(0, ui_1.bold)('xflow db')}: schema, data, migrations
338
+
339
+ xflow db status what is applied, what is waiting, the server
340
+ xflow db list databases of the projects, the servers of the organization
341
+ xflow db schema [table] tables of the project, or the columns of one
342
+ xflow db query 'select ...' read data (rows: the query's own limit, 50 without
343
+ one, 300 at most; --json prints the values whole)
344
+ xflow db query <file>.sql the same, the query read from a file
345
+ xflow db exec 'update ...' a one-off data change: one INSERT, UPDATE, DELETE or MERGE
346
+ (or <file>.sql; --allow-destructive, --json)
347
+ xflow db migrate store the project root, then apply migrations/*.sql
348
+ xflow db move --to <server> --yes
349
+ move the project database to another server
350
+
351
+ Every project has exactly one database of its own, shared by all of its apps and functions,
352
+ and every command here works from any folder of the project. ${(0, ui_1.bold)('list')} is about the
353
+ organization: one database per project, and where each lives: ${(0, ui_1.bold)('platform')} is the common
354
+ server, ${(0, ui_1.bold)('own "<name>"')} an own server of the organization. While an own server is not
355
+ running, its databases do not answer, and that is not fixable from the code. An organization
356
+ with own servers also gets them listed, with the room used on each: that is the choice for
357
+ ${(0, ui_1.bold)('xflow init --server')} and ${(0, ui_1.bold)('db move')}.
358
+
359
+ ${(0, ui_1.bold)('move')} takes the database of this project to another server of the organization:
360
+ ${(0, ui_1.bold)('platform')} or an own server by name (or id, when two share a name). The platform copies
361
+ the tables, switches the functions of the project and removes the old copy; nothing needs a
362
+ deploy. While it copies, the apps read data but cannot save anything new, usually for a
363
+ couple of minutes, which is why it asks for ${(0, ui_1.bold)('--yes')}. It also needs the
364
+ right to move databases on the key: off by default, only its owner turns it on, in the
365
+ platform settings where the key permissions live. The command follows the move to the end;
366
+ stopping it does not stop the move, and ${(0, ui_1.bold)('xflow db status')} shows where it stands. A move
367
+ that fails is rolled back. Buying, paying for and deleting servers stay with the person.
368
+
369
+ ${(0, ui_1.bold)('schema')} answers a different question than the ${(0, ui_1.bold)('migrations/')} directory: the files say
370
+ what you did, the schema says what is actually in there. ${(0, ui_1.bold)('query')} runs inside a READ ONLY transaction, so
371
+ writes are rejected by the database itself, not by us reading your SQL. Single quotes around
372
+ the query hold in PowerShell, bash and zsh, while cmd.exe wants double ones; a query that
373
+ needs quotes of its own goes in a .sql file outside the project, because Windows PowerShell
374
+ strips double quotes from arguments and a file in migrations/ would be taken for a
375
+ migration. Its table cuts
376
+ long values to keep the columns readable; ${(0, ui_1.bold)('--json')} prints them whole, together with
377
+ the applied limit and its ceiling. ${(0, ui_1.bold)('status')} named by id (${(0, ui_1.bold)('--project')}) answers with
378
+ the applied history alone: comparing against ${(0, ui_1.bold)('migrations/')} needs the folder.
379
+
380
+ ${(0, ui_1.bold)('exec')} fixes data once: one INSERT, UPDATE, DELETE or MERGE, saved at once and kept out
381
+ of the migration history, so a one-off fix no longer becomes a migration forever. Schema
382
+ changes are refused: they stay with ${(0, ui_1.bold)('migrate')}. The platform judges the change by what the
383
+ database reports, cascades and triggers included. Deleting rows, or updating more than 100
384
+ in a table, asks for ${(0, ui_1.bold)('--allow-destructive')}, and those tables are copied for 7 days before it
385
+ is saved; deleting also needs the right to destroy data on the key, as in migrations (see
386
+ below). The rows it holds are locked while it runs, so a statement that waits on a running
387
+ app is cancelled after a few seconds and changes nothing. There is no undo.
388
+
389
+ ${(0, ui_1.bold)('migrate')}: the files live in the project root (${(0, ui_1.bold)('migrations/0001_init.sql')},
390
+ ${(0, ui_1.bold)('migrations/0002_orders.sql')} and so on). The order comes from the file name, which
391
+ is why the leading number is required. The command first stores the project root as a
392
+ revision, so the migrations stay in its history and a clone gets them, then applies what is
393
+ new. Every migration runs in its own transaction, the history is kept in the database
394
+ itself, and anything already applied is not run again.
395
+
396
+ --dry-run report what would be applied without touching the database
397
+ --allow-destructive allow operations that destroy data
398
+
399
+ About destructive ones. The platform keeps no database history and makes no backups,
400
+ so ${(0, ui_1.bold)('DROP TABLE')}, ${(0, ui_1.bold)('DROP COLUMN')}, ${(0, ui_1.bold)('TRUNCATE')} and deleting rows (judged by what
401
+ the database did, not by the text) need two things at once: this flag, and the right to destroy data on the
402
+ access key. That right is off by default, and only its owner turns it on, in the
403
+ platform settings, where the key permissions live: a flag is something an agent adds by itself, a
404
+ right is not. With both in place, the contents of the affected tables are dumped and
405
+ kept for 7 days: that is a "caught it right away" safety net, not a backup.
406
+ ${(0, ui_1.bold)('DROP DATABASE')} is never allowed.
407
+
408
+ Editing an already applied file achieves nothing: ${(0, ui_1.bold)('migrate')} refuses a name that is
409
+ already applied with different content, and ${(0, ui_1.bold)('xflow db status')} lists such a file
398
410
  separately. Write a new migration instead.`,
399
- logs: `${(0, ui_1.bold)('xflow logs')} and ${(0, ui_1.bold)('xflow functions logs')}: project errors
400
-
401
- Two streams: crashes of the cloud functions of the project, and browser errors of each
402
- app. ${(0, ui_1.bold)('xflow logs')} in an app folder (or with ${(0, ui_1.bold)('--app <id>')}) shows that app, in the
403
- project root every app, each row named by its folder. The last 200 entries of each stream
404
- are kept, older ones fall off.
405
-
406
- --limit N how many to show (10 by default)
407
-
408
- A function reports its own crashes: Yandex exposes its logs only over gRPC, so the
409
- wrapper catches the exception, carries the tail of the console with it (the last 40
410
- ${(0, ui_1.bold)('console.log')} lines of that call) and sends it to the platform. Successful calls
411
- write nothing, otherwise every request would pay for it in latency.
412
-
413
- What never reaches this list: a crash while the module is starting (the function
414
- never gets as far as the wrapper), going over the execution time cap of the plan
415
- (${(0, ui_1.bold)('xflow whoami')} names it), and running out of memory.
416
- Those show up in the answer to ${(0, ui_1.bold)('xflow functions invoke')}.
417
-
418
- Browser errors are collected by ${(0, ui_1.bold)('src/utils/error-logger.ts')} of the template and
411
+ logs: `${(0, ui_1.bold)('xflow logs')} and ${(0, ui_1.bold)('xflow functions logs')}: project errors
412
+
413
+ Two streams: crashes of the cloud functions of the project, and browser errors of each
414
+ app. ${(0, ui_1.bold)('xflow logs')} in an app folder (or with ${(0, ui_1.bold)('--app <id>')}) shows that app, in the
415
+ project root every app, each row named by its folder. The last 200 entries of each stream
416
+ are kept, older ones fall off.
417
+
418
+ --limit N how many to show (10 by default)
419
+
420
+ A function reports its own crashes: Yandex exposes its logs only over gRPC, so the
421
+ wrapper catches the exception, carries the tail of the console with it (the last 40
422
+ ${(0, ui_1.bold)('console.log')} lines of that call) and sends it to the platform. Successful calls
423
+ write nothing, otherwise every request would pay for it in latency.
424
+
425
+ What never reaches this list: a crash while the module is starting (the function
426
+ never gets as far as the wrapper), going over the execution time cap of the plan
427
+ (${(0, ui_1.bold)('xflow whoami')} names it), and running out of memory.
428
+ Those show up in the answer to ${(0, ui_1.bold)('xflow functions invoke')}.
429
+
430
+ Browser errors are collected by ${(0, ui_1.bold)('src/utils/error-logger.ts')} of the template and
419
431
  only from the published addresses: a local ${(0, ui_1.bold)('xflow dev')} writes nothing here.`,
420
- storage: `${(0, ui_1.bold)('xflow storage')}: files of the application
421
-
422
- The sources archive is capped at 10 MB and is rebuilt and re-uploaded on every
423
- deploy, so photos, video and PDFs do not belong in the repository. File storage
424
- is the place for them: the files sit beside the versions, survive a build, and
425
- are metered against the plan of the organization.
426
-
427
- xflow storage ls [folder] what is there, with addresses and sizes
428
- xflow storage push ./media --to media
429
- upload a folder, keeping its structure
430
- xflow storage rm <address> delete one file
431
- xflow storage rm --folder photos --yes
432
- delete a folder with everything in it
433
-
434
- --to <folder> where to put it in the project storage
435
- --replace upload files whose size differs from the stored one
436
- --json machine-readable output, with every address
437
-
438
- ${(0, ui_1.bold)('push')} sends only what is missing: a file already stored under the same path and
439
- size is skipped, so running the command again after a broken connection is cheap.
440
- A file that differs is left alone unless ${(0, ui_1.bold)('--replace')} is given. Bytes go straight
441
- into the bucket, they do not pass through the platform.
442
-
443
- Addresses are permanent and belong to the record, not to the bytes: replacing a
444
- file keeps its address, so the links in the code and in the tables of the
445
- application keep working. That is why there is no delete-and-upload-again.
446
-
447
- The address is not public: it is a page of the platform that checks the rights of
448
- the viewer on every request, so a closed project stops serving its pictures too.
449
- Use the address as it came back, in ${(0, ui_1.bold)('<img src>')} and in the data of the application.
450
-
451
- Dot entries and symbolic links are skipped, and ${(0, ui_1.bold)('.xflowignore')} is not read here:
452
- those are the rules for the sources, and a media folder is exactly what one is
453
- asked to put there so it stays out of the archive.
454
-
455
- ${(0, ui_1.bold)('rm')} needs a right of its own (${(0, ui_1.bold)('Delete files')}, where the key permissions live in
456
- the platform settings), off by default. The reason is the neighbourhood: whatever the users of
457
- the application uploaded lives in the same folders, and there is no undo. A folder
458
- first answers with the number of files it holds and deletes only on ${(0, ui_1.bold)('--yes')}.
459
-
432
+ storage: `${(0, ui_1.bold)('xflow storage')}: files of the application
433
+
434
+ The sources archive is capped at 10 MB and is rebuilt and re-uploaded on every
435
+ deploy, so photos, video and PDFs do not belong in the repository. File storage
436
+ is the place for them: the files sit beside the versions, survive a build, and
437
+ are metered against the plan of the organization.
438
+
439
+ xflow storage ls [folder] what is there, with addresses and sizes
440
+ xflow storage push ./media --to media
441
+ upload a folder, keeping its structure
442
+ xflow storage rm <address> delete one file
443
+ xflow storage rm --folder photos --yes
444
+ delete a folder with everything in it
445
+
446
+ --to <folder> where to put it in the project storage
447
+ --replace upload files whose size differs from the stored one
448
+ --json machine-readable output, with every address
449
+
450
+ ${(0, ui_1.bold)('push')} sends only what is missing: a file already stored under the same path and
451
+ size is skipped, so running the command again after a broken connection is cheap.
452
+ A file that differs is left alone unless ${(0, ui_1.bold)('--replace')} is given. Bytes go straight
453
+ into the bucket, they do not pass through the platform.
454
+
455
+ Addresses are permanent and belong to the record, not to the bytes: replacing a
456
+ file keeps its address, so the links in the code and in the tables of the
457
+ application keep working. That is why there is no delete-and-upload-again.
458
+
459
+ The address is not public: it is a page of the platform that checks the rights of
460
+ the viewer on every request, so a closed project stops serving its pictures too.
461
+ Use the address as it came back, in ${(0, ui_1.bold)('<img src>')} and in the data of the application.
462
+
463
+ Dot entries and symbolic links are skipped, and ${(0, ui_1.bold)('.xflowignore')} is not read here:
464
+ those are the rules for the sources, and a media folder is exactly what one is
465
+ asked to put there so it stays out of the archive.
466
+
467
+ ${(0, ui_1.bold)('rm')} needs a right of its own (${(0, ui_1.bold)('Delete files')}, where the key permissions live in
468
+ the platform settings), off by default. The reason is the neighbourhood: whatever the users of
469
+ the application uploaded lives in the same folders, and there is no undo. A folder
470
+ first answers with the number of files it holds and deletes only on ${(0, ui_1.bold)('--yes')}.
471
+
460
472
  There is no command back: sources return with ${(0, ui_1.bold)('xflow pull')}, files do not.`,
461
- env: `${(0, ui_1.bold)('xflow env')}: environment variables of the functions
462
-
463
- Keys, passwords and third-party addresses are kept by the platform, not by the
464
- repository. A value goes up and never comes back: the only place to see it is inside
465
- the function. So keep your own copy wherever you got it from.
466
-
467
- xflow env what is stored (names, not values)
468
- xflow env check what the functions of this project are missing
469
- xflow env set SMTP_PASSWORD=… store
470
- xflow env rm SMTP_PASSWORD delete
471
-
472
- --scope project visible to this project only
473
- --scope organization visible to every project of the organization (the default)
474
-
475
- Those two words are the whole list: a third is refused rather than read as the default,
476
- because a value meant for one project would have gone out to all of them.
477
-
478
- ${(0, ui_1.bold)('check')} reads the sources in ${(0, ui_1.bold)('functions/')} and looks for ${(0, ui_1.bold)('process.env.NAME')}
479
- references. The same rule applies on deploy: a function receives only the variables it
480
- mentions by name. A name assembled from an expression (${(0, ui_1.bold)("process.env['KEY_' + n]")}),
481
- reached through ${(0, ui_1.bold)('?.')}, destructured or spread never reaches the environment, so read
482
- variables literally. ${(0, ui_1.bold)('check')} names those reads and the build rejects them: a secret
483
- that quietly fails to arrive is found by the function falling over weeks later.
484
-
485
- The value reaches the function on deploy, not at the moment it is stored: after
486
- ${(0, ui_1.bold)('env set')} run ${(0, ui_1.bold)('xflow deploy functions')}. It ships a function whose code did not
487
- change but whose variables did, so nothing is left holding an old value. The same after a
488
- delete: a function already deployed keeps the old value until its next deploy. Apps get no
473
+ env: `${(0, ui_1.bold)('xflow env')}: environment variables of the functions
474
+
475
+ Keys, passwords and third-party addresses are kept by the platform, not by the
476
+ repository. A value goes up and never comes back: the only place to see it is inside
477
+ the function. So keep your own copy wherever you got it from.
478
+
479
+ xflow env what is stored (names, not values)
480
+ xflow env check what the functions of this project are missing
481
+ xflow env set SMTP_PASSWORD=… store
482
+ xflow env rm SMTP_PASSWORD delete
483
+
484
+ --scope project visible to this project only
485
+ --scope organization visible to every project of the organization (the default)
486
+
487
+ Those two words are the whole list: a third is refused rather than read as the default,
488
+ because a value meant for one project would have gone out to all of them.
489
+
490
+ ${(0, ui_1.bold)('check')} reads the sources in ${(0, ui_1.bold)('functions/')} and looks for ${(0, ui_1.bold)('process.env.NAME')}
491
+ references. The same rule applies on deploy: a function receives only the variables it
492
+ mentions by name. A name assembled from an expression (${(0, ui_1.bold)("process.env['KEY_' + n]")}),
493
+ reached through ${(0, ui_1.bold)('?.')}, destructured or spread never reaches the environment, so read
494
+ variables literally. ${(0, ui_1.bold)('check')} names those reads and the build rejects them: a secret
495
+ that quietly fails to arrive is found by the function falling over weeks later.
496
+
497
+ The value reaches the function on deploy, not at the moment it is stored: after
498
+ ${(0, ui_1.bold)('env set')} run ${(0, ui_1.bold)('xflow deploy functions')}. It ships a function whose code did not
499
+ change but whose variables did, so nothing is left holding an old value. The same after a
500
+ delete: a function already deployed keeps the old value until its next deploy. Apps get no
489
501
  variables at all: whatever a secret protects is reached through a function.`,
490
- connections: `${(0, ui_1.bold)('xflow connections')}: accounts connected to the organization
491
-
492
- Somebody signs in to Yandex Metrika, Bitrix or a mail service once, in the platform,
493
- and that account becomes a connection of the organization. Linked to a project, it
494
- hands its credentials to the cloud functions as environment variables. Nobody has to
495
- paste a token into the repository, and nobody sees the value: it goes straight from
496
- the platform into the function.
497
-
498
- xflow connections what this project can use, and what it already uses
499
- xflow connections link <name> --as ALIAS
500
- hand its credentials to the functions
501
- xflow connections unlink <ALIAS> take them away again
502
- xflow connections call <name> <path>
503
- ask that API something, once, without deploying
504
-
505
- Every row says whether the connection is linked to this project (its alias), which of
506
- its fields a call can fill in (${(0, ui_1.bold)('{CLIENT_ID}')} and the like) and what state the
507
- access is in. ${(0, ui_1.bold)('available, not linked')} is the useful one: the account exists in
508
- the organization, but this project gets nothing from it yet: link it.
509
-
510
- Both commands take the name of the connection, the first column of the list, and both
511
- also take its identifier. ${(0, ui_1.bold)('unlink')} takes the alias as well, and prefers it: the
512
- alias is the name your own code already uses. One organization can hold several accounts
513
- of the same service under one name; then the command prints their identifiers and asks
514
- for one of those instead of guessing.
515
-
516
- ${(0, ui_1.bold)('unlink')} refuses while a function still reads one of the variables and names
517
- those functions; ${(0, ui_1.bold)('--yes')} goes through anyway. Seeing them needs the sources,
518
- so under ${(0, ui_1.bold)('--project')} there is no such check: the link goes, and the variables
519
- that go with it are printed instead. Both commands take the flag, because an account
520
- linked to a service project without a folder has to be removable the same way.
521
-
522
- Variables are named after the alias: an OAuth connection called ${(0, ui_1.bold)('YANDEX_METRIKA')}
523
- gives ${(0, ui_1.bold)('YANDEX_METRIKA_TOKEN')}, a key-based one gives a variable per field. They
524
- arrive at a function on its next deploy, like every other variable, so after a change
525
- run ${(0, ui_1.bold)('xflow deploy functions')}. A token close to expiry is renewed by that deploy on the
526
- way; a revoked one is not renewed by anything until a human reconnects the account.
527
-
528
- Linking needs a right on the key, ${(0, ui_1.bold)('connections:link')}. Keys are issued with it,
529
- and a person can take it away in the platform settings, where the key permissions live; a key cannot give
530
- it back to itself. Losing it is what the refusal says, and the answer is to ask a person,
531
- not to look for another route. Either way only the accounts granted to you personally can
532
- be linked at all: that rule holds whatever the key is allowed to do.
533
-
534
- Connecting a new account and switching one off stay with a person, in the platform
535
- interface. There is no command for either.
536
-
537
- ${(0, ui_1.bold)('call')} answers the question an API only answers by being asked: what does this
538
- endpoint actually return, and is the field you need in it. The platform attaches the
539
- credentials of the connected account and makes the request itself, so nothing has to be
540
- deployed to find out, and the key never reaches you.
541
-
542
- xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
543
- xflow connections call AMO /api/v4/leads --method POST --data '{"name":"Test"}'
544
- xflow connections call OZON /v1/product/list --method POST --data '{}' \\
545
- --header '{"Client-Id":"{CLIENT_ID}"}'
546
-
547
- The path is written after the address of the connector, query string and all, or given in
548
- full as ${(0, ui_1.bold)('https://...')}. Only the hosts of that connector are reachable, and a
549
- refusal lists them. ${(0, ui_1.bold)('--method')} defaults to GET, or to POST when there is a body.
550
-
551
- ${(0, ui_1.bold)('{NAME}')} in the path or in a header value is filled in by the platform from the
552
- fields of the connection. That is how the two-header APIs are called: Ozon wants
553
- ${(0, ui_1.bold)('Client-Id')} beside its key, Yandex Direct wants ${(0, ui_1.bold)('Client-Login')} for an
554
- agency account, and you never see those values. Which names a connection has is the fourth
555
- column of ${(0, ui_1.bold)('xflow connections')}, printed in braces and the same whether the account
556
- is linked or not. Do not derive them from ${(0, ui_1.bold)('xflow env')}: that list is the variables
557
- of a link, and it names both more and less than this one. A name the connection does not
558
- have is left in the text as you typed it and the request still goes out, so an invented
559
- name reaches somebody's live account as text.
560
-
561
- The answer is printed head first, at most a few kilobytes, and it says so when there is
562
- more. Ask for the shape, not the volume: one day and ${(0, ui_1.bold)('limit=1')} tells you what the
563
- fields are called, while the whole dataset belongs in a cloud function. ${(0, ui_1.bold)('--json')}
564
- prints the envelope for a pipe: ${(0, ui_1.bold)("--json | jq '.response.body | keys'")} gives the
565
- field names without the body ever entering your context.
566
-
567
- An account linked to this project is called by its alias. One that is only available is
568
- called by its name, and no linking is needed: what the command asks is that you could have
569
- linked it yourself, which needs the ${(0, ui_1.bold)('connections:link')} right and the account being
570
- granted to you personally. Either way nothing widens: linked credentials already reach the
571
- functions of this project, and an account you may link you may also read by deploying a
572
- function into it. An account granted to somebody else stays out of reach, and the refusal
573
- says which of the two is missing, the right or the grant.
574
-
575
- The call goes out for real: ${(0, ui_1.bold)('POST')} and ${(0, ui_1.bold)('DELETE')} change things in
502
+ connections: `${(0, ui_1.bold)('xflow connections')}: accounts connected to the organization
503
+
504
+ Somebody signs in to Yandex Metrika, Bitrix or a mail service once, in the platform,
505
+ and that account becomes a connection of the organization. Linked to a project, it
506
+ hands its credentials to the cloud functions as environment variables. Nobody has to
507
+ paste a token into the repository, and nobody sees the value: it goes straight from
508
+ the platform into the function.
509
+
510
+ xflow connections what this project can use, and what it already uses
511
+ xflow connections link <name> --as ALIAS
512
+ hand its credentials to the functions
513
+ xflow connections unlink <ALIAS> take them away again
514
+ xflow connections call <name> <path>
515
+ ask that API something, once, without deploying
516
+
517
+ Every row says whether the connection is linked to this project (its alias), which of
518
+ its fields a call can fill in (${(0, ui_1.bold)('{CLIENT_ID}')} and the like) and what state the
519
+ access is in. ${(0, ui_1.bold)('available, not linked')} is the useful one: the account exists in
520
+ the organization, but this project gets nothing from it yet: link it.
521
+
522
+ Both commands take the name of the connection, the first column of the list, and both
523
+ also take its identifier. ${(0, ui_1.bold)('unlink')} takes the alias as well, and prefers it: the
524
+ alias is the name your own code already uses. One organization can hold several accounts
525
+ of the same service under one name; then the command prints their identifiers and asks
526
+ for one of those instead of guessing.
527
+
528
+ ${(0, ui_1.bold)('unlink')} refuses while a function still reads one of the variables and names
529
+ those functions; ${(0, ui_1.bold)('--yes')} goes through anyway. Seeing them needs the sources,
530
+ so under ${(0, ui_1.bold)('--project')} there is no such check: the link goes, and the variables
531
+ that go with it are printed instead. Both commands take the flag, because an account
532
+ linked to a service project without a folder has to be removable the same way.
533
+
534
+ Variables are named after the alias: an OAuth connection called ${(0, ui_1.bold)('YANDEX_METRIKA')}
535
+ gives ${(0, ui_1.bold)('YANDEX_METRIKA_TOKEN')}, a key-based one gives a variable per field. They
536
+ arrive at a function on its next deploy, like every other variable, so after a change
537
+ run ${(0, ui_1.bold)('xflow deploy functions')}. A token close to expiry is renewed by that deploy on the
538
+ way; a revoked one is not renewed by anything until a human reconnects the account.
539
+
540
+ Linking needs a right on the key, ${(0, ui_1.bold)('connections:link')}. Keys are issued with it,
541
+ and a person can take it away in the platform settings, where the key permissions live; a key cannot give
542
+ it back to itself. Losing it is what the refusal says, and the answer is to ask a person,
543
+ not to look for another route. Either way only the accounts granted to you personally can
544
+ be linked at all: that rule holds whatever the key is allowed to do.
545
+
546
+ Connecting a new account and switching one off stay with a person, in the platform
547
+ interface. There is no command for either.
548
+
549
+ ${(0, ui_1.bold)('call')} answers the question an API only answers by being asked: what does this
550
+ endpoint actually return, and is the field you need in it. The platform attaches the
551
+ credentials of the connected account and makes the request itself, so nothing has to be
552
+ deployed to find out, and the key never reaches you.
553
+
554
+ xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
555
+ xflow connections call AMO /api/v4/leads --method POST --data '{"name":"Test"}'
556
+ xflow connections call OZON /v1/product/list --method POST --data '{}' \\
557
+ --header '{"Client-Id":"{CLIENT_ID}"}'
558
+
559
+ The path is written after the address of the connector, query string and all, or given in
560
+ full as ${(0, ui_1.bold)('https://...')}. Only the hosts of that connector are reachable, and a
561
+ refusal lists them. ${(0, ui_1.bold)('--method')} defaults to GET, or to POST when there is a body.
562
+
563
+ ${(0, ui_1.bold)('{NAME}')} in the path or in a header value is filled in by the platform from the
564
+ fields of the connection. That is how the two-header APIs are called: Ozon wants
565
+ ${(0, ui_1.bold)('Client-Id')} beside its key, Yandex Direct wants ${(0, ui_1.bold)('Client-Login')} for an
566
+ agency account, and you never see those values. Which names a connection has is the fourth
567
+ column of ${(0, ui_1.bold)('xflow connections')}, printed in braces and the same whether the account
568
+ is linked or not. Do not derive them from ${(0, ui_1.bold)('xflow env')}: that list is the variables
569
+ of a link, and it names both more and less than this one. A name the connection does not
570
+ have is left in the text as you typed it and the request still goes out, so an invented
571
+ name reaches somebody's live account as text.
572
+
573
+ The answer is printed head first, at most a few kilobytes, and it says so when there is
574
+ more. Ask for the shape, not the volume: one day and ${(0, ui_1.bold)('limit=1')} tells you what the
575
+ fields are called, while the whole dataset belongs in a cloud function. ${(0, ui_1.bold)('--json')}
576
+ prints the envelope for a pipe: ${(0, ui_1.bold)("--json | jq '.response.body | keys'")} gives the
577
+ field names without the body ever entering your context.
578
+
579
+ An account linked to this project is called by its alias. One that is only available is
580
+ called by its name, and no linking is needed: what the command asks is that you could have
581
+ linked it yourself, which needs the ${(0, ui_1.bold)('connections:link')} right and the account being
582
+ granted to you personally. Either way nothing widens: linked credentials already reach the
583
+ functions of this project, and an account you may link you may also read by deploying a
584
+ function into it. An account granted to somebody else stays out of reach, and the refusal
585
+ says which of the two is missing, the right or the grant.
586
+
587
+ The call goes out for real: ${(0, ui_1.bold)('POST')} and ${(0, ui_1.bold)('DELETE')} change things in
576
588
  somebody's live account, and no flag will undo that.`,
577
- schedules: `${(0, ui_1.bold)('xflow schedules')}: running functions on a timer
578
-
579
- A schedule is a Yandex timer trigger: it calls the function itself, with no
580
- application and no browser involved.
581
-
582
- xflow schedules set report "0 3 ? * * *" every day at 03:00
583
- xflow schedules set digest "0 */4 ? * * *" every 4 hours
584
- xflow schedules rm report take off every schedule of a function
585
-
586
- Six fields: ${(0, ui_1.bold)('minutes hours day-of-month month day-of-week year')}. Exactly one of
587
- the day-of-month and day-of-week fields has to be ${(0, ui_1.bold)('?')}, which is not our quirk but
588
- how Yandex works. ${(0, ui_1.bold)('The time is always UTC')}, local time is not understood.
589
-
590
- --payload '{"mode":"full"}' the body the function will receive
591
- --payload-file body.json the same body from a file
592
-
593
- ${(0, ui_1.bold)('list')} prints that body next to the schedule, so what a timer sends can be read
594
- back without setting it again.
595
-
596
- A scheduled run arrives at the handler as a POST with ${(0, ui_1.bold)("event.xflow.caller === 'schedule'")}:
597
- the envelope is signed by the platform, so such an event cannot be forged from outside. A
598
- function without callers that only a schedule wakes is a workflow, and a project without
599
- apps is made of those. The function has to be deployed already, since the schedule points
600
- at it and not the other way round.
601
-
602
- A failed run shows up in ${(0, ui_1.bold)('xflow functions logs')}: nobody is watching a scheduled
589
+ schedules: `${(0, ui_1.bold)('xflow schedules')}: running functions on a timer
590
+
591
+ A schedule is a Yandex timer trigger: it calls the function itself, with no
592
+ application and no browser involved.
593
+
594
+ xflow schedules set report "0 3 ? * * *" every day at 03:00
595
+ xflow schedules set digest "0 */4 ? * * *" every 4 hours
596
+ xflow schedules rm report take off every schedule of a function
597
+
598
+ Six fields: ${(0, ui_1.bold)('minutes hours day-of-month month day-of-week year')}. Exactly one of
599
+ the day-of-month and day-of-week fields has to be ${(0, ui_1.bold)('?')}, which is not our quirk but
600
+ how Yandex works. ${(0, ui_1.bold)('The time is always UTC')}, local time is not understood.
601
+
602
+ --payload '{"mode":"full"}' the body the function will receive
603
+ --payload-file body.json the same body from a file
604
+
605
+ ${(0, ui_1.bold)('list')} prints that body next to the schedule, so what a timer sends can be read
606
+ back without setting it again.
607
+
608
+ A scheduled run arrives at the handler as a POST with ${(0, ui_1.bold)("event.xflow.caller === 'schedule'")}:
609
+ the envelope is signed by the platform, so such an event cannot be forged from outside. A
610
+ function without callers that only a schedule wakes is a workflow, and a project without
611
+ apps is made of those. The function has to be deployed already, since the schedule points
612
+ at it and not the other way round.
613
+
614
+ A failed run shows up in ${(0, ui_1.bold)('xflow functions logs')}: nobody is watching a scheduled
603
615
  function, so the wrapper reports a crash the same way it does on an ordinary call.`,
604
- functions: `${(0, ui_1.bold)('xflow functions')}: what is deployed, and calling it
605
-
606
- xflow functions list name, state, callers, open, keys, last call and error
607
- xflow functions invoke <name> call one as yourself, through the platform
608
- xflow functions logs [name] crashes: stack and console output
609
-
610
- Functions belong to the project and ship on their own step: ${(0, ui_1.bold)('xflow deploy functions')}
611
- sends the project root and deploys everything in ${(0, ui_1.bold)('functions/<name>/index.ts')}. Every app
612
- of the project runs the new code the moment it finishes; there is no publish step for them.
613
-
614
- Who may call a function is its callers, in the root xflow.json:
615
-
616
- { "projectId": "…", "functions": { "book-slot": { "callers": ["zapis"] } } }
617
-
618
- A caller is an app folder of this project (apps/zapis is "zapis"); pages are static and call
619
- nothing. A function nobody lists has no callers: a workflow, woken by a schedule, by a service
620
- holding a key a person issued, or by ${(0, ui_1.bold)('invoke')}. ${(0, ui_1.bold)('open')} in the list means a caller is an app
621
- anyone with the link opens, so anyone on the internet can call it. The address in the note
622
- under the table is for webhooks and other functions, with a key; apps call ${(0, ui_1.bold)('/_fn/<name>')} on
623
- their own address.
624
-
625
- ${(0, ui_1.bold)('invoke')} calls it as the person who owns the key: the platform checks the role, gives
626
- the function a one-minute ticket and calls it itself, so the function sees
627
- ${(0, ui_1.bold)("event.xflow.caller === 'platform'")} and the person in ${(0, ui_1.bold)('event.xflow.user')}.
628
-
629
- --data '{"a":1}' request body (the method becomes POST by default)
630
- --data-file body.json the same body from a file
631
- --method GET a different method
632
-
633
- The body has to be JSON, and it is checked before the call goes out: PowerShell
634
- strips double quotes from a native call, so ${(0, ui_1.bold)('--data')} needs them escaped there
635
- (${(0, ui_1.bold)('--data \'{\\"a\\":1}\'')}) and a file needs nothing.
636
-
637
- Prints the body of the answer and nothing else, so it goes straight into a pipe
638
- (${(0, ui_1.bold)('| jq')}, ${(0, ui_1.bold)('| python')}). A non-zero exit code on 4xx and 5xx: in CI such a call has to fail the step. A
639
- refusal of the guard names its reason in the body (${(0, ui_1.bold)('not_bound')}, ${(0, ui_1.bold)('no_access')},
616
+ functions: `${(0, ui_1.bold)('xflow functions')}: what is deployed, and calling it
617
+
618
+ xflow functions list name, state, callers, open, keys, last call and error
619
+ xflow functions invoke <name> call one as yourself, through the platform
620
+ xflow functions logs [name] crashes: stack and console output
621
+
622
+ Functions belong to the project and ship on their own step: ${(0, ui_1.bold)('xflow deploy functions')}
623
+ sends the project root and deploys everything in ${(0, ui_1.bold)('functions/<name>/index.ts')}. Every app
624
+ of the project runs the new code the moment it finishes; there is no publish step for them.
625
+
626
+ Who may call a function is its callers, in the root xflow.json:
627
+
628
+ { "projectId": "…", "functions": { "book-slot": { "callers": ["zapis"] } } }
629
+
630
+ A caller is an app folder of this project (apps/zapis is "zapis"); pages are static and call
631
+ nothing. A function nobody lists has no callers: a workflow, woken by a schedule, by a service
632
+ holding a key a person issued, or by ${(0, ui_1.bold)('invoke')}. ${(0, ui_1.bold)('open')} in the list means a caller is an app
633
+ anyone with the link opens, so anyone on the internet can call it. The address in the note
634
+ under the table is for webhooks and other functions, with a key; apps call ${(0, ui_1.bold)('/_fn/<name>')} on
635
+ their own address.
636
+
637
+ ${(0, ui_1.bold)('invoke')} calls it as the person who owns the key: the platform checks the role, gives
638
+ the function a one-minute ticket and calls it itself, so the function sees
639
+ ${(0, ui_1.bold)("event.xflow.caller === 'platform'")} and the person in ${(0, ui_1.bold)('event.xflow.user')}.
640
+
641
+ --data '{"a":1}' request body (the method becomes POST by default)
642
+ --data-file body.json the same body from a file
643
+ --method GET a different method
644
+
645
+ The body has to be JSON, and it is checked before the call goes out: PowerShell
646
+ strips double quotes from a native call, so ${(0, ui_1.bold)('--data')} needs them escaped there
647
+ (${(0, ui_1.bold)('--data \'{\\"a\\":1}\'')}) and a file needs nothing.
648
+
649
+ Prints the body of the answer and nothing else, so it goes straight into a pipe
650
+ (${(0, ui_1.bold)('| jq')}, ${(0, ui_1.bold)('| python')}). A non-zero exit code on 4xx and 5xx: in CI such a call has to fail the step. A
651
+ refusal of the guard names its reason in the body (${(0, ui_1.bold)('not_bound')}, ${(0, ui_1.bold)('no_access')},
640
652
  ${(0, ui_1.bold)('functions_stopped')}); for a crash the hint points at ${(0, ui_1.bold)('xflow functions logs <name>')}.`,
641
- skills: `${(0, ui_1.bold)('xflow skills')}: platform instructions for an AI agent
642
-
643
- An agent has no way to know about XFlow: the platform is not in its training. The
644
- instructions explain how to release and publish, where the design system components
645
- come from, and where to look for production errors. ${(0, ui_1.bold)('init')} and ${(0, ui_1.bold)('link')} lay them
646
- down on their own for the common agents, so this command exists to pick tools
647
- precisely. Refreshing after a CLI update is not your job any more: the skill and the
648
- CLI ship as one version, and ${(0, ui_1.bold)('xflow update')} rewrites the copies you already have.
649
-
650
- Without flags it installs for Claude Code, Cursor and Codex in this project (globally:
651
- Claude Code, Codex and the shared folder) and refreshes every copy already installed.
652
-
653
- xflow skills list the agents, their folders and what is installed
654
- --agent claude,cursor exact agents instead of the defaults
655
- --global the home folder instead of the project
656
- --refresh rewrite the copies that exist, create none
657
-
658
- The format is shared (agentskills.io) and inside a project most tools read the shared
659
- .agents/skills folder, so the paths differ mostly in the home directory. Cursor also
660
- gets .cursor/rules/xflow.mdc: its older versions do not read the shared format.
661
-
662
- Plus a few pointer lines in ${(0, ui_1.bold)('AGENTS.md')}. A skill is picked up lazily, only when its
663
- description matches the task, and "add a customers table" will not trigger it. AGENTS.md
664
- is always read by the agent, which is why the pointer is appended there, at the end of
653
+ skills: `${(0, ui_1.bold)('xflow skills')}: platform instructions for an AI agent
654
+
655
+ An agent has no way to know about XFlow: the platform is not in its training. The
656
+ instructions explain how to release and publish, where the design system components
657
+ come from, and where to look for production errors. ${(0, ui_1.bold)('init')} and ${(0, ui_1.bold)('link')} lay them
658
+ down on their own for the common agents, so this command exists to pick tools
659
+ precisely. Refreshing after a CLI update is not your job any more: the skill and the
660
+ CLI ship as one version, and ${(0, ui_1.bold)('xflow update')} rewrites the copies you already have.
661
+
662
+ Without flags it installs for Claude Code, Cursor and Codex in this project (globally:
663
+ Claude Code, Codex and the shared folder) and refreshes every copy already installed.
664
+
665
+ xflow skills list the agents, their folders and what is installed
666
+ --agent claude,cursor exact agents instead of the defaults
667
+ --global the home folder instead of the project
668
+ --refresh rewrite the copies that exist, create none
669
+
670
+ The format is shared (agentskills.io) and inside a project most tools read the shared
671
+ .agents/skills folder, so the paths differ mostly in the home directory. Cursor also
672
+ gets .cursor/rules/xflow.mdc: its older versions do not read the shared format.
673
+
674
+ Plus a few pointer lines in ${(0, ui_1.bold)('AGENTS.md')}. A skill is picked up lazily, only when its
675
+ description matches the task, and "add a customers table" will not trigger it. AGENTS.md
676
+ is always read by the agent, which is why the pointer is appended there, at the end of
665
677
  the file and once. ${(0, ui_1.bold)('CLAUDE.md')} is left alone: Claude Code reads .claude/skills anyway.`,
666
- dev: `${(0, ui_1.bold)('xflow dev')}: run an app on this machine
667
-
668
- Runs ${(0, ui_1.bold)('npm run dev')} in the app folder it stands in (127.0.0.1:3000) with the access key
669
- of the organization that holds the project. Every ${(0, ui_1.bold)('/_fn/<name>')} call of the app goes to
670
- the platform as this app, so the callers, the access and the person calling are the same
671
- as on its address. Nothing about functions is kept on this machine: a function someone
672
- else shipped answers here at once.
673
-
674
- Use it instead of a bare ${(0, ui_1.bold)('npm run dev')}: that one is handed no key and no app id, so
675
- its calls to functions fail. The platform address comes from the CLI too, so the key goes
676
- to the platform that issued it and nowhere else. It never reaches the browser.
677
-
678
- The server listens on 127.0.0.1 and nowhere else, because it holds that key: on a
679
- shared network, anything else would let a neighbour call the functions of the project
678
+ dev: `${(0, ui_1.bold)('xflow dev')}: run an app on this machine
679
+
680
+ Runs ${(0, ui_1.bold)('npm run dev')} in the app folder it stands in (127.0.0.1:3000) with the access key
681
+ of the organization that holds the project. Every ${(0, ui_1.bold)('/_fn/<name>')} call of the app goes to
682
+ the platform as this app, so the callers, the access and the person calling are the same
683
+ as on its address. Nothing about functions is kept on this machine: a function someone
684
+ else shipped answers here at once.
685
+
686
+ Use it instead of a bare ${(0, ui_1.bold)('npm run dev')}: that one is handed no key and no app id, so
687
+ its calls to functions fail. The platform address comes from the CLI too, so the key goes
688
+ to the platform that issued it and nowhere else. It never reaches the browser.
689
+
690
+ The server listens on 127.0.0.1 and nowhere else, because it holds that key: on a
691
+ shared network, anything else would let a neighbour call the functions of the project
680
692
  as you. A phone on the same Wi-Fi therefore cannot open it either.`,
681
- pull: `${(0, ui_1.bold)('xflow pull')} [functions|apps/<folder>]: fetch the sources
682
-
683
- Every app and the project root are separate histories, and pull fetches one of them: in an
684
- app folder that app, in the project root the root (apps/ stays as it is), or the one named.
685
- It refuses to write into a non-empty folder: the platform cannot merge changes, that is
686
- git's job.
687
-
688
- --into <dir> unpack alongside, to compare; in the project root with nothing named
689
- it fetches the whole project there
690
- --revision <N> a specific revision of that history (xflow deployments lists them)
693
+ pull: `${(0, ui_1.bold)('xflow pull')} [functions|apps/<folder>]: fetch the sources
694
+
695
+ Every app and the project root are separate histories, and pull fetches one of them: in an
696
+ app folder that app, in the project root the root (apps/ stays as it is), or the one named.
697
+ It refuses to write into a non-empty folder: the platform cannot merge changes, that is
698
+ git's job.
699
+
700
+ --into <dir> unpack alongside, to compare; in the project root with nothing named
701
+ it fetches the whole project there
702
+ --revision <N> a specific revision of that history (xflow deployments lists them)
691
703
  --force overwrite the folder completely`,
692
- deploy: `${(0, ui_1.bold)('xflow deploy')}: deploy the functions or build an app, then publish
693
-
694
- xflow deploy functions -m "…" send the project root, check it, apply the new
695
- migrations, ship the functions and their callers
696
- xflow deploy apps/<folder> -m "…" send one app and build it; inside its folder a plain
697
- xflow deploy does the same
698
- xflow publish [build] point the app address at the newest build, or a named one
699
- xflow rollback <build> the same, the number required
700
- xflow deployments [functions] the builds of the app, or the deploys of the functions
701
-
702
- In the project root a plain ${(0, ui_1.bold)('xflow deploy')} refuses and names the targets: functions and
703
- apps never go in one run, because a change of functions reaches every app at once.
704
-
705
- -m <text> the version comment, required: one short line in the language the
706
- user speaks, saying what changed (--message is the long form)
707
- --revision <N> run a revision the platform already holds: that is how the functions
708
- come back (xflow deployments functions lists them)
709
- --project <id> with --revision only: the functions of that project, no folder needed
710
- --app <id> with --revision only: that app, no folder needed
711
- --force allow overwriting the server revision while sending
712
- --allow-removals agree in advance to remove the functions gone from the sources
713
-
714
- What goes up: an app sends its own folder; the project root sends everything but apps/,
715
- plus the apps/<folder>/xflow.json files its callers are read from. Never sent: node_modules,
716
- .git, dist, build, .next, any .env, plus everything listed in .xflowignore and in the
717
- ignore field of xflow.json.
718
-
719
- If the server holds a revision newer than the one you worked from, the sources are
720
- rejected and the build does not start. That means somebody deployed before you: fetch
721
- their changes alongside (${(0, ui_1.bold)('xflow pull --into ./server-copy')}), merge them in git on
722
- your side, and retry; or build what the server holds as it is with ${(0, ui_1.bold)('--revision <N>')}, the
723
- refusal names the number. A folder with no record of the server revision (a fresh clone, a CI
724
- checkout) is not rejected: its first deploy goes on top of the current revision and says
725
- so, the check applies from the next one. ${(0, ui_1.bold)('--force')} sends this folder over the server revision
726
- anyway. The overwritten revision is not lost at once: it stays in the history while it is among
727
- the last 10, and the deploy prints the ${(0, ui_1.bold)('xflow pull --revision <N>')} line that brings it back.
728
-
729
- The platform builds, in a clean sandbox on one Node version for everybody. Before that the
730
- sources are checked: what breaks silently (the platform files, the calls in the entry point,
731
- keys in the code, alert instead of a dialog) is printed as a list and nothing starts. What is
732
- merely unfinished (the template page title, no icon) goes through and prints a line
733
- ${(0, ui_1.bold)('Unfinished:')}. Fix it before telling the person the app is done.
734
-
735
- ${(0, ui_1.bold)('deploy functions')} first applies the new migrations of ${(0, ui_1.bold)('migrations/')}: if one fails or
736
- would destroy data, nothing ships, and ${(0, ui_1.bold)('xflow db migrate')} is where that gets settled. Then it
737
- bundles everything in ${(0, ui_1.bold)('functions/<name>/index.ts')}. A function whose
738
- code and variables did not change is left alone, and one that fails to ship fails the whole
739
- deploy. It then records the callers and prints which functions are open to the whole
740
- internet now. A change that touches data and screens goes in this order: the functions
741
- (with the migration), then every app that shows it; an app deploy warns when the root holds
742
- changes nobody deployed.
743
-
744
- A function gone from the sources would be deleted from the cloud along with its keys and
745
- schedules, and that is final. So the deploy does not start at all: it names the functions and stops, and ${(0, ui_1.bold)('--allow-removals')}
746
- is the way to agree. If the sources hold no functions at all while the cloud holds several, nothing
747
- is deleted and nothing is asked: that looks like a directory which never made it (a
748
- ${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
749
-
750
- A build of an app is not shown to visitors until ${(0, ui_1.bold)('xflow publish')}: publishing points the
751
- app address at a build and takes a second, and an older kept build is the rollback. Only
752
- the app moves: the functions and the database are one per project and stay as they are.
753
- The very first build of a new app publishes itself. The files of the three latest builds
754
- are kept; ${(0, ui_1.bold)('xflow deployments')} marks the rest "no pages". Give a person the app address
704
+ deploy: `${(0, ui_1.bold)('xflow deploy')}: deploy the functions or build an app, then publish
705
+
706
+ xflow deploy functions -m "…" send the project root, check it, apply the new
707
+ migrations, ship the functions and their callers
708
+ xflow deploy apps/<folder> -m "…" send one app and build it; inside its folder a plain
709
+ xflow deploy does the same
710
+ xflow publish [build] point the app address at the newest build, or a named one
711
+ xflow rollback <build> the same, the number required
712
+ xflow deployments [functions] the builds of the app, or the deploys of the functions
713
+
714
+ In the project root a plain ${(0, ui_1.bold)('xflow deploy')} refuses and names the targets: functions and
715
+ apps never go in one run, because a change of functions reaches every app at once.
716
+
717
+ -m <text> the version comment, required: one short line in the language the
718
+ user speaks, saying what changed (--message is the long form)
719
+ --revision <N> run a revision the platform already holds: that is how the functions
720
+ come back (xflow deployments functions lists them)
721
+ --project <id> with --revision only: the functions of that project, no folder needed
722
+ --app <id> with --revision only: that app, no folder needed
723
+ --force allow overwriting the server revision while sending
724
+ --allow-removals agree in advance to remove the functions gone from the sources
725
+
726
+ What goes up: an app sends its own folder; the project root sends everything but apps/,
727
+ plus the apps/<folder>/xflow.json files its callers are read from. Never sent: node_modules,
728
+ .git, dist, build, .next, any .env, plus everything listed in .xflowignore and in the
729
+ ignore field of xflow.json.
730
+
731
+ If the server holds a revision newer than the one you worked from, the sources are
732
+ rejected and the build does not start. That means somebody deployed before you: fetch
733
+ their changes alongside (${(0, ui_1.bold)('xflow pull --into ./server-copy')}), merge them in git on
734
+ your side, and retry; or build what the server holds as it is with ${(0, ui_1.bold)('--revision <N>')}, the
735
+ refusal names the number. A folder with no record of the server revision (a fresh clone, a CI
736
+ checkout) is not rejected: its first deploy goes on top of the current revision and says
737
+ so, the check applies from the next one. ${(0, ui_1.bold)('--force')} sends this folder over the server revision
738
+ anyway. The overwritten revision is not lost at once: it stays in the history while it is among
739
+ the last 10, and the deploy prints the ${(0, ui_1.bold)('xflow pull --revision <N>')} line that brings it back.
740
+
741
+ The platform builds, in a clean sandbox on one Node version for everybody. Before that the
742
+ sources are checked: what breaks silently (the platform files, the calls in the entry point,
743
+ keys in the code, alert instead of a dialog) is printed as a list and nothing starts. What is
744
+ merely unfinished (the template page title, no icon) goes through and prints a line
745
+ ${(0, ui_1.bold)('Unfinished:')}. Fix it before telling the person the app is done.
746
+
747
+ ${(0, ui_1.bold)('deploy functions')} first applies the new migrations of ${(0, ui_1.bold)('migrations/')}: if one fails or
748
+ would destroy data, nothing ships, and ${(0, ui_1.bold)('xflow db migrate')} is where that gets settled. Then it
749
+ bundles everything in ${(0, ui_1.bold)('functions/<name>/index.ts')}. A function whose
750
+ code and variables did not change is left alone, and one that fails to ship fails the whole
751
+ deploy. It then records the callers and prints which functions are open to the whole
752
+ internet now. A change that touches data and screens goes in this order: the functions
753
+ (with the migration), then every app that shows it; an app deploy warns when the root holds
754
+ changes nobody deployed.
755
+
756
+ A function gone from the sources would be deleted from the cloud along with its keys and
757
+ schedules, and that is final. So the deploy does not start at all: it names the functions and stops, and ${(0, ui_1.bold)('--allow-removals')}
758
+ is the way to agree. If the sources hold no functions at all while the cloud holds several, nothing
759
+ is deleted and nothing is asked: that looks like a directory which never made it (a
760
+ ${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
761
+
762
+ A build of an app is not shown to visitors until ${(0, ui_1.bold)('xflow publish')}: publishing points the
763
+ app address at a build and takes a second, and an older kept build is the rollback. Only
764
+ the app moves: the functions and the database are one per project and stay as they are.
765
+ The very first build of a new app publishes itself. The files of the three latest builds
766
+ are kept; ${(0, ui_1.bold)('xflow deployments')} marks the rest "no pages". Give a person the app address
755
767
  the CLI prints, and name builds by their number.`,
756
- init: `${(0, ui_1.bold)('xflow init')} [dir]: a new project with its first app
757
-
758
- Creates the project on the platform and lays out its folder: the root xflow.json, a
759
- package.json for the functions, empty ${(0, ui_1.bold)('functions/')} and ${(0, ui_1.bold)('migrations/')}, and the first app in
760
- apps/<folder> from the platform template: React on Vite, Tailwind, components in
761
- ${(0, ui_1.bold)('src/components/ui')} and blocks in ${(0, ui_1.bold)('src/components/blocks')}. Those are a starting set:
762
- change them, restyle them or delete what you do not use.
763
-
764
- --name <name> project name (the folder name by default)
765
- --app <folder> the folder of the first app (made from the name by default)
766
- --no-app a project without apps: its functions are workflows
767
- --server <name> the database of the project, right away on that server:
768
- platform, or an own server of the organization (xflow db list)
769
-
770
- More apps later: ${(0, ui_1.bold)('xflow apps add <folder>')}. If writing the files fails, the project stays
768
+ init: `${(0, ui_1.bold)('xflow init')} [dir]: a new project with its first app
769
+
770
+ Creates the project on the platform and lays out its folder: the root xflow.json, a
771
+ package.json for the functions, empty ${(0, ui_1.bold)('functions/')} and ${(0, ui_1.bold)('migrations/')}, and the first app in
772
+ apps/<folder> from the platform template: React on Vite, Tailwind, components in
773
+ ${(0, ui_1.bold)('src/components/ui')} and blocks in ${(0, ui_1.bold)('src/components/blocks')}. Those are a starting set:
774
+ change them, restyle them or delete what you do not use.
775
+
776
+ --name <name> project name (the folder name by default)
777
+ --app <folder> the folder of the first app (made from the name by default)
778
+ --no-app a project without apps: its functions are workflows
779
+ --server <name> the database of the project, right away on that server:
780
+ platform, or an own server of the organization (xflow db list)
781
+
782
+ More apps later: ${(0, ui_1.bold)('xflow apps add <folder>')}. If writing the files fails, the project stays
771
783
  on the platform and the CLI says how to pick it up with the link command.`,
772
784
  };