@getxflow/cli 0.10.2 → 0.10.3

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/args.js CHANGED
@@ -1,22 +1,22 @@
1
1
  "use strict";
2
2
  /** Argument parsing, dependency-free. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.BOOLEAN_FLAGS = void 0;
4
5
  exports.parseArgs = parseArgs;
5
6
  exports.flagString = flagString;
6
7
  exports.flagBool = flagBool;
7
8
  exports.flagNumber = flagNumber;
8
9
  /** Flags that never take a value, so `--force ./dir` keeps the path as a word. */
9
- const BOOLEAN_FLAGS = new Set([
10
+ exports.BOOLEAN_FLAGS = new Set([
10
11
  'force',
11
12
  'yes',
12
13
  'json',
14
+ 'replace',
13
15
  'help',
14
16
  'version',
15
17
  'global',
16
18
  'refresh',
17
- 'live',
18
19
  'no-push',
19
- 'skip-build',
20
20
  'all',
21
21
  'dry-run',
22
22
  'allow-destructive',
@@ -40,7 +40,7 @@ function parseArgs(argv) {
40
40
  continue;
41
41
  }
42
42
  const next = argv[i + 1];
43
- if (BOOLEAN_FLAGS.has(body) || next === undefined || next.startsWith('-')) {
43
+ if (exports.BOOLEAN_FLAGS.has(body) || next === undefined || next.startsWith('-')) {
44
44
  flags[body] = true;
45
45
  }
46
46
  else {
package/dist/bin.js CHANGED
@@ -6,6 +6,7 @@ const args_1 = require("./args");
6
6
  const config_1 = require("./config");
7
7
  const credentials_1 = require("./credentials");
8
8
  const errors_1 = require("./errors");
9
+ const flags_1 = require("./flags");
9
10
  const help_1 = require("./help");
10
11
  const limits_1 = require("./limits");
11
12
  const state_1 = require("./state");
@@ -255,6 +256,7 @@ async function main() {
255
256
  // carries the same advice in its hint.
256
257
  let quiet = args.words[0] === 'update';
257
258
  try {
259
+ (0, flags_1.checkFlags)(args);
258
260
  await run(args);
259
261
  return 0;
260
262
  }
@@ -164,7 +164,10 @@ async function envSet(args) {
164
164
  const value = pair.slice(pair.indexOf('=') + 1);
165
165
  const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env`, {
166
166
  method: 'POST',
167
- body: { name, value, scope: (0, args_1.flagString)(args, 'scope') === 'project' ? 'project' : 'organization' },
167
+ // The word goes to the platform as typed: it knows the two scopes and
168
+ // refuses a third. Guessing here would have made --scope proj mean the
169
+ // whole organization, which is the opposite of what such a typo means.
170
+ body: { name, value, scope: (0, args_1.flagString)(args, 'scope') },
168
171
  });
169
172
  (0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} stored (${result.scope === 'project' ? 'this project only' : 'the whole organization'})`);
170
173
  // Values reach a function on its next deploy.
@@ -6,6 +6,7 @@ const api_1 = require("../api");
6
6
  const args_1 = require("../args");
7
7
  const config_1 = require("../config");
8
8
  const errors_1 = require("../errors");
9
+ const json_arg_1 = require("../json-arg");
9
10
  const session_1 = require("../session");
10
11
  const ui_1 = require("../ui");
11
12
  /** Who can call the function: inside the app only, or an outside service holding a key. */
@@ -46,6 +47,11 @@ async function functionsInvoke(args) {
46
47
  const name = args.words[1];
47
48
  if (!name)
48
49
  throw new errors_1.CliError('A function name is required', 'What is deployed: xflow functions list');
50
+ // The body is judged before anything goes over the network: a broken one is the
51
+ // user's own line, and it costs nothing to say so at once.
52
+ const data = (0, json_arg_1.jsonArg)(args, 'data');
53
+ const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
54
+ const sendsBody = method !== 'GET' && method !== 'HEAD';
49
55
  const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}`);
50
56
  const fn = card.functions.find((item) => item.name === name);
51
57
  if (!fn || !fn.invoke_url) {
@@ -53,9 +59,6 @@ async function functionsInvoke(args) {
53
59
  ? `Deployed: ${card.functions.map((item) => item.name).join(', ')}`
54
60
  : 'Functions ship with the build: xflow deploy');
55
61
  }
56
- const data = (0, args_1.flagString)(args, 'data');
57
- const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
58
- const sendsBody = method !== 'GET' && method !== 'HEAD';
59
62
  // A pass for the person who owns the key: the project token alone is not an
60
63
  // identity, and a project built with the current template refuses without one.
61
64
  // A read-only key cannot get a pass, so the call still goes out without it.
@@ -4,9 +4,9 @@ exports.schedulesList = schedulesList;
4
4
  exports.schedulesSet = schedulesSet;
5
5
  exports.schedulesRemove = schedulesRemove;
6
6
  const api_1 = require("../api");
7
- const args_1 = require("../args");
8
7
  const config_1 = require("../config");
9
8
  const errors_1 = require("../errors");
9
+ const json_arg_1 = require("../json-arg");
10
10
  const session_1 = require("../session");
11
11
  const ui_1 = require("../ui");
12
12
  async function schedulesList() {
@@ -33,9 +33,10 @@ async function schedulesSet(args) {
33
33
  if (!functionName || !cron) {
34
34
  throw new errors_1.CliError('A function name and a schedule are required', 'For example: xflow schedules set nightly-report "0 3 ? * * *" runs every day at 03:00 UTC');
35
35
  }
36
+ const payload = (0, json_arg_1.jsonArg)(args, 'payload') ?? null;
36
37
  const row = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules`, {
37
38
  method: 'POST',
38
- body: { function: functionName, cron, payload: (0, args_1.flagString)(args, 'payload') ?? null },
39
+ body: { function: functionName, cron, payload },
39
40
  });
40
41
  (0, ui_1.ok)(`${(0, ui_1.bold)(row.function)} runs ${row.description}`);
41
42
  (0, ui_1.note)((0, ui_1.dim)(` The time is UTC. To check by hand: xflow functions invoke ${row.function}`));
package/dist/flags.js ADDED
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkFlags = checkFlags;
4
+ const args_1 = require("./args");
5
+ const errors_1 = require("./errors");
6
+ const help_1 = require("./help");
7
+ /**
8
+ * Which flags each command answers to.
9
+ *
10
+ * An unknown flag used to pass in silence, and one written before a word ate that
11
+ * word: `functions logs --nosuch flows` answered for every function instead of the
12
+ * one asked for, and the answer looked truthful. A mistyped switch is worse still:
13
+ * `db migrate --dryrun` applied the migrations for real.
14
+ *
15
+ * Keys are the command as it is typed. A command that is not here is not checked,
16
+ * so a new one keeps working until it is added, and the list also drives the hint:
17
+ * whoever is at the keyboard gets the flags of this very command back.
18
+ */
19
+ const KNOWN = {
20
+ help: [],
21
+ login: [],
22
+ logout: ['all'],
23
+ whoami: [],
24
+ org: [],
25
+ 'org list': [],
26
+ 'org switch': [],
27
+ templates: [],
28
+ init: ['name', 'template', 'database'],
29
+ link: [],
30
+ projects: [],
31
+ 'projects list': [],
32
+ 'projects get': [],
33
+ skills: ['refresh', 'global', 'agent', 'yes'],
34
+ mcp: ['show-token'],
35
+ 'mcp install': ['show-token'],
36
+ functions: [],
37
+ 'functions list': [],
38
+ 'functions invoke': ['data', 'data-file', 'method'],
39
+ 'functions logs': ['limit'],
40
+ logs: ['limit'],
41
+ env: [],
42
+ 'env list': [],
43
+ 'env set': ['scope'],
44
+ 'env rm': [],
45
+ 'env remove': [],
46
+ 'env check': [],
47
+ connections: [],
48
+ 'connections list': [],
49
+ 'connections link': ['as'],
50
+ 'connections unlink': ['force'],
51
+ schedules: [],
52
+ 'schedules list': [],
53
+ 'schedules set': ['payload', 'payload-file'],
54
+ 'schedules rm': [],
55
+ 'schedules remove': [],
56
+ db: [],
57
+ 'db status': [],
58
+ 'db schema': [],
59
+ 'db query': ['limit'],
60
+ 'db migrate': ['dry-run', 'allow-destructive'],
61
+ storage: ['json'],
62
+ 'storage ls': ['json'],
63
+ 'storage list': ['json'],
64
+ 'storage push': ['to', 'replace', 'json'],
65
+ 'storage rm': ['folder', 'yes'],
66
+ 'storage remove': ['folder', 'yes'],
67
+ status: [],
68
+ pull: ['into', 'force', 'revision'],
69
+ deploy: ['no-push', 'force', 'allow-removals'],
70
+ publish: [],
71
+ rollback: [],
72
+ deployments: [],
73
+ update: [],
74
+ };
75
+ /** Answered everywhere: both are handled before the command runs. */
76
+ const GLOBAL = new Set(['help', 'version']);
77
+ /**
78
+ * Commands whose second word is a subcommand rather than a value. An unknown one
79
+ * belongs to the dispatcher, which names it and lists the real ones: complaining
80
+ * about a flag of a command that does not exist would send the reader the wrong way.
81
+ */
82
+ const NAMESPACES = new Set([
83
+ 'org',
84
+ 'projects',
85
+ 'functions',
86
+ 'env',
87
+ 'connections',
88
+ 'schedules',
89
+ 'db',
90
+ 'storage',
91
+ 'mcp',
92
+ ]);
93
+ /** The command these words name, as far as the table knows it. */
94
+ function commandOf(words) {
95
+ const [first, second] = words;
96
+ if (!first)
97
+ return null;
98
+ if (second !== undefined) {
99
+ if (Object.hasOwn(KNOWN, `${first} ${second}`))
100
+ return `${first} ${second}`;
101
+ if (NAMESPACES.has(first))
102
+ return null;
103
+ }
104
+ return Object.hasOwn(KNOWN, first) ? first : null;
105
+ }
106
+ function checkFlags(args) {
107
+ const command = commandOf(args.words);
108
+ if (command === null)
109
+ return;
110
+ const allowed = KNOWN[command];
111
+ const topic = command.split(' ')[0];
112
+ // Not every command has a page, and sending the reader to "No page for login"
113
+ // would be a second dead end in the same breath as the first.
114
+ const page = (0, help_1.hasHelpPage)(topic) ? `xflow help ${topic}` : 'xflow help';
115
+ for (const [name, value] of Object.entries(args.flags)) {
116
+ if (GLOBAL.has(name))
117
+ continue;
118
+ if (!allowed.includes(name)) {
119
+ throw new errors_1.CliError(`xflow ${command} has no flag --${name}`, allowed.length > 0
120
+ ? `It takes ${allowed.map((flag) => `--${flag}`).join(', ')}. What each one does: ${page}`
121
+ : `It takes no flags. What it does: ${page}`);
122
+ }
123
+ // The value went missing, so the flag arrived as a bare switch. Left alone it
124
+ // would mean the default, which is never what the person typing it wanted.
125
+ if (value === true && !args_1.BOOLEAN_FLAGS.has(name)) {
126
+ throw new errors_1.CliError(`--${name} needs a value`, `Put it after the flag, or write --${name}=value when the value starts with a dash`);
127
+ }
128
+ }
129
+ }
package/dist/help.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasHelpPage = hasHelpPage;
3
4
  exports.help = help;
4
5
  const ui_1 = require("./ui");
5
6
  const version_1 = require("./version");
@@ -14,6 +15,10 @@ const ALIASES = {
14
15
  deployments: 'rollback',
15
16
  };
16
17
  /** Help text. For agents this is the reference documentation. */
18
+ /** Whether `xflow help <topic>` has a page: a hint that sends nowhere is a second dead end. */
19
+ function hasHelpPage(topic) {
20
+ return Object.hasOwn(TOPICS, ALIASES[topic] ?? topic);
21
+ }
17
22
  function help(topic) {
18
23
  if (topic) {
19
24
  const page = TOPICS[ALIASES[topic] ?? topic];
@@ -259,7 +264,11 @@ the function. So keep your own copy wherever you got it from.
259
264
  xflow env set SMTP_PASSWORD=… store
260
265
  xflow env rm SMTP_PASSWORD delete
261
266
 
262
- --scope project visible to this project only, not the whole organization
267
+ --scope project visible to this project only
268
+ --scope organization visible to every project of the organization (the default)
269
+
270
+ Those two words are the whole list: a third is refused rather than read as the default,
271
+ because a value meant for one project would have gone out to all of them.
263
272
 
264
273
  ${(0, ui_1.bold)('check')} reads the sources in ${(0, ui_1.bold)('functions/')} and looks for ${(0, ui_1.bold)('process.env.NAME')}
265
274
  references. The same rule applies on deploy: a function receives only the variables it
@@ -326,6 +335,7 @@ the day-of-month and day-of-week fields has to be ${(0, ui_1.bold)('?')}, which
326
335
  how Yandex works. ${(0, ui_1.bold)('The time is always UTC')}, local time is not understood.
327
336
 
328
337
  --payload '{"mode":"full"}' the body the function will receive
338
+ --payload-file body.json the same body from a file
329
339
 
330
340
  A scheduled run arrives at the handler as a POST with no headers, and the project
331
341
  token is not checked on it: such an event cannot be forged from outside. The function
@@ -339,8 +349,13 @@ Calls it exactly the way the application does: the project token plus a visitor
339
349
  for the person who owns the key, so the function sees a real caller and its role.
340
350
  Both are taken from the platform, no local .env is needed.
341
351
 
342
- --data '{"a":1}' request body (the method becomes POST by default)
343
- --method GET a different method
352
+ --data '{"a":1}' request body (the method becomes POST by default)
353
+ --data-file body.json the same body from a file
354
+ --method GET a different method
355
+
356
+ The body has to be JSON, and it is checked before the call goes out: PowerShell
357
+ strips double quotes from a native call, so ${(0, ui_1.bold)('--data')} needs them escaped there
358
+ (${(0, ui_1.bold)('--data \'{\\"a\\":1}\'')}) and a file needs nothing.
344
359
 
345
360
  Prints the status, the response time and the body. A non-zero exit code on 4xx and
346
361
  5xx: in CI such a call has to fail the step. The cause of a crash is shown by
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jsonProblem = jsonProblem;
4
+ exports.jsonHint = jsonHint;
5
+ exports.jsonArg = jsonArg;
6
+ const node_fs_1 = require("node:fs");
7
+ const args_1 = require("./args");
8
+ const errors_1 = require("./errors");
9
+ /**
10
+ * A JSON body typed on the command line: --data for a call, --payload for a
11
+ * schedule. Checked here, before it leaves: PowerShell strips the double quotes
12
+ * out of a native call, and an unchecked body arrives at the function as garbage,
13
+ * so a shell problem looks like a crash in the project's own code.
14
+ */
15
+ /** No quote left where JSON needs them: a shell ate them, this is not a typo. */
16
+ function quotesStripped(raw) {
17
+ return !raw.includes('"') && /^[{[]/.test(raw);
18
+ }
19
+ /** What the parser says about the text, or null when it is JSON. */
20
+ function jsonProblem(raw) {
21
+ const text = raw.trim();
22
+ if (!text)
23
+ return 'the value is empty';
24
+ try {
25
+ JSON.parse(text);
26
+ return null;
27
+ }
28
+ catch (e) {
29
+ return e instanceof Error ? e.message : String(e);
30
+ }
31
+ }
32
+ /** Where to look, and on a stripped value the shell is the first suspect. */
33
+ function jsonHint(flag, raw) {
34
+ const file = `--${flag}-file body.json`;
35
+ if (quotesStripped(raw.trim())) {
36
+ return (`Not one double quote survived: the shell removed them, PowerShell does this to ` +
37
+ `native calls. Escape them: --${flag} '{\\"key\\":\\"value\\"}', or keep the body ` +
38
+ `in a file: ${file}`);
39
+ }
40
+ return `A single JSON value is expected, or keep the body in a file: ${file}`;
41
+ }
42
+ /** The body from --<flag> or --<flag>-file, already known to be JSON. */
43
+ function jsonArg(args, flag) {
44
+ const fileFlag = `${flag}-file`;
45
+ const inline = args.flags[flag];
46
+ const path = (0, args_1.flagString)(args, fileFlag);
47
+ if (inline !== undefined && path !== undefined) {
48
+ throw new errors_1.CliError(`--${flag} and --${fileFlag} cannot be used together`, 'Give the body once');
49
+ }
50
+ if (path !== undefined) {
51
+ if (!(0, node_fs_1.existsSync)(path)) {
52
+ throw new errors_1.CliError(`No file ${path}`, `--${fileFlag} takes the path to a file with one JSON value`);
53
+ }
54
+ const text = (0, node_fs_1.readFileSync)(path, 'utf-8').trim();
55
+ const problem = jsonProblem(text);
56
+ if (problem)
57
+ throw new errors_1.CliError(`${path} is not valid JSON: ${problem}`, 'The file has to hold one JSON value');
58
+ return text;
59
+ }
60
+ if (inline === undefined)
61
+ return undefined;
62
+ if (inline === true)
63
+ throw new errors_1.CliError(`--${flag} needs a value`, `For example: --${flag} '{"mode":"full"}'`);
64
+ const raw = String(inline).trim();
65
+ const problem = jsonProblem(raw);
66
+ if (problem)
67
+ throw new errors_1.CliError(`--${flag} is not valid JSON: ${raw}`, jsonHint(flag, raw));
68
+ return raw;
69
+ }
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.2';
5
+ exports.CLI_VERSION = '0.10.3';
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.2",
3
+ "version": "0.10.3",
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.2. They travel inside the package, so the copy
27
+ These instructions ship with xflow CLI 0.10.3. 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
 
@@ -234,10 +234,15 @@ you to delete. Put the directories back instead, and if the removal really is in
234
234
  which functions are about to go and let the user answer.
235
235
 
236
236
  Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
237
- the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body),
238
- and `xflow functions logs <name>` shows the failures, each with its stack and the console
239
- output of that call. Only failed calls are logged, so an empty output means the function
240
- never crashed, not that logging is broken.
237
+ the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body,
238
+ `--data-file body.json` sends the same from a file), and `xflow functions logs <name>` shows
239
+ the failures, each with its stack and the console output of that call. Only failed calls are
240
+ logged, so an empty output means the function never crashed, not that logging is broken.
241
+
242
+ The body has to be JSON and the CLI checks it before calling. In PowerShell the double
243
+ quotes never reach the CLI: `--data '{"a":1}'` arrives as `{a:1}`, so escape them
244
+ (`--data '{\"a\":1}'`) or pass a file. This is a shell habit, not a bug in the function, and
245
+ it bites `--payload` of a schedule the same way.
241
246
 
242
247
  From the app, call a function through `src/lib/xflow.ts`:
243
248
  `await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the credentials
@@ -303,6 +308,11 @@ writes one, `xflow env` lists the names, `xflow env check` tells you which varia
303
308
  functions read but the platform does not have. Values never come back out — the only place
304
309
  they exist is inside the running function.
305
310
 
311
+ A variable goes to the whole organization unless you say otherwise: `--scope project` keeps it
312
+ to this project alone, `--scope organization` is the default, and a third word is refused
313
+ rather than read as the default. Pick the narrow one for anything that belongs to one
314
+ application.
315
+
306
316
  These commands see only what this project can see: variables shared across the organization
307
317
  and the ones bound to this project. A variable bound to another project is invisible here, so
308
318
  `env rm` reports it as missing even though names are unique within the organization.
@@ -342,7 +352,9 @@ grant it to itself. Connecting a new account and switching one off stay with a p
342
352
  `xflow schedules set report "0 3 ? * * *"` runs a function daily at 03:00. Six fields, UTC,
343
353
  and exactly one of day-of-month / day-of-week must be `?` — that is how Yandex wants it.
344
354
  A scheduled run reaches the handler as a POST with no headers, and `--payload '{"mode":"full"}'`
345
- is how it gets a body. How often a schedule may run is a plan limit, see **Plan limits**.
355
+ (or `--payload-file body.json`) is how it gets a body. The payload has to be JSON: a broken one
356
+ is refused when the schedule is set, not at three in the morning with nobody watching. How often
357
+ a schedule may run is a plan limit, see **Plan limits**.
346
358
 
347
359
  The pieces line up in one pass. From a new function to a verified schedule:
348
360