@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 CHANGED
@@ -7,7 +7,7 @@ npm i -g @getxflow/cli
7
7
  xflow login
8
8
  xflow init my-app
9
9
  cd my-app && npm install
10
- xflow deploy
10
+ xflow deploy -m "First version"
11
11
  xflow publish
12
12
  ```
13
13
 
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
- const token = argv[i];
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;
@@ -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
  }
@@ -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
- // "raise --limit" used to be the whole advice, and for a long time it was
181
- // impossible to follow: the ceiling and the default were the same number, so
182
- // the flag could only ask for less. Now it can ask for more, and the line
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} of ${data.total}: raise --limit (up to ${data.limit_max}), or narrow the query`
187
- : ` Shown ${data.rows.length} of ${data.total}: narrow the query, with a where clause or an offset`));
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) {
@@ -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 90 s cap to see its timeout, not ours.
82
- signal: AbortSignal.timeout(100_000),
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 [--no-push] send the code, ship the functions, build on the platform
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 90 seconds, and running out of memory.
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.10.6';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.10.6",
3
+ "version": "0.11.1",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -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.10.6. They travel inside the package, so the copy
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, developer
69
- and staff seats, database and file storage, function minutes per month, plus schedules and
70
- how often they may run. `xflow whoami` prints every one of them next to what is already
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 builds the application
100
- on the platform, printing each phase and the six-digit number of the version it built.
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 # ships the function, then builds the app
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
- Three things bite an upload that otherwise looks right:
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 `Content-Type` you
481
- named when asking for the link: storage serves the file under the header it received.
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