@getxflow/cli 0.14.1 → 0.15.0

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 CHANGED
@@ -265,6 +265,9 @@ function nudgeSkill() {
265
265
  }
266
266
  async function main() {
267
267
  const args = (0, args_1.parseArgs)(process.argv.slice(2));
268
+ // Decided here rather than in each command: the header of --project, the nudges and
269
+ // every progress line are written on the way to a payload that has to stand alone.
270
+ (0, ui_1.setQuiet)((0, args_1.flagBool)(args, 'json'));
268
271
  if ((0, args_1.flagBool)(args, 'version')) {
269
272
  (0, ui_1.out)(version_1.CLI_VERSION);
270
273
  return 0;
@@ -275,7 +278,7 @@ async function main() {
275
278
  }
276
279
  // The update command says it better itself, and the min-version refusal already
277
280
  // carries the same advice in its hint.
278
- let quiet = args.words[0] === 'update';
281
+ let nudgeCovered = args.words[0] === 'update';
279
282
  try {
280
283
  (0, flags_1.checkFlags)(args);
281
284
  await run(args);
@@ -283,38 +286,38 @@ async function main() {
283
286
  }
284
287
  catch (e) {
285
288
  if (e instanceof api_1.ApiError) {
286
- (0, ui_1.fail)(e.message);
289
+ (0, ui_1.fatal)(e.message);
287
290
  if (e.limit)
288
- (0, ui_1.note)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
291
+ (0, ui_1.fatalNote)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
289
292
  if (e.hint)
290
- (0, ui_1.note)((0, ui_1.dim)(` ${e.hint}`));
293
+ (0, ui_1.fatalNote)((0, ui_1.dim)(` ${e.hint}`));
291
294
  if (e.code === 'not_found') {
292
295
  // XFLOW_TOKEN is taken as given and no organization is looked for, so a
293
296
  // project of another one answers "not found" with nothing else to say.
294
297
  if (process.env.XFLOW_TOKEN?.trim()) {
295
- (0, ui_1.note)((0, ui_1.dim)(' XFLOW_TOKEN is set: it is the key of one organization and is used as it comes, so a project of another one is not found'));
298
+ (0, ui_1.fatalNote)((0, ui_1.dim)(' XFLOW_TOKEN is set: it is the key of one organization and is used as it comes, so a project of another one is not found'));
296
299
  }
297
300
  else if ((0, credentials_1.anyMultipleOrgs)()) {
298
- (0, ui_1.note)((0, ui_1.dim)(' Several organizations are signed in: the answer may live in another one, see xflow org'));
301
+ (0, ui_1.fatalNote)((0, ui_1.dim)(' Several organizations are signed in: the answer may live in another one, see xflow org'));
299
302
  }
300
303
  }
301
- quiet = quiet || e.code === 'outdated_cli';
304
+ nudgeCovered = nudgeCovered || e.code === 'outdated_cli';
302
305
  return 1;
303
306
  }
304
307
  if (e instanceof errors_1.CliError || e instanceof config_1.ConfigError || e instanceof zip_1.ZipError) {
305
- (0, ui_1.fail)(e.message);
308
+ (0, ui_1.fatal)(e.message);
306
309
  const hint = e instanceof errors_1.CliError ? e.hint : undefined;
307
310
  if (hint)
308
- (0, ui_1.note)((0, ui_1.dim)(` ${hint}`));
311
+ (0, ui_1.fatalNote)((0, ui_1.dim)(` ${hint}`));
309
312
  return 1;
310
313
  }
311
- (0, ui_1.fail)(e instanceof Error ? e.message : String(e));
314
+ (0, ui_1.fatal)(e instanceof Error ? e.message : String(e));
312
315
  if (process.env.XFLOW_DEBUG && e instanceof Error && e.stack)
313
- (0, ui_1.note)((0, ui_1.dim)(e.stack));
316
+ (0, ui_1.fatalNote)((0, ui_1.dim)(e.stack));
314
317
  return 1;
315
318
  }
316
319
  finally {
317
- if (!quiet)
320
+ if (!nudgeCovered)
318
321
  nudge();
319
322
  // Not behind the same flag: a copy out of step with the running package stays wrong
320
323
  // whatever the command was, and when the package itself is the problem this steps
@@ -325,6 +328,6 @@ async function main() {
325
328
  main().then((code) => {
326
329
  process.exitCode = code;
327
330
  }, (e) => {
328
- (0, ui_1.fail)(e instanceof Error ? e.message : String(e));
331
+ (0, ui_1.fatal)(e instanceof Error ? e.message : String(e));
329
332
  process.exitCode = 1;
330
333
  });
@@ -6,7 +6,6 @@ exports.connectionsUnlink = connectionsUnlink;
6
6
  exports.connectionsCall = connectionsCall;
7
7
  const api_1 = require("../api");
8
8
  const args_1 = require("../args");
9
- const config_1 = require("../config");
10
9
  const errors_1 = require("../errors");
11
10
  const json_arg_1 = require("../json-arg");
12
11
  const session_1 = require("../session");
@@ -107,11 +106,10 @@ async function connectionsUnlink(args) {
107
106
  if (!target) {
108
107
  throw new errors_1.CliError('A connection is required', 'For example: xflow connections unlink YANDEX_METRIKA');
109
108
  }
110
- const { root, config } = (0, config_1.requireProject)();
111
- const client = (0, session_1.connect)(config);
109
+ const { projectId, client, root } = await (0, session_1.projectTarget)(args);
112
110
  // The alias is what the code of the functions knows, and it wins: it is the
113
111
  // most precise of the three, since it names the link and not just the account.
114
- const data = await (0, api_1.apiJson)(client, endpoint(config.projectId));
112
+ const data = await (0, api_1.apiJson)(client, endpoint(projectId));
115
113
  const byAlias = data.connections.find((item) => item.alias !== null && item.alias === target.toUpperCase());
116
114
  const row = byAlias ?? resolve(data.connections, target);
117
115
  if (!row.alias) {
@@ -119,17 +117,27 @@ async function connectionsUnlink(args) {
119
117
  return;
120
118
  }
121
119
  // The platform does not see the sources, so this check lives here. Without it
122
- // the unlink is silent and the breakage shows up one deploy later.
123
- const needed = (0, env_1.referencedByFunctions)(root).needed;
124
- const inUse = row.env
125
- .map((name) => ({ name, users: needed.get(name) ?? [] }))
126
- .filter((entry) => entry.users.length > 0);
127
- if (inUse.length > 0 && !(0, args_1.flagBool)(args, 'force')) {
128
- (0, ui_1.fail)(`The functions of this project read the variables of ${row.alias}:`);
129
- (0, ui_1.table)(inUse.map((entry) => [entry.name, entry.users.join(', ')]));
130
- throw new errors_1.CliError('They would lose these variables on the next deploy', 'Repeat with --force if that is intended');
120
+ // the unlink is silent and the breakage shows up one deploy later. Named by id
121
+ // there is no folder to read: the link is still removed, since leaving it in
122
+ // place with no way to remove it is the worse of the two, and the names that go
123
+ // are printed instead of the functions that would miss them.
124
+ if (root === null) {
125
+ if (row.env.length > 0) {
126
+ (0, ui_1.note)(`Which functions read ${row.env.join(', ')} is not known here: that answer is in functions/`);
127
+ }
131
128
  }
132
- const result = await (0, api_1.apiJson)(client, `${endpoint(config.projectId)}?connection_id=${encodeURIComponent(row.id)}`, { method: 'DELETE' });
129
+ else {
130
+ const needed = (0, env_1.referencedByFunctions)(root).needed;
131
+ const inUse = row.env
132
+ .map((name) => ({ name, users: needed.get(name) ?? [] }))
133
+ .filter((entry) => entry.users.length > 0);
134
+ if (inUse.length > 0 && !(0, args_1.flagBool)(args, 'force')) {
135
+ (0, ui_1.fail)(`The functions of this project read the variables of ${row.alias}:`);
136
+ (0, ui_1.table)(inUse.map((entry) => [entry.name, entry.users.join(', ')]));
137
+ throw new errors_1.CliError('They would lose these variables on the next deploy', 'Repeat with --force if that is intended');
138
+ }
139
+ }
140
+ const result = await (0, api_1.apiJson)(client, `${endpoint(projectId)}?connection_id=${encodeURIComponent(row.id)}`, { method: 'DELETE' });
133
141
  if (!result.removed) {
134
142
  (0, ui_1.note)(`${row.label} is not linked to this project`);
135
143
  return;
@@ -178,11 +186,14 @@ function shownHeaders(headers) {
178
186
  *
179
187
  * Request headers are not filtered: there are a few, they are yours, and seeing them is how
180
188
  * a {NAME} the connection does not have shows itself.
189
+ *
190
+ * The refusal writes it with the refusal, which is why the line is a parameter: --json keeps
191
+ * nothing but the payload, and a call that never produced one would lose the address too.
181
192
  */
182
- function showRequest(request) {
183
- (0, ui_1.note)((0, ui_1.dim)(` ${request.method} ${request.url}`));
193
+ function showRequest(request, write = ui_1.note) {
194
+ write((0, ui_1.dim)(` ${request.method} ${request.url}`));
184
195
  for (const [name, value] of Object.entries(request.headers))
185
- (0, ui_1.note)((0, ui_1.dim)(` ${name}: ${value}`));
196
+ write((0, ui_1.dim)(` ${name}: ${value}`));
186
197
  }
187
198
  /** Headers typed as JSON. Values may carry {NAME} placeholders, the platform fills those in. */
188
199
  function headerArg(args) {
@@ -253,7 +264,7 @@ async function connectionsCall(args) {
253
264
  // Before the refusal itself, in the order of the answer: what went out, then what came of
254
265
  // it. Only the other side's refusals carry it, the rest never assembled an address.
255
266
  if (e instanceof api_1.ApiError && e.request)
256
- showRequest(e.request);
267
+ showRequest(e.request, ui_1.fatalNote);
257
268
  throw e;
258
269
  }
259
270
  if ((0, args_1.flagBool)(args, 'json')) {
@@ -189,7 +189,8 @@ async function dbQuery(args) {
189
189
  body: { sql, limit: (0, args_1.flagNumber)(args, 'limit') },
190
190
  timeoutMs: 60_000,
191
191
  });
192
- // The whole values, machine-readable, on stdout alone: the notes go to stderr.
192
+ // The whole values, machine-readable, and the only thing printed: --json silences
193
+ // the human lines, so stdout is the payload and nothing else.
193
194
  if ((0, args_1.flagBool)(args, 'json')) {
194
195
  (0, ui_1.out)(JSON.stringify({
195
196
  schema: data.schema,
@@ -18,7 +18,9 @@ const ui_1 = require("../ui");
18
18
  const AGENTS = [
19
19
  { id: 'claude', label: 'Claude Code', project: ['.claude', 'skills'], global: ['.claude', 'skills'] },
20
20
  { id: 'cursor', label: 'Cursor', project: ['.agents', 'skills'], global: ['.cursor', 'skills'], rule: true },
21
- { id: 'codex', label: 'Codex', project: ['.agents', 'skills'], global: ['.codex', 'skills'] },
21
+ // Codex calls ~/.codex/skills a deprecated location kept for backward compatibility;
22
+ // the user skills of every agent that shares the format live in ~/.agents/skills.
23
+ { id: 'codex', label: 'Codex', project: ['.agents', 'skills'], global: ['.agents', 'skills'], legacyGlobal: ['.codex', 'skills'] },
22
24
  { id: 'copilot', label: 'GitHub Copilot', project: ['.agents', 'skills'], global: ['.copilot', 'skills'] },
23
25
  { id: 'gemini', label: 'Gemini CLI', project: ['.agents', 'skills'], global: ['.gemini', 'skills'] },
24
26
  { id: 'opencode', label: 'OpenCode', project: ['.agents', 'skills'], global: ['.config', 'opencode', 'skills'] },
@@ -198,6 +200,10 @@ function copyPaths(base) {
198
200
  if ((0, update_1.classifyPath)(__dirname) !== 'checkout') {
199
201
  for (const agent of AGENTS)
200
202
  add(skillPath(base, agent, true), false);
203
+ for (const agent of AGENTS) {
204
+ if (agent.legacyGlobal)
205
+ add((0, node_path_1.join)((0, node_os_1.homedir)(), ...agent.legacyGlobal, 'xflow', 'SKILL.md'), false);
206
+ }
201
207
  }
202
208
  return paths;
203
209
  }
package/dist/flags.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KNOWN = void 0;
3
+ exports.WORKING_COPY = exports.KNOWN = void 0;
4
4
  exports.checkFlags = checkFlags;
5
5
  const args_1 = require("./args");
6
6
  const errors_1 = require("./errors");
@@ -19,10 +19,12 @@ const help_1 = require("./help");
19
19
  *
20
20
  * `--project` is a flag like any other here, and it is written against a command
21
21
  * only when that command can answer without the working copy. The ones that cannot
22
- * (deploy, pull, status, db migrate, env check, env set, connections unlink) have
23
- * a folder to stand in, and naming a project they cannot read would be a lie.
22
+ * have a folder to stand in, and naming a project they cannot read would be a lie;
23
+ * they are listed in WORKING_COPY below, each with what it reads from that folder.
24
24
  * `db status` sits halfway: named by id it answers with the applied history alone,
25
- * without the comparison against migrations/.
25
+ * without the comparison against migrations/. `connections unlink` sits there too:
26
+ * by id it removes the link without the check of what reads the variables, because
27
+ * a link that can be made without a folder has to be removable without one.
26
28
  *
27
29
  * flags-sync.test.ts holds this table against the code: a flag listed here has to
28
30
  * be read by the handler of its command, and a flag the handler reads has to be
@@ -60,7 +62,7 @@ exports.KNOWN = {
60
62
  connections: ['project'],
61
63
  'connections list': ['project'],
62
64
  'connections link': ['as', 'project'],
63
- 'connections unlink': ['force'],
65
+ 'connections unlink': ['force', 'project'],
64
66
  'connections call': ['method', 'data', 'data-file', 'header', 'json', 'project'],
65
67
  schedules: ['project'],
66
68
  'schedules list': ['project'],
@@ -87,6 +89,26 @@ exports.KNOWN = {
87
89
  deployments: ['project'],
88
90
  update: [],
89
91
  };
92
+ /**
93
+ * What each of those commands reads from the working copy, in its own words.
94
+ *
95
+ * `has no flag --project` alone leaves the reader one dead end short of the answer:
96
+ * the next try, without the flag, ends at the missing xflow.json, and neither line
97
+ * tells a rule from an oversight. Naming the file the command would have to read
98
+ * does, and it also says when the errand needs no folder at all.
99
+ *
100
+ * Keys are the commands KNOWN keeps `--project` from, and flags.test.ts holds the
101
+ * two lists against each other: a command that later gains the flag has to lose its
102
+ * line here, or a refusal nobody prints any more stays behind as a lie.
103
+ */
104
+ exports.WORKING_COPY = {
105
+ deploy: 'it sends the sources next to it',
106
+ pull: 'it writes the sources next to it',
107
+ status: 'it compares the folder with the revision on the platform',
108
+ 'db migrate': 'it applies the files of migrations/',
109
+ 'env check': 'it looks for the variables the code of the functions reads',
110
+ 'env set': 'it names the functions that need a deploy for the value to arrive',
111
+ };
90
112
  /** Answered everywhere: both are handled before the command runs. */
91
113
  const GLOBAL = new Set(['help', 'version']);
92
114
  /**
@@ -118,6 +140,15 @@ function commandOf(words) {
118
140
  }
119
141
  return Object.hasOwn(exports.KNOWN, first) ? first : null;
120
142
  }
143
+ function hintFor(command, flag, allowed, page) {
144
+ const reads = exports.WORKING_COPY[command];
145
+ if (flag === 'project' && reads !== undefined) {
146
+ return `It works in the project folder: ${reads}. Which commands take --project: xflow help project`;
147
+ }
148
+ return allowed.length > 0
149
+ ? `It takes ${allowed.map((name) => `--${name}`).join(', ')}. What each one does: ${page}`
150
+ : `It takes no flags. What it does: ${page}`;
151
+ }
121
152
  function checkFlags(args) {
122
153
  const command = commandOf(args.words);
123
154
  if (command === null)
@@ -131,9 +162,7 @@ function checkFlags(args) {
131
162
  if (GLOBAL.has(name))
132
163
  continue;
133
164
  if (!allowed.includes(name)) {
134
- throw new errors_1.CliError(`xflow ${command} has no flag --${name}`, allowed.length > 0
135
- ? `It takes ${allowed.map((flag) => `--${flag}`).join(', ')}. What each one does: ${page}`
136
- : `It takes no flags. What it does: ${page}`);
165
+ throw new errors_1.CliError(`xflow ${command} has no flag --${name}`, hintFor(command, name, allowed, page));
137
166
  }
138
167
  // The value went missing, so the flag arrived as a bare switch. Left alone it
139
168
  // would mean the default, which is never what the person typing it wanted.
package/dist/help.js CHANGED
@@ -154,15 +154,17 @@ no working copy of its own:
154
154
 
155
155
  Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('functions list')},
156
156
  ${(0, ui_1.bold)('functions invoke')}, ${(0, ui_1.bold)('functions logs')}, ${(0, ui_1.bold)('logs')}, ${(0, ui_1.bold)('env')}, ${(0, ui_1.bold)('env rm')},
157
- ${(0, ui_1.bold)('connections')}, ${(0, ui_1.bold)('connections link')}, ${(0, ui_1.bold)('schedules')}, ${(0, ui_1.bold)('schedules set')},
158
- ${(0, ui_1.bold)('schedules rm')}, ${(0, ui_1.bold)('storage ls')}, ${(0, ui_1.bold)('storage push')}, ${(0, ui_1.bold)('storage rm')},
159
- ${(0, ui_1.bold)('deployments')}, ${(0, ui_1.bold)('publish')}, ${(0, ui_1.bold)('rollback')}. ${(0, ui_1.bold)('db status')} answers with less
160
- under the flag: the applied history alone, because comparing it against the local
161
- ${(0, ui_1.bold)('migrations/')} directory needs the folder.
157
+ ${(0, ui_1.bold)('connections')}, ${(0, ui_1.bold)('connections link')}, ${(0, ui_1.bold)('connections unlink')}, ${(0, ui_1.bold)('schedules')},
158
+ ${(0, ui_1.bold)('schedules set')}, ${(0, ui_1.bold)('schedules rm')}, ${(0, ui_1.bold)('storage ls')}, ${(0, ui_1.bold)('storage push')},
159
+ ${(0, ui_1.bold)('storage rm')}, ${(0, ui_1.bold)('deployments')}, ${(0, ui_1.bold)('publish')}, ${(0, ui_1.bold)('rollback')}. Two of them
160
+ answer with less under the flag: ${(0, ui_1.bold)('db status')} gives the applied history alone,
161
+ because comparing it against the local ${(0, ui_1.bold)('migrations/')} directory needs the folder,
162
+ and ${(0, ui_1.bold)('connections unlink')} removes the link without naming the functions that read
163
+ its variables, for the same reason.
162
164
 
163
165
  The rest cannot answer without the working copy and have nothing to do with an id
164
166
  instead: ${(0, ui_1.bold)('deploy')}, ${(0, ui_1.bold)('pull')}, ${(0, ui_1.bold)('status')}, ${(0, ui_1.bold)('db migrate')}, ${(0, ui_1.bold)('env check')},
165
- ${(0, ui_1.bold)('env set')}, ${(0, ui_1.bold)('connections unlink')}. They answer about the files next to them, so
167
+ ${(0, ui_1.bold)('env set')}. They answer about the files next to them, so
166
168
  they keep asking for a folder. ${(0, ui_1.bold)('projects get')} and ${(0, ui_1.bold)('link')} take no flag either:
167
169
  they name the project by its id as an argument.
168
170
 
@@ -381,7 +383,10 @@ of the same service under one name; then the command prints their identifiers an
381
383
  for one of those instead of guessing.
382
384
 
383
385
  ${(0, ui_1.bold)('unlink')} refuses while a function still reads one of the variables and names
384
- those functions; ${(0, ui_1.bold)('--force')} goes through anyway.
386
+ those functions; ${(0, ui_1.bold)('--force')} goes through anyway. Seeing them needs the sources,
387
+ so under ${(0, ui_1.bold)('--project')} there is no such check: the link goes, and the variables
388
+ that go with it are printed instead. Both commands take the flag, because an account
389
+ linked to a service project without a folder has to be removable the same way.
385
390
 
386
391
  Variables are named after the alias: an OAuth connection called ${(0, ui_1.bold)('YANDEX_METRIKA')}
387
392
  gives ${(0, ui_1.bold)('YANDEX_METRIKA_TOKEN')}, a key-based one gives a variable per field. They
package/dist/session.js CHANGED
@@ -129,6 +129,10 @@ function isProjectId(value) {
129
129
  * rollback and deployments look for the organization of an unbound folder,
130
130
  * everything else takes the key it resolves to. Named by id there is no binding
131
131
  * to lean on, so the search always happens.
132
+ *
133
+ * `root` is that folder, and null when the id was named: a command that reads the
134
+ * sources for a check of its own then knows the check is not available rather than
135
+ * running it against whatever folder the caller happened to stand in.
132
136
  */
133
137
  async function projectTarget(args, options) {
134
138
  const named = (0, args_1.flagString)(args, 'project');
@@ -136,12 +140,13 @@ async function projectTarget(args, options) {
136
140
  if (!isProjectId(named)) {
137
141
  throw new errors_1.CliError(`--project takes a project id, got "${named}"`, 'The ids: xflow projects list');
138
142
  }
139
- // Unconditional: the flag is the only place the id is typed by hand, and a
140
- // wrong one answers 404 on a read and works on a write.
143
+ // Echoed back on every human run: the flag is the only place the id is typed by
144
+ // hand, and a wrong one answers 404 on a read and works on a write. Under --json
145
+ // the line is silenced with the rest, where the payload is the whole answer.
141
146
  (0, ui_1.note)((0, ui_1.dim)(` Project ${named} (--project)`));
142
- return { projectId: named, client: await connectProject(named, null) };
147
+ return { projectId: named, client: await connectProject(named, null), root: null };
143
148
  }
144
149
  const { root, config } = (0, config_1.requireProject)();
145
150
  const client = options?.probe === true ? await connectProject(config.projectId, root, config) : connect(config);
146
- return { projectId: config.projectId, client };
151
+ return { projectId: config.projectId, client, root };
147
152
  }
package/dist/ui.js CHANGED
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dim = exports.bold = void 0;
4
+ exports.setQuiet = setQuiet;
4
5
  exports.out = out;
5
6
  exports.note = note;
6
7
  exports.step = step;
7
8
  exports.ok = ok;
8
9
  exports.warn = warn;
9
10
  exports.fail = fail;
11
+ exports.fatal = fatal;
12
+ exports.fatalNote = fatalNote;
10
13
  exports.table = table;
11
14
  exports.formatBytes = formatBytes;
12
15
  exports.formatAge = formatAge;
@@ -15,10 +18,22 @@ exports.confirmWord = confirmWord;
15
18
  const node_child_process_1 = require("node:child_process");
16
19
  const node_readline_1 = require("node:readline");
17
20
  /**
18
- * All CLI output. Data goes to stdout, progress and warnings to stderr,
19
- * so `xflow pull > file` captures data only.
21
+ * All CLI output. A run that succeeds writes to stdout and nothing else; stderr
22
+ * carries the exit path alone (`fatal` below) and the questions, which need a
23
+ * terminal anyway.
24
+ *
25
+ * Progress used to go to stderr, to keep stdout clean under a pipe. PowerShell 5.1
26
+ * turns every stderr line of a native program into an ErrorRecord, so a successful
27
+ * command came out red and threw under `$ErrorActionPreference = 'Stop'`. The pipe
28
+ * is served by `--json` instead: it silences the human lines rather than moving
29
+ * them aside, and the payload is then the whole of stdout.
20
30
  */
21
31
  const color = process.stdout.isTTY === true && !process.env.NO_COLOR;
32
+ /** Set by --json: the payload on stdout is the whole answer, and nobody reads the rest. */
33
+ let quiet = false;
34
+ function setQuiet(on) {
35
+ quiet = on;
36
+ }
22
37
  function paint(code, text) {
23
38
  return color ? `\u001b[${code}m${text}\u001b[0m` : text;
24
39
  }
@@ -33,7 +48,9 @@ function out(line = '') {
33
48
  process.stdout.write(`${line}\n`);
34
49
  }
35
50
  function note(line = '') {
36
- process.stderr.write(`${line}\n`);
51
+ if (quiet)
52
+ return;
53
+ process.stdout.write(`${line}\n`);
37
54
  }
38
55
  function step(message) {
39
56
  note((0, exports.dim)(`→ ${message}`));
@@ -47,6 +64,19 @@ function warn(message) {
47
64
  function fail(message) {
48
65
  note(red(`✗ ${message}`));
49
66
  }
67
+ /**
68
+ * The exit path: what comes with a non-zero code. The only lines still written to
69
+ * stderr, and never silenced by --json, because a caller reading the payload has to
70
+ * see why there is none. `fail` above is the same red mark inside a report that ends
71
+ * well, `xflow env check` for one, and stays with the rest of it on stdout.
72
+ */
73
+ function fatal(message) {
74
+ process.stderr.write(`${red(`✗ ${message}`)}\n`);
75
+ }
76
+ /** A line belonging to the failure just announced: the hint, the limit, the stack. */
77
+ function fatalNote(line) {
78
+ process.stderr.write(`${line}\n`);
79
+ }
50
80
  /** Column-aligned rows, no header. */
51
81
  function table(rows) {
52
82
  if (rows.length === 0)
@@ -118,8 +148,8 @@ function offerBrowser(url) {
118
148
  /** Dangerous-action confirmation: type the exact word. Unavailable without a TTY. */
119
149
  async function confirmWord(question, expected) {
120
150
  if (!process.stdin.isTTY) {
121
- fail('Confirmation is only possible in an interactive terminal');
122
- note((0, exports.dim)(' In CI this is the right behaviour: a version conflict has to fail the build rather than overwrite what the platform holds'));
151
+ fatal('Confirmation is only possible in an interactive terminal');
152
+ fatalNote((0, exports.dim)(' In CI this is the right behaviour: a version conflict has to fail the build rather than overwrite what the platform holds'));
123
153
  return false;
124
154
  }
125
155
  const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stderr });
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.14.1';
5
+ exports.CLI_VERSION = '0.15.0';
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.14.1",
3
+ "version": "0.15.0",
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.14.1. They travel inside the package, so the copy
27
+ These instructions ship with xflow CLI 0.15.0. 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
 
@@ -263,7 +263,9 @@ logged, so an empty output means the function never crashed, not that logging is
263
263
  The body has to be JSON and the CLI checks it before calling. In PowerShell the double
264
264
  quotes never reach the CLI: `--data '{"a":1}'` arrives as `{a:1}`, so escape them
265
265
  (`--data '{\"a\":1}'`) or pass a file. This is a shell habit, not a bug in the function, and
266
- it bites `--payload` of a schedule the same way.
266
+ it bites `--payload` of a schedule the same way. A command that worked writes nothing to
267
+ stderr, so a red line in PowerShell means it really failed; `--json` prints the payload and
268
+ nothing else.
267
269
 
268
270
  From the app, call a function through `src/lib/xflow.ts`:
269
271
  `await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the credentials
@@ -360,8 +362,10 @@ unavailable: the account you need is often connected already, one link away.
360
362
 
361
363
  `xflow connections link "Яндекс Метрика" --as YANDEX_METRIKA` is that link and
362
364
  `xflow connections unlink YANDEX_METRIKA` undoes it; `xflow help connections` has the
363
- naming rules and the flags. Unlink refuses while a function still reads one of the
364
- variables and names those functions, so read that list before reaching for `--force`.
365
+ naming rules and the flags. In the project folder unlink reads `functions/` and refuses
366
+ while a function still reads one of the variables, naming those functions, so read that
367
+ list before reaching for `--force`. Under `--project` there is no folder to read: it
368
+ prints the variables that go and removes the link.
365
369
 
366
370
  Linking needs the `connections:link` right on the key, and only accounts granted to the
367
371
  owner of the key personally can be linked at all; the same right covers calling an unlinked