@getxflow/cli 1.2.0 → 1.2.2

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