@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 +16 -13
- package/dist/commands/connections.js +29 -18
- package/dist/commands/db.js +2 -1
- package/dist/commands/skills.js +7 -1
- package/dist/flags.js +37 -8
- package/dist/help.js +12 -7
- package/dist/session.js +9 -4
- package/dist/ui.js +35 -5
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +8 -4
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
|
|
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.
|
|
289
|
+
(0, ui_1.fatal)(e.message);
|
|
287
290
|
if (e.limit)
|
|
288
|
-
(0, ui_1.
|
|
291
|
+
(0, ui_1.fatalNote)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
|
|
289
292
|
if (e.hint)
|
|
290
|
-
(0, ui_1.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
311
|
+
(0, ui_1.fatalNote)((0, ui_1.dim)(` ${hint}`));
|
|
309
312
|
return 1;
|
|
310
313
|
}
|
|
311
|
-
(0, ui_1.
|
|
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.
|
|
316
|
+
(0, ui_1.fatalNote)((0, ui_1.dim)(e.stack));
|
|
314
317
|
return 1;
|
|
315
318
|
}
|
|
316
319
|
finally {
|
|
317
|
-
if (!
|
|
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.
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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')) {
|
package/dist/commands/db.js
CHANGED
|
@@ -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,
|
|
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,
|
package/dist/commands/skills.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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)('
|
|
158
|
-
${(0, ui_1.bold)('schedules
|
|
159
|
-
${(0, ui_1.bold)('
|
|
160
|
-
under the flag: the applied history alone,
|
|
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')}
|
|
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
|
-
//
|
|
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.
|
|
19
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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.
|
|
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
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.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.
|
|
364
|
-
variables
|
|
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
|