@getxflow/cli 0.10.6 → 0.11.1
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/README.md +1 -1
- package/dist/args.js +2 -1
- package/dist/commands/auth.js +6 -0
- package/dist/commands/db.js +5 -6
- package/dist/commands/deploy.js +28 -4
- package/dist/commands/functions.js +6 -2
- package/dist/flags.js +1 -1
- package/dist/help.js +6 -2
- package/dist/limits.js +7 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +38 -11
package/README.md
CHANGED
package/dist/args.js
CHANGED
|
@@ -27,7 +27,8 @@ function parseArgs(argv) {
|
|
|
27
27
|
const words = [];
|
|
28
28
|
const flags = {};
|
|
29
29
|
for (let i = 0; i < argv.length; i++) {
|
|
30
|
-
|
|
30
|
+
// -m is what git taught everyone to type for a message; honour it as an alias.
|
|
31
|
+
const token = argv[i] === '-m' ? '--message' : argv[i];
|
|
31
32
|
if (token === '--') {
|
|
32
33
|
words.push(...argv.slice(i + 1));
|
|
33
34
|
break;
|
package/dist/commands/auth.js
CHANGED
|
@@ -158,5 +158,11 @@ async function whoami() {
|
|
|
158
158
|
? `yes, no more often than one run every ${interval} min`
|
|
159
159
|
: 'yes'
|
|
160
160
|
: 'no'}`);
|
|
161
|
+
if (typeof me.features.function_timeout_s === 'number') {
|
|
162
|
+
(0, ui_1.out)(`Function time cap: ${me.features.function_timeout_s} s per call, applied on the next build`);
|
|
163
|
+
}
|
|
164
|
+
if (typeof me.features.builds_concurrent === 'number') {
|
|
165
|
+
(0, ui_1.out)(`Concurrent builds: ${me.features.builds_concurrent}`);
|
|
166
|
+
}
|
|
161
167
|
}
|
|
162
168
|
}
|
package/dist/commands/db.js
CHANGED
|
@@ -177,14 +177,13 @@ async function dbQuery(args) {
|
|
|
177
177
|
}
|
|
178
178
|
(0, ui_1.table)([data.columns, ...data.rows.map((row) => data.columns.map((column) => cell(row[column])))]);
|
|
179
179
|
if (data.truncated) {
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// the flag
|
|
183
|
-
// says so only while there is room left.
|
|
180
|
+
// No exact total here: the platform stops reading at limit + 1 rows, so all
|
|
181
|
+
// it knows is that more exist. The "raise --limit" advice shows only while
|
|
182
|
+
// the flag still has room below the ceiling.
|
|
184
183
|
const room = data.limit !== undefined && data.limit_max !== undefined && data.limit < data.limit_max;
|
|
185
184
|
(0, ui_1.note)((0, ui_1.dim)(room
|
|
186
|
-
? ` Shown ${data.rows.length}
|
|
187
|
-
: ` Shown ${data.rows.length}
|
|
185
|
+
? ` Shown the first ${data.rows.length} rows, more exist: raise --limit (up to ${data.limit_max}), or narrow the query`
|
|
186
|
+
: ` Shown the first ${data.rows.length} rows, more exist: narrow the query, with a where clause or an offset`));
|
|
188
187
|
}
|
|
189
188
|
}
|
|
190
189
|
async function dbMigrate(args) {
|
package/dist/commands/deploy.js
CHANGED
|
@@ -72,12 +72,12 @@ async function refreshFunctionsEnv(root, client, projectId) {
|
|
|
72
72
|
* refuses such a build until the caller says yes, and the question is asked
|
|
73
73
|
* here, before the build starts, rather than reported once it already has.
|
|
74
74
|
*/
|
|
75
|
-
async function startBuild(client, projectId, revision, allowRemovals) {
|
|
75
|
+
async function startBuild(client, projectId, revision, allowRemovals, comment) {
|
|
76
76
|
const path = `/api/v1/projects/${projectId}/builds`;
|
|
77
77
|
try {
|
|
78
78
|
return await (0, api_1.apiJson)(client, path, {
|
|
79
79
|
method: 'POST',
|
|
80
|
-
body: { revision, allow_removals: allowRemovals },
|
|
80
|
+
body: { revision, allow_removals: allowRemovals, comment },
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
catch (e) {
|
|
@@ -104,7 +104,7 @@ async function startBuild(client, projectId, revision, allowRemovals) {
|
|
|
104
104
|
throw new errors_1.CliError('The build did not start', 'Nothing was removed');
|
|
105
105
|
return (0, api_1.apiJson)(client, path, {
|
|
106
106
|
method: 'POST',
|
|
107
|
-
body: { revision, allow_removals: true },
|
|
107
|
+
body: { revision, allow_removals: true, comment },
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -120,7 +120,29 @@ async function startBuild(client, projectId, revision, allowRemovals) {
|
|
|
120
120
|
async function assertBuildAllowed(client, projectId) {
|
|
121
121
|
await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/builds?check=plan`);
|
|
122
122
|
}
|
|
123
|
+
/** Keep in sync with the builds route on the platform. */
|
|
124
|
+
const MAX_COMMENT_LENGTH = 200;
|
|
125
|
+
/** Control and bidi-override characters: they spoof terminals, not comments. */
|
|
126
|
+
const COMMENT_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
|
|
127
|
+
/**
|
|
128
|
+
* The version comment, checked before anything goes up: a refusal after the push
|
|
129
|
+
* would leave the project with a revision newer than its build for no good reason.
|
|
130
|
+
*/
|
|
131
|
+
function requireComment(args) {
|
|
132
|
+
const comment = ((0, args_1.flagString)(args, 'message') ?? '')
|
|
133
|
+
.replace(/\s+/g, ' ')
|
|
134
|
+
.replace(COMMENT_CONTROL_CHARS, '')
|
|
135
|
+
.trim();
|
|
136
|
+
if (!comment) {
|
|
137
|
+
throw new errors_1.CliError('Every version needs a comment saying what changed in it', 'Add -m: xflow deploy -m "Cart and card payments". One short line, written in the language the user speaks: it names this version in the history');
|
|
138
|
+
}
|
|
139
|
+
if (comment.length > MAX_COMMENT_LENGTH) {
|
|
140
|
+
throw new errors_1.CliError(`The version comment is too long: ${MAX_COMMENT_LENGTH} characters at most`, 'One short line about what changed in this version is enough');
|
|
141
|
+
}
|
|
142
|
+
return comment;
|
|
143
|
+
}
|
|
123
144
|
async function deploy(args) {
|
|
145
|
+
const comment = requireComment(args);
|
|
124
146
|
const { root, config } = (0, config_1.requireProject)();
|
|
125
147
|
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
126
148
|
let revision;
|
|
@@ -137,7 +159,7 @@ async function deploy(args) {
|
|
|
137
159
|
revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
138
160
|
}
|
|
139
161
|
(0, ui_1.step)(`Building on the platform from revision ${revision}`);
|
|
140
|
-
const started = await startBuild(client, config.projectId, revision, (0, args_1.flagBool)(args, 'allow-removals'));
|
|
162
|
+
const started = await startBuild(client, config.projectId, revision, (0, args_1.flagBool)(args, 'allow-removals'), comment);
|
|
141
163
|
if (started.removed_functions?.length) {
|
|
142
164
|
(0, ui_1.warn)(`Removed from the cloud, gone from the sources: ${started.removed_functions.join(', ')}`);
|
|
143
165
|
}
|
|
@@ -213,6 +235,8 @@ async function deployments(args) {
|
|
|
213
235
|
d.revision !== null ? `revision ${d.revision}` : 'no revision',
|
|
214
236
|
d.status === 'deployed' ? 'ready' : d.status,
|
|
215
237
|
(0, ui_1.formatAge)(d.deployed_at ?? d.created_at),
|
|
238
|
+
// Plain text only: a padded cell with ANSI codes would skew every column after it.
|
|
239
|
+
d.comment && d.comment.length > 60 ? `${d.comment.slice(0, 59)}…` : (d.comment ?? ''),
|
|
216
240
|
[d.is_dev ? (0, ui_1.bold)('dev') : '', d.is_live ? (0, ui_1.bold)('live') : ''].filter(Boolean).join(' '),
|
|
217
241
|
]));
|
|
218
242
|
(0, ui_1.note)((0, ui_1.dim)(' Serve the pages of an earlier build: xflow rollback <number>'));
|
|
@@ -67,6 +67,10 @@ async function functionsInvoke(args) {
|
|
|
67
67
|
catch {
|
|
68
68
|
(0, ui_1.note)((0, ui_1.dim)(' Could not get a visitor pass: calling with the project token only'));
|
|
69
69
|
}
|
|
70
|
+
// The cap the deployed version actually carries, not the plan's current one:
|
|
71
|
+
// they diverge until the next build. Null means deployed before the platform
|
|
72
|
+
// recorded caps, that is with 90.
|
|
73
|
+
const capS = fn.execution_timeout_s ?? 90;
|
|
70
74
|
const started = Date.now();
|
|
71
75
|
let response;
|
|
72
76
|
try {
|
|
@@ -78,8 +82,8 @@ async function functionsInvoke(args) {
|
|
|
78
82
|
...(pass ? { 'X-Project-Pass': pass } : {}),
|
|
79
83
|
},
|
|
80
84
|
body: sendsBody ? (data ?? '{}') : undefined,
|
|
81
|
-
// Wait past the function's own
|
|
82
|
-
signal: AbortSignal.timeout(
|
|
85
|
+
// Wait past the function's own cap to see its timeout, not ours.
|
|
86
|
+
signal: AbortSignal.timeout((capS + 10) * 1000),
|
|
83
87
|
});
|
|
84
88
|
}
|
|
85
89
|
catch (e) {
|
package/dist/flags.js
CHANGED
|
@@ -73,7 +73,7 @@ const KNOWN = {
|
|
|
73
73
|
'storage remove': ['folder', 'yes', 'project'],
|
|
74
74
|
status: [],
|
|
75
75
|
pull: ['into', 'force', 'revision'],
|
|
76
|
-
deploy: ['no-push', 'force', 'allow-removals'],
|
|
76
|
+
deploy: ['no-push', 'force', 'allow-removals', 'message'],
|
|
77
77
|
publish: ['project'],
|
|
78
78
|
rollback: ['project'],
|
|
79
79
|
deployments: ['project'],
|
package/dist/help.js
CHANGED
|
@@ -57,7 +57,7 @@ ${(0, ui_1.bold)('Files')}
|
|
|
57
57
|
delete a folder with everything in it
|
|
58
58
|
|
|
59
59
|
${(0, ui_1.bold)('Releasing')}
|
|
60
|
-
xflow deploy
|
|
60
|
+
xflow deploy -m "what changed" send the code, ship the functions, build on the platform
|
|
61
61
|
xflow publish show the dev version to visitors
|
|
62
62
|
xflow rollback <version number> serve the pages of an earlier build
|
|
63
63
|
xflow deployments version history
|
|
@@ -253,7 +253,8 @@ ${(0, ui_1.bold)('console.log')} lines of that call) and sends it to the platfor
|
|
|
253
253
|
write nothing, otherwise every request would pay for it in latency.
|
|
254
254
|
|
|
255
255
|
What never reaches this list: a crash while the module is starting (the function
|
|
256
|
-
never gets as far as the wrapper), going over
|
|
256
|
+
never gets as far as the wrapper), going over the execution time cap of the plan
|
|
257
|
+
(${(0, ui_1.bold)('xflow whoami')} names it), and running out of memory.
|
|
257
258
|
Those show up in the answer to ${(0, ui_1.bold)('xflow functions invoke')}.
|
|
258
259
|
|
|
259
260
|
Browser errors are collected by ${(0, ui_1.bold)('src/utils/error-logger.ts')} of the template and
|
|
@@ -467,6 +468,9 @@ Three steps: sending the sources, shipping the cloud functions, building the app
|
|
|
467
468
|
The build command and the output directory come from xflow.json (npm run build and dist
|
|
468
469
|
by default).
|
|
469
470
|
|
|
471
|
+
-m <text> the version comment, required: one short line in the language the
|
|
472
|
+
user speaks, saying what changed. It names the version in the
|
|
473
|
+
history (--message is the long form)
|
|
470
474
|
--no-push do not send sources, build from the latest server revision
|
|
471
475
|
--force allow overwriting the server revision while sending
|
|
472
476
|
--allow-removals agree in advance to remove the functions gone from the sources
|
package/dist/limits.js
CHANGED
|
@@ -5,8 +5,11 @@ exports.limitLine = limitLine;
|
|
|
5
5
|
exports.quotaRows = quotaRows;
|
|
6
6
|
const DENIAL_LABELS = {
|
|
7
7
|
limit_projects: 'projects',
|
|
8
|
+
limit_databases: 'databases',
|
|
9
|
+
limit_connections: 'active connector connections',
|
|
8
10
|
limit_functions: 'cloud functions',
|
|
9
11
|
limit_builds: 'builds this month',
|
|
12
|
+
limit_builds_concurrent: 'builds running at once',
|
|
10
13
|
limit_seats: 'seats in the organization',
|
|
11
14
|
limit_schedule_interval: 'schedule frequency',
|
|
12
15
|
db_write_locked: 'database volume, writes are off',
|
|
@@ -29,6 +32,8 @@ function limitLine(detail) {
|
|
|
29
32
|
}
|
|
30
33
|
const QUOTA_ORDER = [
|
|
31
34
|
'projects',
|
|
35
|
+
'databases',
|
|
36
|
+
'connections',
|
|
32
37
|
'functions',
|
|
33
38
|
'developers',
|
|
34
39
|
'members',
|
|
@@ -39,6 +44,8 @@ const QUOTA_ORDER = [
|
|
|
39
44
|
];
|
|
40
45
|
const QUOTA_LABELS = {
|
|
41
46
|
projects: 'Projects',
|
|
47
|
+
databases: 'Databases',
|
|
48
|
+
connections: 'Active connections',
|
|
42
49
|
functions: 'Cloud functions',
|
|
43
50
|
developers: 'Developers',
|
|
44
51
|
members: 'Staff',
|
package/dist/version.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
|
|
4
4
|
/** Keep in sync with cli/package.json. */
|
|
5
|
-
exports.CLI_VERSION = '0.
|
|
5
|
+
exports.CLI_VERSION = '0.11.1';
|
|
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
package/skills/xflow/SKILL.md
CHANGED
|
@@ -24,7 +24,7 @@ contains the fix.
|
|
|
24
24
|
|
|
25
25
|
## Keeping these instructions current
|
|
26
26
|
|
|
27
|
-
These instructions ship with xflow CLI 0.
|
|
27
|
+
These instructions ship with xflow CLI 0.11.1. They travel inside the package, so the copy
|
|
28
28
|
you are reading can be older than the CLI answering your commands, and nothing about that
|
|
29
29
|
is visible in the text itself.
|
|
30
30
|
|
|
@@ -65,11 +65,23 @@ the error points away from the cause. The sections below carry the details.
|
|
|
65
65
|
|
|
66
66
|
## Plan limits
|
|
67
67
|
|
|
68
|
-
The organization runs on a plan with finite limits: projects, cloud functions,
|
|
69
|
-
and staff seats, database and file storage,
|
|
70
|
-
|
|
68
|
+
The organization runs on a plan with finite limits: projects, databases, cloud functions,
|
|
69
|
+
active connector connections, developer and staff seats, database and file storage,
|
|
70
|
+
function minutes and builds per month, builds running at once, plus schedules and how
|
|
71
|
+
often they may run. `xflow whoami` prints every one of them next to what is already
|
|
71
72
|
used, and reading it before a long task is cheaper than hitting a wall mid-way.
|
|
72
73
|
|
|
74
|
+
Two of them deserve a word. A database outlives its project (deleting a project only
|
|
75
|
+
detaches it), so a refused database on a small plan usually means an orphan: the fix is
|
|
76
|
+
deleting the unused database in the "Data" section of the web interface, not renaming
|
|
77
|
+
anything. Connector connections are counted while active: switching an unused connection
|
|
78
|
+
off in the web interface frees the seat without deleting it.
|
|
79
|
+
|
|
80
|
+
The plan also caps how long one call of a cloud function may run. The cap is written
|
|
81
|
+
into the function when it is deployed, so a plan change reaches the cloud only with the
|
|
82
|
+
next `xflow deploy` of each project, in both directions. `xflow whoami` names the plan's
|
|
83
|
+
cap; what a deployed function actually carries is what it was last deployed with.
|
|
84
|
+
|
|
73
85
|
The same output names the rights of your own key, which is the other half of the answer:
|
|
74
86
|
destroying data in a migration, deleting files from storage and linking a connected
|
|
75
87
|
account each need a right that is off by default. Reading that line first turns a refusal
|
|
@@ -96,8 +108,10 @@ back within an hour of the data going under the limit.
|
|
|
96
108
|
script, then `npx tsc --noEmit`).
|
|
97
109
|
3. `npm run build` if the change is substantial. The platform builds again in its own
|
|
98
110
|
sandbox, on one Node version for everyone, so this is only a fast way to see errors early.
|
|
99
|
-
4. `xflow deploy` sends the sources, ships the cloud functions and
|
|
100
|
-
on the platform, printing each phase and the six-digit number
|
|
111
|
+
4. `xflow deploy -m "what changed"` sends the sources, ships the cloud functions and
|
|
112
|
+
builds the application on the platform, printing each phase and the six-digit number
|
|
113
|
+
of the version it built. The comment is required: one short line in the language the
|
|
114
|
+
user speaks, it names this version in the history.
|
|
101
115
|
5. Give the user the project link the CLI printed and let them look. Do not open a
|
|
102
116
|
browser for them.
|
|
103
117
|
6. `xflow publish` makes that same version visible to visitors.
|
|
@@ -221,6 +235,13 @@ enforces this, but a project where every function answers its own way costs an a
|
|
|
221
235
|
on every call. Branch the frontend on `error.code`, never on `error.message`: wording
|
|
222
236
|
gets rewritten on any edit, a code does not.
|
|
223
237
|
|
|
238
|
+
How a non-2xx answer reaches the page depends on the generation of `src/lib/xflow.ts`
|
|
239
|
+
in the project, so look at that file before writing the catch: a copy that declares
|
|
240
|
+
`XFlowError` throws it carrying `code` and `status`, and a copy that says nothing about
|
|
241
|
+
errors is older and throws a plain `Error` whose message is all there is. In an older
|
|
242
|
+
project, a function that wants its error text seen on the page answers `200` with
|
|
243
|
+
`{ success: false, error }` in the body.
|
|
244
|
+
|
|
224
245
|
The sources are the whole truth about which functions exist. Delete the directory and the
|
|
225
246
|
next deploy would delete the function from the cloud, schedules included, and that cannot be
|
|
226
247
|
undone: a function created again later gets a different address. So never remove a function
|
|
@@ -361,7 +382,7 @@ The pieces line up in one pass. From a new function to a verified schedule:
|
|
|
361
382
|
```
|
|
362
383
|
xflow env set SMTP_PASSWORD=... # secrets first: values ride the next deploy
|
|
363
384
|
# write functions/report/index.ts, reading process.env.SMTP_PASSWORD literally
|
|
364
|
-
xflow deploy
|
|
385
|
+
xflow deploy -m "Nightly report function" # ships the function, then builds the app
|
|
365
386
|
xflow schedules set report "0 3 ? * * *" --payload '{"mode":"full"}' # needs a deployed function
|
|
366
387
|
xflow functions invoke report # run it once, the way the app would
|
|
367
388
|
xflow functions logs report # empty output means it never crashed
|
|
@@ -467,7 +488,7 @@ for a visitor who is signed in and has access to this project, the same rule tha
|
|
|
467
488
|
application itself, so it works on your pages and does nothing in an email or on a page
|
|
468
489
|
anyone can open.
|
|
469
490
|
|
|
470
|
-
|
|
491
|
+
Four things bite an upload that otherwise looks right:
|
|
471
492
|
|
|
472
493
|
- **A name already taken in that folder is refused before the link is issued.** Pass
|
|
473
494
|
`overwrite: true` to replace the file: the bytes change and the address stays, which is
|
|
@@ -477,11 +498,17 @@ Three things bite an upload that otherwise looks right:
|
|
|
477
498
|
bytes are already up; the platform then removes the object and your table stays clean.
|
|
478
499
|
- **Confirm only after the PUT has finished.** The platform looks the object up in storage
|
|
479
500
|
and takes its real size and content type from there, not from what you declared, so an
|
|
480
|
-
early `confirm` answers that the file is not there. Send on the PUT the
|
|
481
|
-
named when asking for the link:
|
|
501
|
+
early `confirm` answers that the file is not there. Send on the PUT exactly the
|
|
502
|
+
`Content-Type` you named when asking for the link: the link is signed for that header, a
|
|
503
|
+
different or missing type gets 403, and storage serves the file under it.
|
|
504
|
+
- **Storage takes application assets, not code.** Images, documents, data files, audio,
|
|
505
|
+
video, fonts and archives pass; pages, scripts and executables (`html`, `js`, `exe`, …)
|
|
506
|
+
are refused with `forbidden_type` when the link is requested. Both the extension and the
|
|
507
|
+
declared `content_type` are checked. Application code travels through `xflow deploy`, not
|
|
508
|
+
through storage.
|
|
482
509
|
|
|
483
510
|
A refusal comes back as `{ error, code }`. Branch on `code` (`invalid_name`, `file_too_large`,
|
|
484
|
-
`quota_exceeded`, `not_uploaded`, `duplicate_name`, `not_found`, …) and never on the text:
|
|
511
|
+
`quota_exceeded`, `not_uploaded`, `duplicate_name`, `forbidden_type`, `not_found`, …) and never on the text:
|
|
485
512
|
the wording is free to change, the code is not.
|
|
486
513
|
|
|
487
514
|
What the app may do with files is decided inside that function, because the page in the
|