@getxflow/cli 0.13.0 → 0.14.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/dist/api.js CHANGED
@@ -12,21 +12,20 @@ class ApiError extends Error {
12
12
  code;
13
13
  status;
14
14
  hint;
15
- /** Check violations, e.g. why a build was rejected. */
16
15
  issues;
17
- /** Set when a plan limit was hit. */
18
16
  limit;
19
- /** Cloud functions a build would delete, with code `confirm_required`. */
20
17
  removing;
21
- constructor(message, code, status, hint, issues, limit, removing) {
18
+ request;
19
+ constructor(message, code, status, hint, details = {}) {
22
20
  super(message);
23
21
  this.name = 'ApiError';
24
22
  this.code = code;
25
23
  this.status = status;
26
24
  this.hint = hint;
27
- this.issues = issues;
28
- this.limit = limit;
29
- this.removing = removing;
25
+ this.issues = details.issues;
26
+ this.limit = details.limit;
27
+ this.removing = details.removing;
28
+ this.request = details.request;
30
29
  }
31
30
  }
32
31
  exports.ApiError = ApiError;
@@ -127,7 +126,7 @@ async function readError(response, client) {
127
126
  const text = await response.text().catch(() => '');
128
127
  try {
129
128
  const parsed = JSON.parse(text);
130
- return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed.issues, parsed.limit, parsed.removing);
129
+ return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed);
131
130
  }
132
131
  catch {
133
132
  return notAnAnswer(response.status, client.apiUrl, text);
@@ -164,9 +164,6 @@ function head(text) {
164
164
  /**
165
165
  * Response headers worth a line each: rate limits, the type, and where a redirect points.
166
166
  * A CDN adds two dozen more, and on every call that is noise.
167
- *
168
- * Request headers are not filtered: there are a few, they are yours, and seeing them is how
169
- * a `{NAME}` the connection does not have shows itself.
170
167
  */
171
168
  function shownHeaders(headers) {
172
169
  const wanted = (name) => name === 'content-type' || name === 'retry-after' || name === 'location' || name.startsWith('x-ratelimit');
@@ -174,8 +171,18 @@ function shownHeaders(headers) {
174
171
  .filter(([name]) => wanted(name.toLowerCase()))
175
172
  .map(([name, value]) => `${name}: ${value}`);
176
173
  }
177
- function allHeaders(headers) {
178
- return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
174
+ /**
175
+ * The request as it went out, printed on the answer and on a refusal alike. On a refusal it
176
+ * is the whole point: the host of a connector comes from the connection, not from what was
177
+ * typed, so "check the address" is unfollowable while the address is nowhere on screen.
178
+ *
179
+ * Request headers are not filtered: there are a few, they are yours, and seeing them is how
180
+ * a {NAME} the connection does not have shows itself.
181
+ */
182
+ function showRequest(request) {
183
+ (0, ui_1.note)((0, ui_1.dim)(` ${request.method} ${request.url}`));
184
+ for (const [name, value] of Object.entries(request.headers))
185
+ (0, ui_1.note)((0, ui_1.dim)(` ${name}: ${value}`));
179
186
  }
180
187
  /** Headers typed as JSON. Values may carry {NAME} placeholders, the platform fills those in. */
181
188
  function headerArg(args) {
@@ -211,10 +218,10 @@ function looksJson(headers) {
211
218
  * credentials. For looking at what an API returns without deploying a function for it.
212
219
  */
213
220
  async function connectionsCall(args) {
214
- const alias = args.words[1];
221
+ const connection = args.words[1];
215
222
  const path = args.words[2];
216
- if (!alias) {
217
- throw new errors_1.CliError('A connection is required', 'Its alias in this project: xflow connections');
223
+ if (!connection) {
224
+ throw new errors_1.CliError('A connection is required', 'Its alias in this project, or its name from the first column of xflow connections');
218
225
  }
219
226
  if (!path) {
220
227
  throw new errors_1.CliError('A path is required', 'For example: xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01');
@@ -223,36 +230,42 @@ async function connectionsCall(args) {
223
230
  const body = (0, json_arg_1.bodyArg)(args, 'data');
224
231
  // Judged before anything goes over the network, the same as in functions invoke: a broken
225
232
  // body is the caller's own line. A form body is not judged at all, it is text by design.
226
- if (body && looksJson(headers)) {
227
- const problem = (0, json_arg_1.jsonProblem)(body.text);
228
- if (problem) {
229
- throw body.fromFile
230
- ? new errors_1.CliError(`${body.fromFile} is not valid JSON: ${problem}`, 'The file has to hold one JSON value')
231
- : new errors_1.CliError(`--data is not valid JSON: ${body.text}`, (0, json_arg_1.jsonHint)('data', body.text));
232
- }
233
- }
233
+ if (body && looksJson(headers))
234
+ (0, json_arg_1.assertJson)('data', body);
234
235
  const { projectId, client } = await (0, session_1.projectTarget)(args);
235
- const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/connections/call`, {
236
- method: 'POST',
237
- body: {
238
- connection: alias,
239
- path,
240
- method: (0, args_1.flagString)(args, 'method') ?? (body ? 'POST' : 'GET'),
241
- ...(body ? { body: body.text } : {}),
242
- ...(headers ? { headers } : {}),
243
- },
244
- // Past the platform's own cap on the call, so a slow API reads as a slow API and not as
245
- // an unreachable platform.
246
- timeoutMs: 60_000,
247
- });
236
+ let data;
237
+ try {
238
+ data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/connections/call`, {
239
+ method: 'POST',
240
+ body: {
241
+ connection,
242
+ path,
243
+ method: (0, args_1.flagString)(args, 'method') ?? (body ? 'POST' : 'GET'),
244
+ ...(body ? { body: body.text } : {}),
245
+ ...(headers ? { headers } : {}),
246
+ },
247
+ // Past the platform's own cap on the call, so a slow API reads as a slow API and not as
248
+ // an unreachable platform.
249
+ timeoutMs: 60_000,
250
+ });
251
+ }
252
+ catch (e) {
253
+ // Before the refusal itself, in the order of the answer: what went out, then what came of
254
+ // it. Only the other side's refusals carry it, the rest never assembled an address.
255
+ if (e instanceof api_1.ApiError && e.request)
256
+ showRequest(e.request);
257
+ throw e;
258
+ }
248
259
  if ((0, args_1.flagBool)(args, 'json')) {
249
260
  (0, ui_1.out)(JSON.stringify(data, null, 2));
250
261
  return;
251
262
  }
252
263
  const { request, response } = data;
253
- (0, ui_1.note)((0, ui_1.dim)(` ${request.method} ${request.url}`));
254
- for (const line of allHeaders(request.headers))
255
- (0, ui_1.note)((0, ui_1.dim)(` ${line}`));
264
+ // Named the account only when it was not called by its alias: with several accounts of one
265
+ // service under the same name, the alias is the only thing the caller already knew.
266
+ if (!data.alias)
267
+ (0, ui_1.note)((0, ui_1.dim)(` ${data.label} (${data.connection_id}), not linked to this project`));
268
+ showRequest(request);
256
269
  const size = response.truncated ? `at least ${(0, ui_1.formatBytes)(response.bytes)}` : (0, ui_1.formatBytes)(response.bytes);
257
270
  (0, ui_1.note)((0, ui_1.dim)(` ${response.status} in ${data.elapsed_ms} ms, ${size}`));
258
271
  for (const line of shownHeaders(response.headers))
package/dist/help.js CHANGED
@@ -254,7 +254,7 @@ About destructive ones. The platform keeps no database history and makes no back
254
254
  so ${(0, ui_1.bold)('DROP TABLE')}, ${(0, ui_1.bold)('DROP COLUMN')}, ${(0, ui_1.bold)('TRUNCATE')} and ${(0, ui_1.bold)('DELETE FROM')} without a
255
255
  condition need two things at once: this flag, and the right to destroy data on the
256
256
  access key. That right is off by default, and only its owner turns it on, in the
257
- platform settings under Developers: a flag is something an agent adds by itself, a
257
+ platform settings, where the key permissions live: a flag is something an agent adds by itself, a
258
258
  right is not. With both in place, the contents of the affected tables are dumped and
259
259
  kept for 7 days: that is a "caught it right away" safety net, not a backup.
260
260
  ${(0, ui_1.bold)('DROP DATABASE')} is never allowed, because the database is shared across the
@@ -321,8 +321,8 @@ Dot entries and symbolic links are skipped, and ${(0, ui_1.bold)('.xflowignore')
321
321
  those are the rules for the sources, and a media folder is exactly what one is
322
322
  asked to put there so it stays out of the archive.
323
323
 
324
- ${(0, ui_1.bold)('rm')} needs a right of its own (${(0, ui_1.bold)('Delete files')} in the Developers section of the
325
- platform), off by default. The reason is the neighbourhood: whatever the users of
324
+ ${(0, ui_1.bold)('rm')} needs a right of its own (${(0, ui_1.bold)('Delete files')}, where the key permissions live in
325
+ the platform settings), off by default. The reason is the neighbourhood: whatever the users of
326
326
  the application uploaded lives in the same folders, and there is no undo. A folder
327
327
  first answers with the number of files it holds and deletes only on ${(0, ui_1.bold)('--yes')}.
328
328
 
@@ -367,7 +367,7 @@ the platform into the function.
367
367
  xflow connections link <name> --as ALIAS
368
368
  hand its credentials to the functions
369
369
  xflow connections unlink <ALIAS> take them away again
370
- xflow connections call <ALIAS> <path>
370
+ xflow connections call <name> <path>
371
371
  ask that API something, once, without deploying
372
372
 
373
373
  Every row says whether the connection is linked to this project (its alias) and what
@@ -390,7 +390,7 @@ run ${(0, ui_1.bold)('xflow deploy')}. A token close to expiry is renewed by any
390
390
  a revoked one is not renewed by anything until a human reconnects the account.
391
391
 
392
392
  Linking needs a right on the key, ${(0, ui_1.bold)('connections:link')}. Keys are issued with it,
393
- and a person can take it away in the platform settings under Developers; a key cannot give
393
+ and a person can take it away in the platform settings, where the key permissions live; a key cannot give
394
394
  it back to itself. Losing it is what the refusal says, and the answer is to ask a person,
395
395
  not to look for another route. Either way only the accounts granted to you personally can
396
396
  be linked at all: that rule holds whatever the key is allowed to do.
@@ -426,11 +426,16 @@ fields are called, while the whole dataset belongs in a cloud function. ${(0, ui
426
426
  prints the envelope for a pipe: ${(0, ui_1.bold)("--json | jq '.response.body | keys'")} gives the
427
427
  field names without the body ever entering your context.
428
428
 
429
- Only an account already linked to this project can be called, because its credentials
430
- already reach the functions of that project: the command saves a deploy, it does not widen
431
- what you may touch. An account that is merely available has to be linked first, and that is
432
- a separate right and a personal grant. The call also goes out for real: ${(0, ui_1.bold)('POST')} and
433
- ${(0, ui_1.bold)('DELETE')} change things in somebody's live account, and no flag will undo that.`,
429
+ An account linked to this project is called by its alias. One that is only available is
430
+ called by its name, and no linking is needed: what the command asks is that you could have
431
+ linked it yourself, which needs the ${(0, ui_1.bold)('connections:link')} right and the account being
432
+ granted to you personally. Either way nothing widens: linked credentials already reach the
433
+ functions of this project, and an account you may link you may also read by deploying a
434
+ function into it. An account granted to somebody else stays out of reach, and the refusal
435
+ says which of the two is missing, the right or the grant.
436
+
437
+ The call goes out for real: ${(0, ui_1.bold)('POST')} and ${(0, ui_1.bold)('DELETE')} change things in
438
+ somebody's live account, and no flag will undo that.`,
434
439
  schedules: `${(0, ui_1.bold)('xflow schedules')}: running functions on a timer
435
440
 
436
441
  A schedule is a Yandex timer trigger: it calls the function itself, with no
package/dist/json-arg.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.jsonProblem = jsonProblem;
4
4
  exports.jsonHint = jsonHint;
5
5
  exports.bodyArg = bodyArg;
6
+ exports.assertJson = assertJson;
6
7
  exports.jsonArg = jsonArg;
7
8
  const node_fs_1 = require("node:fs");
8
9
  const args_1 = require("./args");
@@ -30,15 +31,19 @@ function jsonProblem(raw) {
30
31
  return e instanceof Error ? e.message : String(e);
31
32
  }
32
33
  }
33
- /** Where to look, and on a stripped value the shell is the first suspect. */
34
+ /**
35
+ * Where to look, and on a stripped value the shell is the first suspect.
36
+ *
37
+ * No file is offered here: not every JSON flag has that twin, and --header used to be sent
38
+ * to --header-file, which the flag check refuses one line later. A hint has to survive being
39
+ * followed, so the offer lives in assertJson, the one path that reads a file.
40
+ */
34
41
  function jsonHint(flag, raw) {
35
- const file = `--${flag}-file body.json`;
36
42
  if (quotesStripped(raw.trim())) {
37
43
  return (`Not one double quote survived: the shell removed them, PowerShell does this to ` +
38
- `native calls. Escape them: --${flag} '{\\"key\\":\\"value\\"}', or keep the body ` +
39
- `in a file: ${file}`);
44
+ `native calls. Escape them: --${flag} '{\\"key\\":\\"value\\"}'`);
40
45
  }
41
- return `A single JSON value is expected, or keep the body in a file: ${file}`;
46
+ return 'A single JSON value is expected';
42
47
  }
43
48
  /**
44
49
  * The body from --<flag> or --<flag>-file, whatever it is.
@@ -65,15 +70,23 @@ function bodyArg(args, flag) {
65
70
  throw new errors_1.CliError(`--${flag} needs a value`, `For example: --${flag} '{"mode":"full"}'`);
66
71
  return { text: String(inline).trim(), fromFile: null };
67
72
  }
73
+ /**
74
+ * Refuse a body that is not JSON. Takes what bodyArg returned, and only this path names
75
+ * --<flag>-file: a flag read through bodyArg has that twin by construction.
76
+ */
77
+ function assertJson(flag, body) {
78
+ const problem = jsonProblem(body.text);
79
+ if (!problem)
80
+ return;
81
+ throw body.fromFile
82
+ ? new errors_1.CliError(`${body.fromFile} is not valid JSON: ${problem}`, 'The file has to hold one JSON value')
83
+ : new errors_1.CliError(`--${flag} is not valid JSON: ${body.text}`, `${jsonHint(flag, body.text)}, or keep the body in a file: --${flag}-file body.json`);
84
+ }
68
85
  /** The body from --<flag> or --<flag>-file, already known to be JSON. */
69
86
  function jsonArg(args, flag) {
70
87
  const body = bodyArg(args, flag);
71
88
  if (body === undefined)
72
89
  return undefined;
73
- const problem = jsonProblem(body.text);
74
- if (!problem)
75
- return body.text;
76
- throw body.fromFile
77
- ? new errors_1.CliError(`${body.fromFile} is not valid JSON: ${problem}`, 'The file has to hold one JSON value')
78
- : new errors_1.CliError(`--${flag} is not valid JSON: ${body.text}`, jsonHint(flag, body.text));
90
+ assertJson(flag, body);
91
+ return body.text;
79
92
  }
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.13.0';
5
+ exports.CLI_VERSION = '0.14.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.13.0",
3
+ "version": "0.14.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.13.0. They travel inside the package, so the copy
27
+ These instructions ship with xflow CLI 0.14.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
 
@@ -364,8 +364,9 @@ naming rules and the flags. Unlink refuses while a function still reads one of t
364
364
  variables and names those functions, so read that list before reaching for `--force`.
365
365
 
366
366
  Linking needs the `connections:link` right on the key, and only accounts granted to the
367
- owner of the key personally can be linked at all. If the right was taken away, say so and
368
- ask the person to turn it back on in the platform settings under Developers: a key cannot
367
+ owner of the key personally can be linked at all; the same right covers calling an unlinked
368
+ account. If the right was taken away, say so and
369
+ ask the person to turn it back on where the key permissions live in the platform settings: a key cannot
369
370
  grant it to itself. Connecting a new account and switching one off stay with a person too.
370
371
 
371
372
  ### Asking an API what it returns
@@ -374,15 +375,20 @@ Before writing a function against an unfamiliar API, find out what it actually a
374
375
 
375
376
  ```
376
377
  xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
378
+ xflow connections call "Яндекс Метрика" /management/v1/counters
377
379
  xflow connections call OZON /v1/product/list --method POST --data '{}' \
378
380
  --header '{"Client-Id":"{CLIENT_ID}"}'
379
381
  ```
380
382
 
381
- The platform attaches the credentials and makes the request itself, so no deploy is needed
382
- and the key never reaches you. Only an account already linked to this project can be called:
383
- its credentials already reach that project's functions, so this saves a deploy rather than
384
- widening what you may touch. An account that is only `available, not linked` has to be linked
385
- first, and that stays a decision for a person.
383
+ The platform attaches the credentials and makes the request itself, so no deploy is needed and
384
+ the key never reaches you. A linked account is called by its alias, an `available, not linked`
385
+ one by its name: **linking first is not needed to look**, so do not ask a person about that.
386
+
387
+ **Calling is looking, not wiring**: no variable reaches the functions because you called
388
+ something. To use the account in code, link it and deploy.
389
+
390
+ Two refusals do need a person, and different ones: `connections:link` right — the owner of the
391
+ key grants it where the key permissions live; `not granted to you` — ask whoever connected the account.
386
392
 
387
393
  `{NAME}` in the path or in a header value is filled in from the fields of the connection: the
388
394
  names are what `xflow env` shows without the alias in front, so `OZON_CLIENT_ID` is written
@@ -442,8 +448,8 @@ fills those in itself, and your value under one of them would shadow the real on
442
448
  The platform keeps no database history and no backups, so anything that destroys data
443
449
  (`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) needs two
444
450
  things at once: the `--allow-destructive` flag, and the right to destroy data on the key.
445
- That right is off by default and only its owner turns it on, under Developers in the
446
- platform settings. When the refusal is about the right rather than the flag, adding the flag
451
+ That right is off by default and only its owner turns it on, where the key permissions live in
452
+ the platform settings. When the refusal is about the right rather than the flag, adding the flag
447
453
  changes nothing: say what needs deleting and why, and let the person decide. With both in
448
454
  place the affected tables are dumped first and kept for 7 days. Check with `--dry-run` first.
449
455
 
@@ -482,8 +488,8 @@ whose size differs. An address belongs to the record rather than to the bytes, s
482
488
  replacement keeps it and the links in your code and tables keep working. Put those addresses
483
489
  into the code or into a table: there is no command that fetches files back to a machine.
484
490
 
485
- Deleting needs a right of its own, off by default (**Delete files** in the Developers
486
- section of the platform), so `xflow storage rm` may answer that the key was not granted it.
491
+ Deleting needs a right of its own, off by default (**Delete files**, where the key permissions
492
+ live in the platform settings), so `xflow storage rm` may answer that the key was not granted it.
487
493
  Ask the person to switch it on, and do not look for a way around: whatever the users of the
488
494
  application uploaded lives in the same folders, and there is no undo. A folder first tells
489
495
  you how many files it holds and deletes them only with `--yes`.