@getxflow/cli 0.10.2 → 0.10.4
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 +4 -4
- package/dist/bin.js +27 -10
- package/dist/commands/connections.js +6 -8
- package/dist/commands/db.js +32 -6
- package/dist/commands/deploy.js +9 -12
- package/dist/commands/env.js +18 -12
- package/dist/commands/functions.js +12 -12
- package/dist/commands/logs.js +2 -4
- package/dist/commands/org.js +19 -0
- package/dist/commands/schedules.js +10 -13
- package/dist/commands/sources.js +2 -2
- package/dist/commands/storage.js +8 -12
- package/dist/flags.js +136 -0
- package/dist/help.js +65 -4
- package/dist/json-arg.js +69 -0
- package/dist/session.js +74 -12
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +24 -8
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
|
-
|
|
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");
|
|
@@ -51,11 +52,15 @@ async function run(args) {
|
|
|
51
52
|
(0, org_1.orgSwitch)({ ...args, words: args.words.slice(2) });
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
55
|
+
if (second === 'members') {
|
|
56
|
+
await (0, org_1.orgMembers)();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
54
59
|
if (second === undefined || second === 'list') {
|
|
55
60
|
(0, org_1.orgList)();
|
|
56
61
|
return;
|
|
57
62
|
}
|
|
58
|
-
throw new errors_1.CliError(`Unknown command: org ${second}`, 'Available: list and switch');
|
|
63
|
+
throw new errors_1.CliError(`Unknown command: org ${second}`, 'Available: list, members and switch');
|
|
59
64
|
case 'templates':
|
|
60
65
|
await (0, projects_1.templates)();
|
|
61
66
|
return;
|
|
@@ -94,7 +99,7 @@ async function run(args) {
|
|
|
94
99
|
return;
|
|
95
100
|
}
|
|
96
101
|
if (second === undefined || second === 'list') {
|
|
97
|
-
await (0, functions_1.functionsList)();
|
|
102
|
+
await (0, functions_1.functionsList)(rest);
|
|
98
103
|
return;
|
|
99
104
|
}
|
|
100
105
|
throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list, invoke and logs');
|
|
@@ -115,7 +120,7 @@ async function run(args) {
|
|
|
115
120
|
return;
|
|
116
121
|
}
|
|
117
122
|
if (second === undefined || second === 'list') {
|
|
118
|
-
await (0, env_1.envList)();
|
|
123
|
+
await (0, env_1.envList)(rest);
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
121
126
|
throw new errors_1.CliError(`Unknown command: env ${second}`, 'Available: list, check, set and rm');
|
|
@@ -129,7 +134,7 @@ async function run(args) {
|
|
|
129
134
|
return;
|
|
130
135
|
}
|
|
131
136
|
if (second === undefined || second === 'list') {
|
|
132
|
-
await (0, connections_1.connectionsList)();
|
|
137
|
+
await (0, connections_1.connectionsList)(rest);
|
|
133
138
|
return;
|
|
134
139
|
}
|
|
135
140
|
throw new errors_1.CliError(`Unknown command: connections ${second}`, 'Available: list, link and unlink');
|
|
@@ -143,7 +148,7 @@ async function run(args) {
|
|
|
143
148
|
return;
|
|
144
149
|
}
|
|
145
150
|
if (second === undefined || second === 'list') {
|
|
146
|
-
await (0, schedules_1.schedulesList)();
|
|
151
|
+
await (0, schedules_1.schedulesList)(rest);
|
|
147
152
|
return;
|
|
148
153
|
}
|
|
149
154
|
throw new errors_1.CliError(`Unknown command: schedules ${second}`, 'Available: list, set and rm');
|
|
@@ -152,6 +157,10 @@ async function run(args) {
|
|
|
152
157
|
await (0, db_1.dbMigrate)(rest);
|
|
153
158
|
return;
|
|
154
159
|
}
|
|
160
|
+
if (second === 'list') {
|
|
161
|
+
await (0, db_1.dbList)();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
155
164
|
if (second === 'schema') {
|
|
156
165
|
await (0, db_1.dbSchema)({ ...args, words: args.words.slice(2) });
|
|
157
166
|
return;
|
|
@@ -164,7 +173,7 @@ async function run(args) {
|
|
|
164
173
|
await (0, db_1.dbStatus)();
|
|
165
174
|
return;
|
|
166
175
|
}
|
|
167
|
-
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, schema, query and migrate');
|
|
176
|
+
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query and migrate');
|
|
168
177
|
case 'storage':
|
|
169
178
|
if (second === 'push') {
|
|
170
179
|
await (0, storage_1.storagePush)(rest);
|
|
@@ -189,13 +198,13 @@ async function run(args) {
|
|
|
189
198
|
await (0, deploy_1.deploy)(rest);
|
|
190
199
|
return;
|
|
191
200
|
case 'publish':
|
|
192
|
-
await (0, deploy_1.publish)();
|
|
201
|
+
await (0, deploy_1.publish)(rest);
|
|
193
202
|
return;
|
|
194
203
|
case 'rollback':
|
|
195
204
|
await (0, deploy_1.rollback)(rest);
|
|
196
205
|
return;
|
|
197
206
|
case 'deployments':
|
|
198
|
-
await (0, deploy_1.deployments)();
|
|
207
|
+
await (0, deploy_1.deployments)(rest);
|
|
199
208
|
return;
|
|
200
209
|
case 'update':
|
|
201
210
|
(0, update_1.update)();
|
|
@@ -255,6 +264,7 @@ async function main() {
|
|
|
255
264
|
// carries the same advice in its hint.
|
|
256
265
|
let quiet = args.words[0] === 'update';
|
|
257
266
|
try {
|
|
267
|
+
(0, flags_1.checkFlags)(args);
|
|
258
268
|
await run(args);
|
|
259
269
|
return 0;
|
|
260
270
|
}
|
|
@@ -265,8 +275,15 @@ async function main() {
|
|
|
265
275
|
(0, ui_1.note)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
|
|
266
276
|
if (e.hint)
|
|
267
277
|
(0, ui_1.note)((0, ui_1.dim)(` ${e.hint}`));
|
|
268
|
-
if (e.code === 'not_found'
|
|
269
|
-
|
|
278
|
+
if (e.code === 'not_found') {
|
|
279
|
+
// XFLOW_TOKEN is taken as given and no organization is looked for, so a
|
|
280
|
+
// project of another one answers "not found" with nothing else to say.
|
|
281
|
+
if (process.env.XFLOW_TOKEN?.trim()) {
|
|
282
|
+
(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'));
|
|
283
|
+
}
|
|
284
|
+
else if ((0, credentials_1.anyMultipleOrgs)()) {
|
|
285
|
+
(0, ui_1.note)((0, ui_1.dim)(' Several organizations are signed in: the answer may live in another one, see xflow org'));
|
|
286
|
+
}
|
|
270
287
|
}
|
|
271
288
|
quiet = quiet || e.code === 'outdated_cli';
|
|
272
289
|
return 1;
|
|
@@ -56,10 +56,9 @@ function resolve(connections, target) {
|
|
|
56
56
|
}
|
|
57
57
|
throw new errors_1.CliError(`No connection ${target}`, 'What this project can use: xflow connections');
|
|
58
58
|
}
|
|
59
|
-
async function connectionsList() {
|
|
60
|
-
const {
|
|
61
|
-
const
|
|
62
|
-
const data = await (0, api_1.apiJson)(client, endpoint(config.projectId));
|
|
59
|
+
async function connectionsList(args) {
|
|
60
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
61
|
+
const data = await (0, api_1.apiJson)(client, endpoint(projectId));
|
|
63
62
|
if (data.connections.length === 0) {
|
|
64
63
|
(0, ui_1.note)('No connections you can use in this organization');
|
|
65
64
|
(0, ui_1.note)((0, ui_1.dim)(' Somebody connects an account first: platform settings, Connectors'));
|
|
@@ -92,11 +91,10 @@ async function connectionsLink(args) {
|
|
|
92
91
|
if (!alias) {
|
|
93
92
|
throw new errors_1.CliError('An alias is required', 'For example: xflow connections link "Яндекс Метрика" --as YANDEX_METRIKA');
|
|
94
93
|
}
|
|
95
|
-
const {
|
|
96
|
-
const
|
|
97
|
-
const data = await (0, api_1.apiJson)(client, endpoint(config.projectId));
|
|
94
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
95
|
+
const data = await (0, api_1.apiJson)(client, endpoint(projectId));
|
|
98
96
|
const connectionId = resolve(data.connections, target).id;
|
|
99
|
-
const result = await (0, api_1.apiJson)(client, endpoint(
|
|
97
|
+
const result = await (0, api_1.apiJson)(client, endpoint(projectId), { method: 'POST', body: { connection_id: connectionId, alias } });
|
|
100
98
|
(0, ui_1.ok)(`${(0, ui_1.bold)(result.label)} linked as ${result.alias}`);
|
|
101
99
|
if (result.env.length > 0)
|
|
102
100
|
(0, ui_1.note)((0, ui_1.dim)(` The functions will get: ${result.env.join(', ')}`));
|
package/dist/commands/db.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dbStatus = dbStatus;
|
|
4
|
+
exports.dbList = dbList;
|
|
4
5
|
exports.dbSchema = dbSchema;
|
|
5
6
|
exports.dbQuery = dbQuery;
|
|
6
7
|
exports.dbMigrate = dbMigrate;
|
|
@@ -76,6 +77,33 @@ async function dbStatus() {
|
|
|
76
77
|
(0, ui_1.warn)('A changed file will not be applied again: write a new migration');
|
|
77
78
|
}
|
|
78
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Logical databases of the organization, and who is attached to them.
|
|
82
|
+
*
|
|
83
|
+
* The identifier is what `xflow init --database <id>` asks for, and the list of
|
|
84
|
+
* projects is not decoration: a database is shared, so a migration written for
|
|
85
|
+
* one application lands in the others too.
|
|
86
|
+
*/
|
|
87
|
+
async function dbList() {
|
|
88
|
+
const client = (0, session_1.connect)((0, config_1.localConfig)());
|
|
89
|
+
const data = await (0, api_1.apiJson)(client, '/api/v1/org/databases');
|
|
90
|
+
if (data.databases.length === 0) {
|
|
91
|
+
(0, ui_1.note)('The organization has no databases');
|
|
92
|
+
(0, ui_1.note)((0, ui_1.dim)(' One is created with the first project: xflow init'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
(0, ui_1.table)(data.databases.map((database) => [
|
|
96
|
+
database.name,
|
|
97
|
+
database.id,
|
|
98
|
+
database.schema ?? '-',
|
|
99
|
+
database.projects.length === 0
|
|
100
|
+
? 'no projects'
|
|
101
|
+
: database.projects.map((project) => project.name).join(', '),
|
|
102
|
+
]));
|
|
103
|
+
if (data.truncated)
|
|
104
|
+
(0, ui_1.note)((0, ui_1.dim)(` Shown ${data.databases.length} of ${data.total}`));
|
|
105
|
+
(0, ui_1.note)((0, ui_1.dim)(' One database is shared across projects: a migration affects all of them'));
|
|
106
|
+
}
|
|
79
107
|
/** Long values would tear the columns apart; the cut is marked, never silent. */
|
|
80
108
|
const CELL_LIMIT = 60;
|
|
81
109
|
function cell(value) {
|
|
@@ -92,10 +120,9 @@ function cell(value) {
|
|
|
92
120
|
* in there".
|
|
93
121
|
*/
|
|
94
122
|
async function dbSchema(args) {
|
|
95
|
-
const {
|
|
96
|
-
const client = (0, session_1.connect)(config);
|
|
123
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
97
124
|
const wanted = args.words[0];
|
|
98
|
-
const path = `/api/v1/projects/${
|
|
125
|
+
const path = `/api/v1/projects/${projectId}/db/schema${wanted ? `?table=${encodeURIComponent(wanted)}` : ''}`;
|
|
99
126
|
if (wanted) {
|
|
100
127
|
const data = await (0, api_1.apiJson)(client, path);
|
|
101
128
|
(0, ui_1.out)(`${(0, ui_1.bold)(`${data.schema}.${data.table}`)}`);
|
|
@@ -134,13 +161,12 @@ async function dbSchema(args) {
|
|
|
134
161
|
* database itself rather than by us guessing at the text of the SQL.
|
|
135
162
|
*/
|
|
136
163
|
async function dbQuery(args) {
|
|
137
|
-
const { config } = (0, config_1.requireProject)();
|
|
138
164
|
const sql = args.words.join(' ').trim();
|
|
139
165
|
if (!sql) {
|
|
140
166
|
throw new errors_1.CliError('A query is required', 'For example: xflow db query "select * from orders order by created_at desc limit 20"');
|
|
141
167
|
}
|
|
142
|
-
const client = (0, session_1.
|
|
143
|
-
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${
|
|
168
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
169
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/query`, {
|
|
144
170
|
method: 'POST',
|
|
145
171
|
body: { sql, limit: (0, args_1.flagNumber)(args, 'limit') },
|
|
146
172
|
timeoutMs: 60_000,
|
package/dist/commands/deploy.js
CHANGED
|
@@ -122,7 +122,7 @@ async function assertBuildAllowed(client, projectId) {
|
|
|
122
122
|
}
|
|
123
123
|
async function deploy(args) {
|
|
124
124
|
const { root, config } = (0, config_1.requireProject)();
|
|
125
|
-
const client = await (0, session_1.connectProject)(root, config);
|
|
125
|
+
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
126
126
|
let revision;
|
|
127
127
|
if ((0, args_1.flagBool)(args, 'no-push')) {
|
|
128
128
|
const server = await (0, sources_1.latestRevision)(client, config.projectId);
|
|
@@ -170,11 +170,10 @@ async function deploy(args) {
|
|
|
170
170
|
(0, ui_1.note)((0, ui_1.dim)(' and are not held back by publishing'));
|
|
171
171
|
(0, ui_1.note)((0, ui_1.dim)(' Show the new pages to visitors: xflow publish'));
|
|
172
172
|
}
|
|
173
|
-
async function publish() {
|
|
174
|
-
const {
|
|
175
|
-
const client = await (0, session_1.connectProject)(root, config);
|
|
173
|
+
async function publish(args) {
|
|
174
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args, { probe: true });
|
|
176
175
|
(0, ui_1.step)('Publishing the current dev version');
|
|
177
|
-
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${
|
|
176
|
+
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/publish`, { method: 'POST', timeoutMs: 120_000 });
|
|
178
177
|
if (result.already_published) {
|
|
179
178
|
(0, ui_1.ok)('This version is already published');
|
|
180
179
|
}
|
|
@@ -186,13 +185,12 @@ async function publish() {
|
|
|
186
185
|
(0, ui_1.note)((0, ui_1.dim)(' have been running the code of the last xflow deploy since it finished'));
|
|
187
186
|
}
|
|
188
187
|
async function rollback(args) {
|
|
189
|
-
const { root, config } = (0, config_1.requireProject)();
|
|
190
188
|
const deployId = args.words[0];
|
|
191
189
|
if (!deployId) {
|
|
192
190
|
throw new errors_1.CliError('A version number is required', 'To see the versions: xflow deployments. For example: xflow rollback 481203');
|
|
193
191
|
}
|
|
194
|
-
const client = await (0, session_1.
|
|
195
|
-
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${
|
|
192
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args, { probe: true });
|
|
193
|
+
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/rollback`, { method: 'POST', body: { deploy_id: deployId } });
|
|
196
194
|
(0, ui_1.ok)(`The project now serves the pages of build ${result.deploy_id}`);
|
|
197
195
|
(0, ui_1.out)(result.project_url);
|
|
198
196
|
(0, ui_1.note)((0, ui_1.dim)(' Only the pages came back. Cloud functions and the database are one per project:'));
|
|
@@ -203,10 +201,9 @@ async function rollback(args) {
|
|
|
203
201
|
}
|
|
204
202
|
(0, ui_1.note)((0, ui_1.dim)(' Visitors see these pages only after xflow publish'));
|
|
205
203
|
}
|
|
206
|
-
async function deployments() {
|
|
207
|
-
const {
|
|
208
|
-
const
|
|
209
|
-
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/deployments`);
|
|
204
|
+
async function deployments(args) {
|
|
205
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args, { probe: true });
|
|
206
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/deployments`);
|
|
210
207
|
if (data.deployments.length === 0) {
|
|
211
208
|
(0, ui_1.note)('No versions yet. To build and release: xflow deploy');
|
|
212
209
|
return;
|
package/dist/commands/env.js
CHANGED
|
@@ -82,14 +82,17 @@ function referencedByFunctions(root) {
|
|
|
82
82
|
}
|
|
83
83
|
return { needed, provided, opaque };
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The stored variables. The project is resolved by the caller: list takes
|
|
87
|
+
* --project, check reads the sources and so needs the folder it is standing in.
|
|
88
|
+
*/
|
|
89
|
+
async function fetchVariables(client, projectId) {
|
|
90
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/env`);
|
|
91
|
+
return { names: new Set(data.variables.map((row) => row.name)), rows: data.variables };
|
|
90
92
|
}
|
|
91
|
-
async function envList() {
|
|
92
|
-
const {
|
|
93
|
+
async function envList(args) {
|
|
94
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
95
|
+
const { rows } = await fetchVariables(client, projectId);
|
|
93
96
|
if (rows.length === 0) {
|
|
94
97
|
(0, ui_1.note)('No variables');
|
|
95
98
|
(0, ui_1.note)((0, ui_1.dim)(' To store one: xflow env set SMTP_PASSWORD=secret'));
|
|
@@ -100,7 +103,8 @@ async function envList() {
|
|
|
100
103
|
}
|
|
101
104
|
/** Check that every referenced variable is stored. */
|
|
102
105
|
async function envCheck() {
|
|
103
|
-
const {
|
|
106
|
+
const { root, config } = (0, config_1.requireProject)();
|
|
107
|
+
const { names } = await fetchVariables((0, session_1.connect)(config), config.projectId);
|
|
104
108
|
const { needed, provided, opaque } = referencedByFunctions(root);
|
|
105
109
|
if (needed.size === 0 && provided.size === 0 && opaque.size === 0) {
|
|
106
110
|
(0, ui_1.note)('The functions of this project read no environment variables');
|
|
@@ -164,7 +168,10 @@ async function envSet(args) {
|
|
|
164
168
|
const value = pair.slice(pair.indexOf('=') + 1);
|
|
165
169
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env`, {
|
|
166
170
|
method: 'POST',
|
|
167
|
-
|
|
171
|
+
// The word goes to the platform as typed: it knows the two scopes and
|
|
172
|
+
// refuses a third. Guessing here would have made --scope proj mean the
|
|
173
|
+
// whole organization, which is the opposite of what such a typo means.
|
|
174
|
+
body: { name, value, scope: (0, args_1.flagString)(args, 'scope') },
|
|
168
175
|
});
|
|
169
176
|
(0, ui_1.ok)(`${(0, ui_1.bold)(result.name)} stored (${result.scope === 'project' ? 'this project only' : 'the whole organization'})`);
|
|
170
177
|
// Values reach a function on its next deploy.
|
|
@@ -174,12 +181,11 @@ async function envSet(args) {
|
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
async function envRemove(args) {
|
|
177
|
-
const { config } = (0, config_1.requireProject)();
|
|
178
|
-
const client = (0, session_1.connect)(config);
|
|
179
184
|
const name = args.words[1];
|
|
180
185
|
if (!name)
|
|
181
186
|
throw new errors_1.CliError('A variable name is required', 'What is stored: xflow env');
|
|
182
|
-
const
|
|
187
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
188
|
+
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/env?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
183
189
|
if (!result.removed) {
|
|
184
190
|
(0, ui_1.note)(`There is no variable ${name}`);
|
|
185
191
|
return;
|
|
@@ -4,8 +4,8 @@ exports.functionsList = functionsList;
|
|
|
4
4
|
exports.functionsInvoke = functionsInvoke;
|
|
5
5
|
const api_1 = require("../api");
|
|
6
6
|
const args_1 = require("../args");
|
|
7
|
-
const config_1 = require("../config");
|
|
8
7
|
const errors_1 = require("../errors");
|
|
8
|
+
const json_arg_1 = require("../json-arg");
|
|
9
9
|
const session_1 = require("../session");
|
|
10
10
|
const ui_1 = require("../ui");
|
|
11
11
|
/** Who can call the function: inside the app only, or an outside service holding a key. */
|
|
@@ -15,10 +15,9 @@ function accessLabel(fn) {
|
|
|
15
15
|
return `external (${fn.external_keys} key${fn.external_keys > 1 ? 's' : ''})`;
|
|
16
16
|
}
|
|
17
17
|
const FUNCTIONS_DIR = 'functions';
|
|
18
|
-
async function functionsList() {
|
|
19
|
-
const {
|
|
20
|
-
const
|
|
21
|
-
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/functions`);
|
|
18
|
+
async function functionsList(args) {
|
|
19
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
20
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/functions`);
|
|
22
21
|
if (data.functions.length === 0) {
|
|
23
22
|
(0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow deploy`);
|
|
24
23
|
return;
|
|
@@ -41,27 +40,28 @@ function prettyBody(text) {
|
|
|
41
40
|
}
|
|
42
41
|
/** Call a function the same way the application does. */
|
|
43
42
|
async function functionsInvoke(args) {
|
|
44
|
-
const {
|
|
45
|
-
const client = (0, session_1.connect)(config);
|
|
43
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
46
44
|
const name = args.words[1];
|
|
47
45
|
if (!name)
|
|
48
46
|
throw new errors_1.CliError('A function name is required', 'What is deployed: xflow functions list');
|
|
49
|
-
|
|
47
|
+
// The body is judged before anything goes over the network: a broken one is the
|
|
48
|
+
// user's own line, and it costs nothing to say so at once.
|
|
49
|
+
const data = (0, json_arg_1.jsonArg)(args, 'data');
|
|
50
|
+
const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
|
|
51
|
+
const sendsBody = method !== 'GET' && method !== 'HEAD';
|
|
52
|
+
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
50
53
|
const fn = card.functions.find((item) => item.name === name);
|
|
51
54
|
if (!fn || !fn.invoke_url) {
|
|
52
55
|
throw new errors_1.CliError(`Function ${name} is not deployed`, card.functions.length > 0
|
|
53
56
|
? `Deployed: ${card.functions.map((item) => item.name).join(', ')}`
|
|
54
57
|
: 'Functions ship with the build: xflow deploy');
|
|
55
58
|
}
|
|
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
59
|
// A pass for the person who owns the key: the project token alone is not an
|
|
60
60
|
// identity, and a project built with the current template refuses without one.
|
|
61
61
|
// A read-only key cannot get a pass, so the call still goes out without it.
|
|
62
62
|
let pass = '';
|
|
63
63
|
try {
|
|
64
|
-
const issued = await (0, api_1.apiJson)(client, `/api/v1/projects/${
|
|
64
|
+
const issued = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/pass`, { method: 'POST' });
|
|
65
65
|
pass = issued.pass;
|
|
66
66
|
}
|
|
67
67
|
catch {
|
package/dist/commands/logs.js
CHANGED
|
@@ -4,7 +4,6 @@ exports.logs = logs;
|
|
|
4
4
|
exports.functionsLogs = functionsLogs;
|
|
5
5
|
const api_1 = require("../api");
|
|
6
6
|
const args_1 = require("../args");
|
|
7
|
-
const config_1 = require("../config");
|
|
8
7
|
const session_1 = require("../session");
|
|
9
8
|
const ui_1 = require("../ui");
|
|
10
9
|
const DEFAULT_LIMIT = 10;
|
|
@@ -15,12 +14,11 @@ function stamp(iso) {
|
|
|
15
14
|
return `${pad(date.getDate())}.${pad(date.getMonth() + 1)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
16
15
|
}
|
|
17
16
|
async function fetchLogs(source, name, args) {
|
|
18
|
-
const {
|
|
19
|
-
const client = (0, session_1.connect)(config);
|
|
17
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
20
18
|
const query = new URLSearchParams({ source, limit: String((0, args_1.flagNumber)(args, 'limit') ?? DEFAULT_LIMIT) });
|
|
21
19
|
if (name)
|
|
22
20
|
query.set('name', name);
|
|
23
|
-
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${
|
|
21
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/logs?${query.toString()}`);
|
|
24
22
|
return data.logs;
|
|
25
23
|
}
|
|
26
24
|
/** Newest at the bottom. */
|
package/dist/commands/org.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.orgList = orgList;
|
|
4
|
+
exports.orgMembers = orgMembers;
|
|
4
5
|
exports.orgSwitch = orgSwitch;
|
|
6
|
+
const api_1 = require("../api");
|
|
5
7
|
const config_1 = require("../config");
|
|
6
8
|
const credentials_1 = require("../credentials");
|
|
7
9
|
const errors_1 = require("../errors");
|
|
10
|
+
const session_1 = require("../session");
|
|
8
11
|
const ui_1 = require("../ui");
|
|
9
12
|
const mcp_1 = require("./mcp");
|
|
10
13
|
function label(org) {
|
|
@@ -35,6 +38,22 @@ function orgList() {
|
|
|
35
38
|
(0, ui_1.note)((0, ui_1.dim)(' Switch: xflow org switch <name or id>'));
|
|
36
39
|
(0, ui_1.note)((0, ui_1.dim)(' To add an organization, sign in to it: xflow login'));
|
|
37
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Who is in the organization this key belongs to, and with which role. Reading
|
|
43
|
+
* only: people and money are the owner's, in the platform interface.
|
|
44
|
+
*/
|
|
45
|
+
async function orgMembers() {
|
|
46
|
+
const client = (0, session_1.connect)((0, config_1.localConfig)());
|
|
47
|
+
const data = await (0, api_1.apiJson)(client, '/api/v1/org/members');
|
|
48
|
+
if (data.members.length === 0) {
|
|
49
|
+
(0, ui_1.note)('The organization has no members');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
(0, ui_1.table)(data.members.map((member) => [member.name ?? '(unnamed)', member.email ?? '-', member.role]));
|
|
53
|
+
const org = (0, credentials_1.listOrgs)(client.apiUrl).find((entry) => entry.organizationId === client.organizationId);
|
|
54
|
+
const count = data.truncated ? `${data.members.length} of ${data.total}` : String(data.total);
|
|
55
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${count} in "${org ? label(org) : 'the organization'}". Inviting and removing people happens in the platform interface`));
|
|
56
|
+
}
|
|
38
57
|
/** Local switch of the active organization: a pointer move, no browser. */
|
|
39
58
|
function orgSwitch(args) {
|
|
40
59
|
const wanted = args.words[0];
|
|
@@ -4,15 +4,13 @@ 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
|
-
const config_1 = require("../config");
|
|
9
7
|
const errors_1 = require("../errors");
|
|
8
|
+
const json_arg_1 = require("../json-arg");
|
|
10
9
|
const session_1 = require("../session");
|
|
11
10
|
const ui_1 = require("../ui");
|
|
12
|
-
async function schedulesList() {
|
|
13
|
-
const {
|
|
14
|
-
const
|
|
15
|
-
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules`);
|
|
11
|
+
async function schedulesList(args) {
|
|
12
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
13
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/schedules`);
|
|
16
14
|
if (data.schedules.length === 0) {
|
|
17
15
|
(0, ui_1.note)('No schedules');
|
|
18
16
|
(0, ui_1.note)((0, ui_1.dim)(' To run a function on a timer: xflow schedules set <function> "0 3 ? * * *"'));
|
|
@@ -26,23 +24,21 @@ async function schedulesList() {
|
|
|
26
24
|
]));
|
|
27
25
|
}
|
|
28
26
|
async function schedulesSet(args) {
|
|
29
|
-
const { config } = (0, config_1.requireProject)();
|
|
30
|
-
const client = (0, session_1.connect)(config);
|
|
31
27
|
const functionName = args.words[1];
|
|
32
28
|
const cron = args.words[2];
|
|
33
29
|
if (!functionName || !cron) {
|
|
34
30
|
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
31
|
}
|
|
36
|
-
const
|
|
32
|
+
const payload = (0, json_arg_1.jsonArg)(args, 'payload') ?? null;
|
|
33
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
34
|
+
const row = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/schedules`, {
|
|
37
35
|
method: 'POST',
|
|
38
|
-
body: { function: functionName, cron, payload
|
|
36
|
+
body: { function: functionName, cron, payload },
|
|
39
37
|
});
|
|
40
38
|
(0, ui_1.ok)(`${(0, ui_1.bold)(row.function)} runs ${row.description}`);
|
|
41
39
|
(0, ui_1.note)((0, ui_1.dim)(` The time is UTC. To check by hand: xflow functions invoke ${row.function}`));
|
|
42
40
|
}
|
|
43
41
|
async function schedulesRemove(args) {
|
|
44
|
-
const { config } = (0, config_1.requireProject)();
|
|
45
|
-
const client = (0, session_1.connect)(config);
|
|
46
42
|
const functionName = args.words[1];
|
|
47
43
|
if (!functionName) {
|
|
48
44
|
throw new errors_1.CliError('A function name is required', 'What runs on a timer: xflow schedules list');
|
|
@@ -51,7 +47,8 @@ async function schedulesRemove(args) {
|
|
|
51
47
|
const query = new URLSearchParams({ function: functionName });
|
|
52
48
|
if (cron)
|
|
53
49
|
query.set('cron', cron);
|
|
54
|
-
const
|
|
50
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
51
|
+
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/schedules?${query.toString()}`, { method: 'DELETE' });
|
|
55
52
|
if (result.removed === 0) {
|
|
56
53
|
(0, ui_1.note)(`Function ${functionName} has no such schedule`);
|
|
57
54
|
return;
|
package/dist/commands/sources.js
CHANGED
|
@@ -154,7 +154,7 @@ async function pull(args) {
|
|
|
154
154
|
const { root, config } = (0, config_1.requireProject)();
|
|
155
155
|
const into = (0, args_1.flagString)(args, 'into');
|
|
156
156
|
const target = into ? (0, node_path_1.resolve)(into) : root;
|
|
157
|
-
const client = await (0, session_1.connectProject)(root, config);
|
|
157
|
+
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
158
158
|
if (hasContent(target) && !(0, args_1.flagBool)(args, 'force')) {
|
|
159
159
|
throw new errors_1.CliError(`The directory ${target} is not empty`, 'The platform cannot merge changes, that is git work. Fetch the server copy alongside: ' +
|
|
160
160
|
'xflow pull --into ./server-copy, or overwrite the folder completely: xflow pull --force');
|
|
@@ -172,7 +172,7 @@ async function pull(args) {
|
|
|
172
172
|
}
|
|
173
173
|
async function status() {
|
|
174
174
|
const { root, config } = (0, config_1.requireProject)();
|
|
175
|
-
const client = await (0, session_1.connectProject)(root, config);
|
|
175
|
+
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
176
176
|
const tree = prepareTree(root, config);
|
|
177
177
|
const state = (0, config_1.readState)(root);
|
|
178
178
|
const [card, server, history] = await Promise.all([
|
package/dist/commands/storage.js
CHANGED
|
@@ -11,7 +11,6 @@ const node_path_1 = require("node:path");
|
|
|
11
11
|
const node_stream_1 = require("node:stream");
|
|
12
12
|
const api_1 = require("../api");
|
|
13
13
|
const args_1 = require("../args");
|
|
14
|
-
const config_1 = require("../config");
|
|
15
14
|
const errors_1 = require("../errors");
|
|
16
15
|
const session_1 = require("../session");
|
|
17
16
|
const ui_1 = require("../ui");
|
|
@@ -158,10 +157,9 @@ async function fetchAll(client, projectId, folder) {
|
|
|
158
157
|
return files;
|
|
159
158
|
}
|
|
160
159
|
async function storageList(args) {
|
|
161
|
-
const {
|
|
162
|
-
const client = (0, session_1.connect)(config);
|
|
160
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
163
161
|
const folder = args.words[1] ?? '';
|
|
164
|
-
const files = await fetchAll(client,
|
|
162
|
+
const files = await fetchAll(client, projectId, folder);
|
|
165
163
|
if ((0, args_1.flagBool)(args, 'json')) {
|
|
166
164
|
(0, ui_1.out)(JSON.stringify({ files }, null, 2));
|
|
167
165
|
return;
|
|
@@ -210,8 +208,6 @@ async function inParallel(items, limit, task) {
|
|
|
210
208
|
return results;
|
|
211
209
|
}
|
|
212
210
|
async function storagePush(args) {
|
|
213
|
-
const { config } = (0, config_1.requireProject)();
|
|
214
|
-
const client = (0, session_1.connect)(config);
|
|
215
211
|
const source = args.words[1];
|
|
216
212
|
if (!source) {
|
|
217
213
|
throw new errors_1.CliError('A folder or a file is required', 'For example: xflow storage push ./media --to media');
|
|
@@ -239,9 +235,10 @@ async function storagePush(args) {
|
|
|
239
235
|
}
|
|
240
236
|
const replace = (0, args_1.flagBool)(args, 'replace');
|
|
241
237
|
const asJson = (0, args_1.flagBool)(args, 'json');
|
|
238
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
242
239
|
// What is already there decides what to send: the same run repeated has to be
|
|
243
240
|
// cheap and quiet, not a pile of conflicts.
|
|
244
|
-
const known = new Map((await fetchAll(client,
|
|
241
|
+
const known = new Map((await fetchAll(client, projectId, target)).map((f) => [f.path, f]));
|
|
245
242
|
const outcome = { uploaded: [], skipped: [], failed: [] };
|
|
246
243
|
const pending = [];
|
|
247
244
|
for (const file of files) {
|
|
@@ -270,7 +267,7 @@ async function storagePush(args) {
|
|
|
270
267
|
(0, ui_1.step)(`Uploading ${pending.length} file(s), ${(0, ui_1.formatBytes)(bytes)}`);
|
|
271
268
|
}
|
|
272
269
|
for (const batch of batches) {
|
|
273
|
-
const { files: tickets } = await (0, api_1.apiJson)(client, `/api/v1/projects/${
|
|
270
|
+
const { files: tickets } = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/storage/upload-url`, {
|
|
274
271
|
method: 'POST',
|
|
275
272
|
body: {
|
|
276
273
|
files: batch.map((file) => ({
|
|
@@ -306,7 +303,7 @@ async function storagePush(args) {
|
|
|
306
303
|
// Confirm right after the batch, not at the end: an object that is in the
|
|
307
304
|
// bucket without a record is invisible to the project and still takes up
|
|
308
305
|
// the quota.
|
|
309
|
-
const confirmed = await (0, api_1.apiJson)(client, `/api/v1/projects/${
|
|
306
|
+
const confirmed = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/storage/confirm`, {
|
|
310
307
|
method: 'POST',
|
|
311
308
|
body: { files: done.map(({ ticket }) => ({ s3_key: ticket.s3_key })) },
|
|
312
309
|
});
|
|
@@ -343,8 +340,6 @@ function report(outcome, asJson) {
|
|
|
343
340
|
}
|
|
344
341
|
}
|
|
345
342
|
async function storageRemove(args) {
|
|
346
|
-
const { config } = (0, config_1.requireProject)();
|
|
347
|
-
const client = (0, session_1.connect)(config);
|
|
348
343
|
const folder = (0, args_1.flagString)(args, 'folder');
|
|
349
344
|
const url = args.words[1];
|
|
350
345
|
if ((!url && !folder) || (url && folder)) {
|
|
@@ -353,9 +348,10 @@ async function storageRemove(args) {
|
|
|
353
348
|
const query = new URLSearchParams(url ? { url } : { folder: folder });
|
|
354
349
|
if ((0, args_1.flagBool)(args, 'yes'))
|
|
355
350
|
query.set('confirm', 'true');
|
|
351
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
356
352
|
let result;
|
|
357
353
|
try {
|
|
358
|
-
result = await (0, api_1.apiJson)(client, `/api/v1/projects/${
|
|
354
|
+
result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/storage?${query.toString()}`, { method: 'DELETE' });
|
|
359
355
|
}
|
|
360
356
|
catch (e) {
|
|
361
357
|
// The platform counts what a folder holds and refuses until the answer is
|
package/dist/flags.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
* `--project` is a flag like any other here, and it is written against a command
|
|
20
|
+
* only when that command does not read the working copy. The ones that do (deploy,
|
|
21
|
+
* pull, status, db migrate, db status, env check, env set, connections unlink) have
|
|
22
|
+
* a folder to stand in, and naming a project they cannot read would be a lie.
|
|
23
|
+
*/
|
|
24
|
+
const KNOWN = {
|
|
25
|
+
help: [],
|
|
26
|
+
login: [],
|
|
27
|
+
logout: ['all'],
|
|
28
|
+
whoami: [],
|
|
29
|
+
org: [],
|
|
30
|
+
'org list': [],
|
|
31
|
+
'org members': [],
|
|
32
|
+
'org switch': [],
|
|
33
|
+
templates: [],
|
|
34
|
+
init: ['name', 'template', 'database'],
|
|
35
|
+
link: [],
|
|
36
|
+
projects: [],
|
|
37
|
+
'projects list': [],
|
|
38
|
+
'projects get': [],
|
|
39
|
+
skills: ['refresh', 'global', 'agent', 'yes'],
|
|
40
|
+
mcp: ['show-token'],
|
|
41
|
+
'mcp install': ['show-token'],
|
|
42
|
+
functions: ['project'],
|
|
43
|
+
'functions list': ['project'],
|
|
44
|
+
'functions invoke': ['data', 'data-file', 'method', 'project'],
|
|
45
|
+
'functions logs': ['limit', 'project'],
|
|
46
|
+
logs: ['limit', 'project'],
|
|
47
|
+
env: ['project'],
|
|
48
|
+
'env list': ['project'],
|
|
49
|
+
'env set': ['scope'],
|
|
50
|
+
'env rm': ['project'],
|
|
51
|
+
'env remove': ['project'],
|
|
52
|
+
'env check': [],
|
|
53
|
+
connections: ['project'],
|
|
54
|
+
'connections list': ['project'],
|
|
55
|
+
'connections link': ['as', 'project'],
|
|
56
|
+
'connections unlink': ['force'],
|
|
57
|
+
schedules: ['project'],
|
|
58
|
+
'schedules list': ['project'],
|
|
59
|
+
'schedules set': ['payload', 'payload-file', 'project'],
|
|
60
|
+
'schedules rm': ['project'],
|
|
61
|
+
'schedules remove': ['project'],
|
|
62
|
+
db: [],
|
|
63
|
+
'db status': [],
|
|
64
|
+
'db list': [],
|
|
65
|
+
'db schema': ['project'],
|
|
66
|
+
'db query': ['limit', 'project'],
|
|
67
|
+
'db migrate': ['dry-run', 'allow-destructive'],
|
|
68
|
+
storage: ['json', 'project'],
|
|
69
|
+
'storage ls': ['json', 'project'],
|
|
70
|
+
'storage list': ['json', 'project'],
|
|
71
|
+
'storage push': ['to', 'replace', 'json', 'project'],
|
|
72
|
+
'storage rm': ['folder', 'yes', 'project'],
|
|
73
|
+
'storage remove': ['folder', 'yes', 'project'],
|
|
74
|
+
status: [],
|
|
75
|
+
pull: ['into', 'force', 'revision'],
|
|
76
|
+
deploy: ['no-push', 'force', 'allow-removals'],
|
|
77
|
+
publish: ['project'],
|
|
78
|
+
rollback: ['project'],
|
|
79
|
+
deployments: ['project'],
|
|
80
|
+
update: [],
|
|
81
|
+
};
|
|
82
|
+
/** Answered everywhere: both are handled before the command runs. */
|
|
83
|
+
const GLOBAL = new Set(['help', 'version']);
|
|
84
|
+
/**
|
|
85
|
+
* Commands whose second word is a subcommand rather than a value. An unknown one
|
|
86
|
+
* belongs to the dispatcher, which names it and lists the real ones: complaining
|
|
87
|
+
* about a flag of a command that does not exist would send the reader the wrong way.
|
|
88
|
+
*/
|
|
89
|
+
const NAMESPACES = new Set([
|
|
90
|
+
'org',
|
|
91
|
+
'projects',
|
|
92
|
+
'functions',
|
|
93
|
+
'env',
|
|
94
|
+
'connections',
|
|
95
|
+
'schedules',
|
|
96
|
+
'db',
|
|
97
|
+
'storage',
|
|
98
|
+
'mcp',
|
|
99
|
+
]);
|
|
100
|
+
/** The command these words name, as far as the table knows it. */
|
|
101
|
+
function commandOf(words) {
|
|
102
|
+
const [first, second] = words;
|
|
103
|
+
if (!first)
|
|
104
|
+
return null;
|
|
105
|
+
if (second !== undefined) {
|
|
106
|
+
if (Object.hasOwn(KNOWN, `${first} ${second}`))
|
|
107
|
+
return `${first} ${second}`;
|
|
108
|
+
if (NAMESPACES.has(first))
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return Object.hasOwn(KNOWN, first) ? first : null;
|
|
112
|
+
}
|
|
113
|
+
function checkFlags(args) {
|
|
114
|
+
const command = commandOf(args.words);
|
|
115
|
+
if (command === null)
|
|
116
|
+
return;
|
|
117
|
+
const allowed = KNOWN[command];
|
|
118
|
+
const topic = command.split(' ')[0];
|
|
119
|
+
// Not every command has a page, and sending the reader to "No page for login"
|
|
120
|
+
// would be a second dead end in the same breath as the first.
|
|
121
|
+
const page = (0, help_1.hasHelpPage)(topic) ? `xflow help ${topic}` : 'xflow help';
|
|
122
|
+
for (const [name, value] of Object.entries(args.flags)) {
|
|
123
|
+
if (GLOBAL.has(name))
|
|
124
|
+
continue;
|
|
125
|
+
if (!allowed.includes(name)) {
|
|
126
|
+
throw new errors_1.CliError(`xflow ${command} has no flag --${name}`, allowed.length > 0
|
|
127
|
+
? `It takes ${allowed.map((flag) => `--${flag}`).join(', ')}. What each one does: ${page}`
|
|
128
|
+
: `It takes no flags. What it does: ${page}`);
|
|
129
|
+
}
|
|
130
|
+
// The value went missing, so the flag arrived as a bare switch. Left alone it
|
|
131
|
+
// would mean the default, which is never what the person typing it wanted.
|
|
132
|
+
if (value === true && !args_1.BOOLEAN_FLAGS.has(name)) {
|
|
133
|
+
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`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
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];
|
|
@@ -77,6 +82,7 @@ ${(0, ui_1.bold)('Functions')}
|
|
|
77
82
|
|
|
78
83
|
${(0, ui_1.bold)('Database')}
|
|
79
84
|
xflow db status which migrations are applied and which are waiting
|
|
85
|
+
xflow db list databases of the organization and the projects on them
|
|
80
86
|
xflow db schema [table] tables of the project, or the columns of one
|
|
81
87
|
xflow db query "select ..." read data, in a read-only transaction
|
|
82
88
|
xflow db migrate [--dry-run] [--allow-destructive]
|
|
@@ -87,12 +93,17 @@ ${(0, ui_1.bold)('Reference')}
|
|
|
87
93
|
xflow projects list projects of the organization
|
|
88
94
|
xflow projects get [id] project card
|
|
89
95
|
xflow org organizations with a stored key, the active one marked
|
|
96
|
+
xflow org members who is in the organization, and with which role
|
|
90
97
|
xflow org switch <name|id> make another organization the active one
|
|
91
98
|
xflow whoami whose key this is and what it can do
|
|
92
99
|
xflow logout [--all] forget the key of the active organization (--all: every one)
|
|
93
100
|
xflow update update the CLI itself, and the skill that ships with it
|
|
94
101
|
xflow --version which version is installed
|
|
95
102
|
|
|
103
|
+
${(0, ui_1.bold)('Outside a project folder')}
|
|
104
|
+
--project <id> name the project by id, for every command that does
|
|
105
|
+
not read the local files (xflow help project)
|
|
106
|
+
|
|
96
107
|
${(0, ui_1.bold)('Environment')}
|
|
97
108
|
XFLOW_TOKEN access key (for CI, instead of xflow login)
|
|
98
109
|
XFLOW_API_URL platform address when it is not app.getxflow.com
|
|
@@ -112,10 +123,14 @@ Which key a command then uses, in order:
|
|
|
112
123
|
3. the active organization
|
|
113
124
|
|
|
114
125
|
xflow org the stored organizations, the active one marked
|
|
126
|
+
xflow org members who is in this one, and with which role
|
|
115
127
|
xflow org switch <name|id> make another one active, no browser involved
|
|
116
128
|
xflow logout forget the key of the active organization
|
|
117
129
|
xflow logout --all forget every key of this platform address
|
|
118
130
|
|
|
131
|
+
${(0, ui_1.bold)('members')} reads only. Inviting people, changing a role and removing somebody stay
|
|
132
|
+
in the platform interface, with the owner: there is no command for any of them.
|
|
133
|
+
|
|
119
134
|
The switch is local: it changes which stored key is used and nothing happens on
|
|
120
135
|
the platform. A folder bound to an organization is not affected, its commands
|
|
121
136
|
stay in its own organization: two projects of two organizations in two terminals
|
|
@@ -129,6 +144,37 @@ A project id is unique across the whole platform, so "project not found" under
|
|
|
129
144
|
the wrong organization can never touch somebody else's project. When that error
|
|
130
145
|
names a project you know exists, the key is simply from another organization:
|
|
131
146
|
check ${(0, ui_1.bold)('xflow org')}.`,
|
|
147
|
+
project: `${(0, ui_1.bold)('--project <id>')}: a project without its folder
|
|
148
|
+
|
|
149
|
+
Every command that only talks to the platform takes it, so a service project needs
|
|
150
|
+
no working copy of its own:
|
|
151
|
+
|
|
152
|
+
xflow db query --project 3f2a… "select id, title from tasks where done = false"
|
|
153
|
+
xflow functions invoke kanban --project 3f2a… --data-file card.json
|
|
154
|
+
|
|
155
|
+
Which commands: ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('functions list')},
|
|
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')}.
|
|
160
|
+
|
|
161
|
+
The rest read the working copy and have nothing to do with an id instead:
|
|
162
|
+
${(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)('db status')},
|
|
163
|
+
${(0, ui_1.bold)('env check')}, ${(0, ui_1.bold)('env set')}, ${(0, ui_1.bold)('connections unlink')}. They answer about the files
|
|
164
|
+
next to them, so they keep asking for a folder.
|
|
165
|
+
|
|
166
|
+
The folder is then ignored whole, and that includes its ${(0, ui_1.bold)('xflow.json')}: the address
|
|
167
|
+
is app.getxflow.com, or ${(0, ui_1.bold)('XFLOW_API_URL')} when it is set, and never the ${(0, ui_1.bold)('api')} field
|
|
168
|
+
of a config that happens to be lying around. Standing in the folder of a self-hosted
|
|
169
|
+
project, the flag therefore goes somewhere else than the folder does.
|
|
170
|
+
|
|
171
|
+
The id is echoed back on every run. It is the one place a project is named by hand,
|
|
172
|
+
and a wrong id answers "not found" on a read but works on a write.
|
|
173
|
+
|
|
174
|
+
With several organizations signed in, the one holding the project is looked for on
|
|
175
|
+
every call: there is no folder to write the answer into, and a second place to
|
|
176
|
+
remember it would be a second thing to keep true. One ${(0, ui_1.bold)('xflow org switch')} to the
|
|
177
|
+
right organization spares that search.`,
|
|
132
178
|
update: `${(0, ui_1.bold)('xflow update')}: bring the CLI up to date
|
|
133
179
|
|
|
134
180
|
Installs the published version and, if anything changed, rewrites the platform
|
|
@@ -155,10 +201,15 @@ will work.`,
|
|
|
155
201
|
db: `${(0, ui_1.bold)('xflow db')}: schema, data, migrations
|
|
156
202
|
|
|
157
203
|
xflow db status what is applied and what is waiting
|
|
204
|
+
xflow db list databases of the organization, and who is on them
|
|
158
205
|
xflow db schema [table] tables of the project, or the columns of one
|
|
159
206
|
xflow db query "select ..." read data (--limit N)
|
|
160
207
|
xflow db migrate apply migrations/*.sql
|
|
161
208
|
|
|
209
|
+
${(0, ui_1.bold)('list')} is about the organization, not about this project: it names the logical
|
|
210
|
+
databases, the identifier ${(0, ui_1.bold)('xflow init --database <id>')} asks for, and the projects
|
|
211
|
+
attached to each one. That last column is the one to read before a migration.
|
|
212
|
+
|
|
162
213
|
${(0, ui_1.bold)('schema')} answers a different question than the ${(0, ui_1.bold)('migrations/')} directory: one logical
|
|
163
214
|
database is shared by several projects, so the files say what you did, and the schema
|
|
164
215
|
says what is actually in there. ${(0, ui_1.bold)('query')} runs inside a READ ONLY transaction, so
|
|
@@ -259,7 +310,11 @@ the function. So keep your own copy wherever you got it from.
|
|
|
259
310
|
xflow env set SMTP_PASSWORD=… store
|
|
260
311
|
xflow env rm SMTP_PASSWORD delete
|
|
261
312
|
|
|
262
|
-
--scope project visible to this project only
|
|
313
|
+
--scope project visible to this project only
|
|
314
|
+
--scope organization visible to every project of the organization (the default)
|
|
315
|
+
|
|
316
|
+
Those two words are the whole list: a third is refused rather than read as the default,
|
|
317
|
+
because a value meant for one project would have gone out to all of them.
|
|
263
318
|
|
|
264
319
|
${(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
320
|
references. The same rule applies on deploy: a function receives only the variables it
|
|
@@ -326,6 +381,7 @@ the day-of-month and day-of-week fields has to be ${(0, ui_1.bold)('?')}, which
|
|
|
326
381
|
how Yandex works. ${(0, ui_1.bold)('The time is always UTC')}, local time is not understood.
|
|
327
382
|
|
|
328
383
|
--payload '{"mode":"full"}' the body the function will receive
|
|
384
|
+
--payload-file body.json the same body from a file
|
|
329
385
|
|
|
330
386
|
A scheduled run arrives at the handler as a POST with no headers, and the project
|
|
331
387
|
token is not checked on it: such an event cannot be forged from outside. The function
|
|
@@ -339,8 +395,13 @@ Calls it exactly the way the application does: the project token plus a visitor
|
|
|
339
395
|
for the person who owns the key, so the function sees a real caller and its role.
|
|
340
396
|
Both are taken from the platform, no local .env is needed.
|
|
341
397
|
|
|
342
|
-
--data '{"a":1}'
|
|
343
|
-
--
|
|
398
|
+
--data '{"a":1}' request body (the method becomes POST by default)
|
|
399
|
+
--data-file body.json the same body from a file
|
|
400
|
+
--method GET a different method
|
|
401
|
+
|
|
402
|
+
The body has to be JSON, and it is checked before the call goes out: PowerShell
|
|
403
|
+
strips double quotes from a native call, so ${(0, ui_1.bold)('--data')} needs them escaped there
|
|
404
|
+
(${(0, ui_1.bold)('--data \'{\\"a\\":1}\'')}) and a file needs nothing.
|
|
344
405
|
|
|
345
406
|
Prints the status, the response time and the body. A non-zero exit code on 4xx and
|
|
346
407
|
5xx: in CI such a call has to fail the step. The cause of a crash is shown by
|
|
@@ -460,7 +521,7 @@ is not a formality: your own palette on top of them looks foreign inside the pla
|
|
|
460
521
|
|
|
461
522
|
--name <name> project name (the folder name by default)
|
|
462
523
|
--template <id> a different template (the list: xflow templates)
|
|
463
|
-
--database <id> attach a logical database of the organization
|
|
524
|
+
--database <id> attach a logical database of the organization (the list: xflow db list)
|
|
464
525
|
|
|
465
526
|
The order: the project is created on the platform first, because without it there is
|
|
466
527
|
nowhere to get the token for .env. If writing the files fails, the project stays empty
|
package/dist/json-arg.js
ADDED
|
@@ -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/session.js
CHANGED
|
@@ -4,15 +4,24 @@ exports.connect = connect;
|
|
|
4
4
|
exports.anonymous = anonymous;
|
|
5
5
|
exports.findProjectOrg = findProjectOrg;
|
|
6
6
|
exports.connectProject = connectProject;
|
|
7
|
+
exports.isProjectId = isProjectId;
|
|
8
|
+
exports.projectTarget = projectTarget;
|
|
7
9
|
const api_1 = require("./api");
|
|
10
|
+
const args_1 = require("./args");
|
|
8
11
|
const config_1 = require("./config");
|
|
9
12
|
const credentials_1 = require("./credentials");
|
|
10
13
|
const errors_1 = require("./errors");
|
|
11
14
|
const ui_1 = require("./ui");
|
|
12
|
-
/**
|
|
13
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Platform connection: address plus key.
|
|
17
|
+
*
|
|
18
|
+
* `ignoreFolder` is what --project needs: the folder around it belongs to another
|
|
19
|
+
* project, so its organization must not decide the key. Taking it would hand the
|
|
20
|
+
* flag the key of the wrong organization, and the answer would be a bare 404.
|
|
21
|
+
*/
|
|
22
|
+
function connect(config, options) {
|
|
14
23
|
const apiUrl = (0, config_1.apiUrlFor)(config);
|
|
15
|
-
const root = (0, config_1.findProjectRoot)();
|
|
24
|
+
const root = options?.ignoreFolder === true ? null : (0, config_1.findProjectRoot)();
|
|
16
25
|
const picked = (0, credentials_1.pickKey)({
|
|
17
26
|
env: process.env.XFLOW_TOKEN?.trim() || null,
|
|
18
27
|
// XFLOW_TOKEN never follows an address taken from the repo's xflow.json:
|
|
@@ -63,23 +72,76 @@ async function findProjectOrg(apiUrl, projectId) {
|
|
|
63
72
|
* gets probed once: the organization that serves the project is found and
|
|
64
73
|
* written into .xflow/state.json, so every later command resolves the key
|
|
65
74
|
* synchronously. With one stored key there is nothing to probe.
|
|
75
|
+
*
|
|
76
|
+
* Without a folder (--project) there is nowhere to write the answer, so the
|
|
77
|
+
* probe repeats on every command. Kept that way on purpose: a second store of
|
|
78
|
+
* "which organization holds what" would be a second truth to keep in step, and
|
|
79
|
+
* the price is paid only by whoever did not switch the active organization.
|
|
66
80
|
*/
|
|
67
|
-
async function connectProject(root, config) {
|
|
68
|
-
const session = connect(config);
|
|
81
|
+
async function connectProject(projectId, root, config) {
|
|
82
|
+
const session = connect(config, { ignoreFolder: root === null });
|
|
69
83
|
if (session.source !== 'active' || session.organizationId === null)
|
|
70
84
|
return session;
|
|
71
85
|
const orgs = (0, credentials_1.listOrgs)(session.apiUrl);
|
|
72
86
|
if (orgs.length <= 1)
|
|
73
87
|
return session;
|
|
74
|
-
const { hit, dead } = await findProjectOrg(session.apiUrl,
|
|
88
|
+
const { hit, dead } = await findProjectOrg(session.apiUrl, projectId);
|
|
75
89
|
if (!hit) {
|
|
76
|
-
throw new errors_1.CliError(`No signed-in organization holds the project ${
|
|
90
|
+
throw new errors_1.CliError(`No signed-in organization holds the project ${projectId}`, dead.length > 0
|
|
77
91
|
? `The key of ${dead.map((d) => d.name ?? d.organizationId).join(', ')} is not working (sign in again: xflow login); the project may live there`
|
|
78
|
-
:
|
|
92
|
+
: root === null
|
|
93
|
+
? 'Check the id, or sign in to the organization that owns it: xflow login'
|
|
94
|
+
: 'Check projectId in xflow.json, or sign in to the organization that owns it: xflow login');
|
|
95
|
+
}
|
|
96
|
+
if (root !== null) {
|
|
97
|
+
(0, config_1.writeState)(root, { organizationId: hit.organizationId });
|
|
98
|
+
if (hit.organizationId !== session.organizationId) {
|
|
99
|
+
(0, ui_1.note)((0, ui_1.dim)(` The project belongs to "${hit.name ?? hit.organizationId}", not to the active organization: the folder is bound to it`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (hit.organizationId !== session.organizationId) {
|
|
103
|
+
// Nothing was written down, so the next command pays for the same search.
|
|
104
|
+
(0, ui_1.note)((0, ui_1.dim)(` The project belongs to "${hit.name ?? hit.organizationId}": xflow org switch spares the next command this search`));
|
|
79
105
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
return {
|
|
107
|
+
apiUrl: session.apiUrl,
|
|
108
|
+
token: hit.token,
|
|
109
|
+
source: root === null ? 'probe' : 'folder',
|
|
110
|
+
organizationId: hit.organizationId,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A project id, and nothing that could be mistaken for one. A name passed where
|
|
115
|
+
* the id belongs would answer a bare 404, and that reads as "no such project"
|
|
116
|
+
* rather than "wrong argument".
|
|
117
|
+
*/
|
|
118
|
+
function isProjectId(value) {
|
|
119
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The project a command works on: the --project flag, or the folder it runs in.
|
|
123
|
+
*
|
|
124
|
+
* The flag ignores the folder whole, its xflow.json included, so the address is
|
|
125
|
+
* the default one or XFLOW_API_URL and nothing else: a config lying around must
|
|
126
|
+
* not be able to redirect a call made by id.
|
|
127
|
+
*
|
|
128
|
+
* `probe` is the difference the folder path already has and keeps: publish,
|
|
129
|
+
* rollback and deployments look for the organization of an unbound folder,
|
|
130
|
+
* everything else takes the key it resolves to. Named by id there is no binding
|
|
131
|
+
* to lean on, so the search always happens.
|
|
132
|
+
*/
|
|
133
|
+
async function projectTarget(args, options) {
|
|
134
|
+
const named = (0, args_1.flagString)(args, 'project');
|
|
135
|
+
if (named !== undefined) {
|
|
136
|
+
if (!isProjectId(named)) {
|
|
137
|
+
throw new errors_1.CliError(`--project takes a project id, got "${named}"`, 'The ids: xflow projects list');
|
|
138
|
+
}
|
|
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.
|
|
141
|
+
(0, ui_1.note)((0, ui_1.dim)(` Project ${named} (--project)`));
|
|
142
|
+
return { projectId: named, client: await connectProject(named, null) };
|
|
83
143
|
}
|
|
84
|
-
|
|
144
|
+
const { root, config } = (0, config_1.requireProject)();
|
|
145
|
+
const client = options?.probe === true ? await connectProject(config.projectId, root, config) : connect(config);
|
|
146
|
+
return { projectId: config.projectId, client };
|
|
85
147
|
}
|
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.
|
|
5
|
+
exports.CLI_VERSION = '0.10.4';
|
|
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.10.
|
|
27
|
+
These instructions ship with xflow CLI 0.10.4. 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
|
-
|
|
239
|
-
output of that call. Only failed calls are
|
|
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.
|
|
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
|
|
|
@@ -386,7 +398,9 @@ place the affected tables are dumped first and kept for 7 days. Check with `--dr
|
|
|
386
398
|
|
|
387
399
|
One logical database can be shared by several projects, so your migration can break an app
|
|
388
400
|
you do not see, and `xflow db status` lists applied migrations that have no file in your
|
|
389
|
-
repository: that is somebody else's project.
|
|
401
|
+
repository: that is somebody else's project. `xflow db list` names the databases of the
|
|
402
|
+
organization and the projects on each, which is where you find out who else is on yours.
|
|
403
|
+
For the same reason `migrations/` is not the
|
|
390
404
|
schema. It says what you did; `xflow db schema [table]` says what is in the database right
|
|
391
405
|
now, and `xflow db query "select ..."` reads it inside a READ ONLY transaction. Look before
|
|
392
406
|
you write a migration against a shared database.
|
|
@@ -482,7 +496,9 @@ One key per organization, stored side by side rather than replacing each other:
|
|
|
482
496
|
|
|
483
497
|
Inside a project folder there is nothing to switch: commands follow the organization the
|
|
484
498
|
folder is bound to, whatever the active one is. That is what lets two projects of two
|
|
485
|
-
organizations work side by side.
|
|
499
|
+
organizations work side by side. The exception is `--project <id>`, which every command
|
|
500
|
+
that does not read the working copy takes (`xflow help project` lists them): it ignores the
|
|
501
|
+
folder whole, so a second project needs no second checkout.
|
|
486
502
|
|
|
487
503
|
A "not found" on a project you know exists usually means the key belongs to another
|
|
488
504
|
organization, not that the project is gone: ids are unique across the platform, so a
|