@getxflow/cli 1.0.7 → 1.0.9
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/bin.js +2 -1
- package/dist/commands/functions.js +1 -2
- package/dist/flags.js +1 -1
- package/dist/help.js +2 -3
- package/dist/session.js +8 -0
- package/dist/version.js +1 -1
- package/package.json +24 -24
- package/skills/xflow/SKILL.md +24 -7
package/dist/bin.js
CHANGED
|
@@ -309,7 +309,8 @@ async function main() {
|
|
|
309
309
|
const args = (0, args_1.parseArgs)(process.argv.slice(2));
|
|
310
310
|
// Decided here rather than in each command: the header of --project, the nudges and
|
|
311
311
|
// every progress line are written on the way to a payload that has to stand alone.
|
|
312
|
-
|
|
312
|
+
// An invoke is always such a payload: its answer goes straight into a pipe.
|
|
313
|
+
(0, ui_1.setQuiet)((0, args_1.flagBool)(args, 'json') || (args.words[0] === 'functions' && args.words[1] === 'invoke'));
|
|
313
314
|
if ((0, args_1.flagBool)(args, 'version')) {
|
|
314
315
|
(0, ui_1.out)(version_1.CLI_VERSION);
|
|
315
316
|
return 0;
|
|
@@ -50,9 +50,8 @@ async function functionsInvoke(args) {
|
|
|
50
50
|
body: { method, ...(data !== undefined && method !== 'GET' && method !== 'HEAD' ? { body: data } : {}) },
|
|
51
51
|
timeoutMs: 15 * 60_000,
|
|
52
52
|
});
|
|
53
|
-
(0, ui_1.note)((0, ui_1.dim)(`${answer.status} in ${answer.elapsedMs} ms`));
|
|
54
53
|
if (answer.body)
|
|
55
|
-
(0, ui_1.out)(
|
|
54
|
+
(0, ui_1.out)(prettyBody(answer.body));
|
|
56
55
|
if (answer.status >= 400)
|
|
57
56
|
throw new errors_1.CliError(`The function answered ${answer.status}`, answer.hint);
|
|
58
57
|
}
|
package/dist/flags.js
CHANGED
|
@@ -57,7 +57,7 @@ exports.KNOWN = {
|
|
|
57
57
|
'mcp install': ['show-token'],
|
|
58
58
|
functions: ['project'],
|
|
59
59
|
'functions list': ['project'],
|
|
60
|
-
'functions invoke': ['data', 'data-file', '
|
|
60
|
+
'functions invoke': ['data', 'data-file', 'method', 'project'],
|
|
61
61
|
'functions logs': ['limit', 'project'],
|
|
62
62
|
logs: ['limit', 'project', 'app'],
|
|
63
63
|
env: ['project'],
|
package/dist/help.js
CHANGED
|
@@ -634,14 +634,13 @@ ${(0, ui_1.bold)("event.xflow.caller === 'platform'")} and the person in ${(0, u
|
|
|
634
634
|
--data '{"a":1}' request body (the method becomes POST by default)
|
|
635
635
|
--data-file body.json the same body from a file
|
|
636
636
|
--method GET a different method
|
|
637
|
-
--json the answer of the function alone, exactly as it came
|
|
638
637
|
|
|
639
638
|
The body has to be JSON, and it is checked before the call goes out: PowerShell
|
|
640
639
|
strips double quotes from a native call, so ${(0, ui_1.bold)('--data')} needs them escaped there
|
|
641
640
|
(${(0, ui_1.bold)('--data \'{\\"a\\":1}\'')}) and a file needs nothing.
|
|
642
641
|
|
|
643
|
-
Prints the
|
|
644
|
-
|
|
642
|
+
Prints the body of the answer and nothing else, so it goes straight into a pipe
|
|
643
|
+
(${(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
|
|
645
644
|
refusal of the guard names its reason in the body (${(0, ui_1.bold)('not_bound')}, ${(0, ui_1.bold)('no_access')},
|
|
646
645
|
${(0, ui_1.bold)('functions_stopped')}); for a crash the hint points at ${(0, ui_1.bold)('xflow functions logs <name>')}.`,
|
|
647
646
|
mcp: `${(0, ui_1.bold)('xflow mcp install')}: connect the agent to the platform directly
|
package/dist/session.js
CHANGED
|
@@ -55,12 +55,18 @@ function anonymous(config) {
|
|
|
55
55
|
*/
|
|
56
56
|
async function findOrgFor(apiUrl, path) {
|
|
57
57
|
const dead = [];
|
|
58
|
+
let frozen = null;
|
|
58
59
|
for (const candidate of (0, credentials_1.listOrgs)(apiUrl)) {
|
|
59
60
|
try {
|
|
60
61
|
await (0, api_1.apiJson)({ apiUrl, token: candidate.token }, path);
|
|
61
62
|
return { hit: candidate, dead };
|
|
62
63
|
}
|
|
63
64
|
catch (e) {
|
|
65
|
+
// A frozen organization refuses every path, so it may or may not hold this one.
|
|
66
|
+
if (e instanceof api_1.ApiError && e.limit?.code === 'subscription_blocked') {
|
|
67
|
+
frozen ??= e;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
64
70
|
if (e instanceof api_1.ApiError && (e.status === 401 || e.status === 403)) {
|
|
65
71
|
dead.push(candidate);
|
|
66
72
|
continue;
|
|
@@ -70,6 +76,8 @@ async function findOrgFor(apiUrl, path) {
|
|
|
70
76
|
throw e;
|
|
71
77
|
}
|
|
72
78
|
}
|
|
79
|
+
if (frozen)
|
|
80
|
+
throw frozen;
|
|
73
81
|
return { hit: null, dead };
|
|
74
82
|
}
|
|
75
83
|
function findProjectOrg(apiUrl, projectId) {
|
package/dist/version.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
|
|
4
4
|
/** Keep in sync with cli/package.json. */
|
|
5
|
-
exports.CLI_VERSION = '1.0.
|
|
5
|
+
exports.CLI_VERSION = '1.0.9';
|
|
6
6
|
/** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
|
|
7
7
|
exports.DEFAULT_API_URL = 'https://app.getxflow.com';
|
package/package.json
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@getxflow/cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"engines": {
|
|
7
|
-
"node": ">=20"
|
|
8
|
-
},
|
|
9
|
-
"bin": {
|
|
10
|
-
"xflow": "dist/bin.js"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"dist",
|
|
14
|
-
"skills"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
18
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
20
|
-
},
|
|
21
|
-
"publishConfig": {
|
|
22
|
-
"access": "public"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@getxflow/cli",
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"xflow": "dist/bin.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"skills"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
18
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/skills/xflow/SKILL.md
CHANGED
|
@@ -23,7 +23,7 @@ contains the fix.
|
|
|
23
23
|
|
|
24
24
|
## Keeping these instructions current
|
|
25
25
|
|
|
26
|
-
These instructions ship with xflow CLI 1.0.
|
|
26
|
+
These instructions ship with xflow CLI 1.0.9, so the copy you are reading can be older than
|
|
27
27
|
the CLI answering your commands.
|
|
28
28
|
|
|
29
29
|
When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
|
|
@@ -121,6 +121,10 @@ halfway through the work into a sentence said before it starts.
|
|
|
121
121
|
deleting it.
|
|
122
122
|
- Function minutes are shared by the whole organization: every app, page and schedule spends
|
|
123
123
|
them. When they run out, every function of the organization stops (`functions_stopped`).
|
|
124
|
+
- An unpaid plan stops the functions at once, and five days later freezes the organization:
|
|
125
|
+
every command except `xflow whoami`, and every MCP tool, answers `subscription_blocked`,
|
|
126
|
+
apps and pages show a "suspended" page. Nothing is deleted and no code fixes it: tell the
|
|
127
|
+
person that the owner pays the invoice in the organization settings, section «Тариф» (Plan).
|
|
124
128
|
- The cap on how long one function call may run is written into the function when it is
|
|
125
129
|
deployed, so a plan change reaches the cloud only with the next `xflow deploy functions` of
|
|
126
130
|
each project, in both directions.
|
|
@@ -240,8 +244,8 @@ A person sets this in the web interface; you cannot, and you should not ask for
|
|
|
240
244
|
- The plan decides which levels exist: on Free an app opens only inside xflow, Start adds the
|
|
241
245
|
direct address and "Anyone with the link".
|
|
242
246
|
- At the closed level the platform signs people in, and a function receives the person in
|
|
243
|
-
`event.xflow.user` (`id`, `role` in the organization, `name`
|
|
244
|
-
(doctor, receptionist) in your own table keyed by `user.id`.
|
|
247
|
+
`event.xflow.user` (`id`, `role` in the organization, `name`, `access` to the project).
|
|
248
|
+
Keep roles inside the app (doctor, receptionist) in your own table keyed by `user.id`.
|
|
245
249
|
- People outside the organization (patients, pickers, clients) are not xflow accounts: make the
|
|
246
250
|
app "Anyone with the link" and give it its own sign-in, see below.
|
|
247
251
|
- At "Anyone with the link" the platform signs nobody in: `event.xflow.user` is null for
|
|
@@ -373,8 +377,9 @@ A function whose code and variables did not change is not shipped again, and one
|
|
|
373
377
|
ship fails the whole deploy.
|
|
374
378
|
|
|
375
379
|
Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it as you,
|
|
376
|
-
through the platform, and prints
|
|
377
|
-
body.json`, `--method GET
|
|
380
|
+
through the platform, and prints the body alone, ready for a pipe (`--data '{"a":1}'`, `--data-file
|
|
381
|
+
body.json`, `--method GET`; 4xx and 5xx exit non-zero; Windows PowerShell 5.1 prepends a BOM when
|
|
382
|
+
piping to another program, so read it with `ConvertFrom-Json` or Python's `utf-8-sig`); `xflow functions logs <name>` shows
|
|
378
383
|
the crashes and 5xx answers with stacks and console output, the latest 10 (`--limit N` for more).
|
|
379
384
|
Only failed calls are logged. Empty output does not clear a function that fails at module start,
|
|
380
385
|
runs over its time cap or out of memory: those never reach the logs, `invoke` shows them.
|
|
@@ -395,8 +400,8 @@ exports.handler = async (event) => {
|
|
|
395
400
|
// project), 'external' (an outside service with a key), 'schedule',
|
|
396
401
|
// 'platform' (a person through xflow functions invoke or the web interface)
|
|
397
402
|
// slug: the address name of the calling app (<id>--<org>) or page, null otherwise
|
|
398
|
-
// user: { id, role, name } of a signed-in person,
|
|
399
|
-
//
|
|
403
|
+
// user: { id, role, name, access } of a signed-in person, null for anonymous
|
|
404
|
+
// visitors, pages, keys. access: 'view' | 'edit' on this project
|
|
400
405
|
// ip: the visitor's address, null for a schedule
|
|
401
406
|
}
|
|
402
407
|
```
|
|
@@ -404,6 +409,18 @@ exports.handler = async (event) => {
|
|
|
404
409
|
Never trust an identity that arrives in the body or in a header of the request: the page lives
|
|
405
410
|
on someone else's computer. `event.xflow` is the only identity the platform stands behind.
|
|
406
411
|
|
|
412
|
+
Project access is separate from the role: a person who only views the project (`access:
|
|
413
|
+
'view'`) still reaches every function. A function that writes refuses them:
|
|
414
|
+
|
|
415
|
+
```js
|
|
416
|
+
if (event.xflow.user && event.xflow.user.access !== 'edit') {
|
|
417
|
+
return { statusCode: 403, body: JSON.stringify({ error: 'Read-only access' }) }
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
To remember who did something, store `user.id` and `user.name` in the row: the name is never
|
|
422
|
+
empty, and the app has no other way to look up other people.
|
|
423
|
+
|
|
407
424
|
A function that changes data should say who may call it instead of checking the role by hand:
|
|
408
425
|
|
|
409
426
|
```js
|