@getxflow/cli 1.5.0 → 1.7.0

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