@getxflow/cli 0.10.3 → 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/bin.js +25 -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 +14 -11
- package/dist/commands/functions.js +6 -9
- package/dist/commands/logs.js +2 -4
- package/dist/commands/org.js +19 -0
- package/dist/commands/schedules.js +7 -11
- package/dist/commands/sources.js +2 -2
- package/dist/commands/storage.js +8 -12
- package/dist/flags.js +35 -28
- package/dist/help.js +47 -1
- package/dist/session.js +74 -12
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +7 -3
package/dist/bin.js
CHANGED
|
@@ -52,11 +52,15 @@ async function run(args) {
|
|
|
52
52
|
(0, org_1.orgSwitch)({ ...args, words: args.words.slice(2) });
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
+
if (second === 'members') {
|
|
56
|
+
await (0, org_1.orgMembers)();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
55
59
|
if (second === undefined || second === 'list') {
|
|
56
60
|
(0, org_1.orgList)();
|
|
57
61
|
return;
|
|
58
62
|
}
|
|
59
|
-
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');
|
|
60
64
|
case 'templates':
|
|
61
65
|
await (0, projects_1.templates)();
|
|
62
66
|
return;
|
|
@@ -95,7 +99,7 @@ async function run(args) {
|
|
|
95
99
|
return;
|
|
96
100
|
}
|
|
97
101
|
if (second === undefined || second === 'list') {
|
|
98
|
-
await (0, functions_1.functionsList)();
|
|
102
|
+
await (0, functions_1.functionsList)(rest);
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
105
|
throw new errors_1.CliError(`Unknown command: functions ${second}`, 'Available: list, invoke and logs');
|
|
@@ -116,7 +120,7 @@ async function run(args) {
|
|
|
116
120
|
return;
|
|
117
121
|
}
|
|
118
122
|
if (second === undefined || second === 'list') {
|
|
119
|
-
await (0, env_1.envList)();
|
|
123
|
+
await (0, env_1.envList)(rest);
|
|
120
124
|
return;
|
|
121
125
|
}
|
|
122
126
|
throw new errors_1.CliError(`Unknown command: env ${second}`, 'Available: list, check, set and rm');
|
|
@@ -130,7 +134,7 @@ async function run(args) {
|
|
|
130
134
|
return;
|
|
131
135
|
}
|
|
132
136
|
if (second === undefined || second === 'list') {
|
|
133
|
-
await (0, connections_1.connectionsList)();
|
|
137
|
+
await (0, connections_1.connectionsList)(rest);
|
|
134
138
|
return;
|
|
135
139
|
}
|
|
136
140
|
throw new errors_1.CliError(`Unknown command: connections ${second}`, 'Available: list, link and unlink');
|
|
@@ -144,7 +148,7 @@ async function run(args) {
|
|
|
144
148
|
return;
|
|
145
149
|
}
|
|
146
150
|
if (second === undefined || second === 'list') {
|
|
147
|
-
await (0, schedules_1.schedulesList)();
|
|
151
|
+
await (0, schedules_1.schedulesList)(rest);
|
|
148
152
|
return;
|
|
149
153
|
}
|
|
150
154
|
throw new errors_1.CliError(`Unknown command: schedules ${second}`, 'Available: list, set and rm');
|
|
@@ -153,6 +157,10 @@ async function run(args) {
|
|
|
153
157
|
await (0, db_1.dbMigrate)(rest);
|
|
154
158
|
return;
|
|
155
159
|
}
|
|
160
|
+
if (second === 'list') {
|
|
161
|
+
await (0, db_1.dbList)();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
156
164
|
if (second === 'schema') {
|
|
157
165
|
await (0, db_1.dbSchema)({ ...args, words: args.words.slice(2) });
|
|
158
166
|
return;
|
|
@@ -165,7 +173,7 @@ async function run(args) {
|
|
|
165
173
|
await (0, db_1.dbStatus)();
|
|
166
174
|
return;
|
|
167
175
|
}
|
|
168
|
-
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');
|
|
169
177
|
case 'storage':
|
|
170
178
|
if (second === 'push') {
|
|
171
179
|
await (0, storage_1.storagePush)(rest);
|
|
@@ -190,13 +198,13 @@ async function run(args) {
|
|
|
190
198
|
await (0, deploy_1.deploy)(rest);
|
|
191
199
|
return;
|
|
192
200
|
case 'publish':
|
|
193
|
-
await (0, deploy_1.publish)();
|
|
201
|
+
await (0, deploy_1.publish)(rest);
|
|
194
202
|
return;
|
|
195
203
|
case 'rollback':
|
|
196
204
|
await (0, deploy_1.rollback)(rest);
|
|
197
205
|
return;
|
|
198
206
|
case 'deployments':
|
|
199
|
-
await (0, deploy_1.deployments)();
|
|
207
|
+
await (0, deploy_1.deployments)(rest);
|
|
200
208
|
return;
|
|
201
209
|
case 'update':
|
|
202
210
|
(0, update_1.update)();
|
|
@@ -267,8 +275,15 @@ async function main() {
|
|
|
267
275
|
(0, ui_1.note)((0, ui_1.dim)(` ${(0, limits_1.limitLine)(e.limit)}`));
|
|
268
276
|
if (e.hint)
|
|
269
277
|
(0, ui_1.note)((0, ui_1.dim)(` ${e.hint}`));
|
|
270
|
-
if (e.code === 'not_found'
|
|
271
|
-
|
|
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
|
+
}
|
|
272
287
|
}
|
|
273
288
|
quiet = quiet || e.code === 'outdated_cli';
|
|
274
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');
|
|
@@ -177,12 +181,11 @@ async function envSet(args) {
|
|
|
177
181
|
}
|
|
178
182
|
}
|
|
179
183
|
async function envRemove(args) {
|
|
180
|
-
const { config } = (0, config_1.requireProject)();
|
|
181
|
-
const client = (0, session_1.connect)(config);
|
|
182
184
|
const name = args.words[1];
|
|
183
185
|
if (!name)
|
|
184
186
|
throw new errors_1.CliError('A variable name is required', 'What is stored: xflow env');
|
|
185
|
-
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' });
|
|
186
189
|
if (!result.removed) {
|
|
187
190
|
(0, ui_1.note)(`There is no variable ${name}`);
|
|
188
191
|
return;
|
|
@@ -4,7 +4,6 @@ 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");
|
|
9
8
|
const json_arg_1 = require("../json-arg");
|
|
10
9
|
const session_1 = require("../session");
|
|
@@ -16,10 +15,9 @@ function accessLabel(fn) {
|
|
|
16
15
|
return `external (${fn.external_keys} key${fn.external_keys > 1 ? 's' : ''})`;
|
|
17
16
|
}
|
|
18
17
|
const FUNCTIONS_DIR = 'functions';
|
|
19
|
-
async function functionsList() {
|
|
20
|
-
const {
|
|
21
|
-
const
|
|
22
|
-
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`);
|
|
23
21
|
if (data.functions.length === 0) {
|
|
24
22
|
(0, ui_1.note)(`No functions. Put the code in ${FUNCTIONS_DIR}/<name>/index.ts and run xflow deploy`);
|
|
25
23
|
return;
|
|
@@ -42,8 +40,7 @@ function prettyBody(text) {
|
|
|
42
40
|
}
|
|
43
41
|
/** Call a function the same way the application does. */
|
|
44
42
|
async function functionsInvoke(args) {
|
|
45
|
-
const {
|
|
46
|
-
const client = (0, session_1.connect)(config);
|
|
43
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
47
44
|
const name = args.words[1];
|
|
48
45
|
if (!name)
|
|
49
46
|
throw new errors_1.CliError('A function name is required', 'What is deployed: xflow functions list');
|
|
@@ -52,7 +49,7 @@ async function functionsInvoke(args) {
|
|
|
52
49
|
const data = (0, json_arg_1.jsonArg)(args, 'data');
|
|
53
50
|
const method = ((0, args_1.flagString)(args, 'method') ?? (data ? 'POST' : 'GET')).toUpperCase();
|
|
54
51
|
const sendsBody = method !== 'GET' && method !== 'HEAD';
|
|
55
|
-
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${
|
|
52
|
+
const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
|
|
56
53
|
const fn = card.functions.find((item) => item.name === name);
|
|
57
54
|
if (!fn || !fn.invoke_url) {
|
|
58
55
|
throw new errors_1.CliError(`Function ${name} is not deployed`, card.functions.length > 0
|
|
@@ -64,7 +61,7 @@ async function functionsInvoke(args) {
|
|
|
64
61
|
// A read-only key cannot get a pass, so the call still goes out without it.
|
|
65
62
|
let pass = '';
|
|
66
63
|
try {
|
|
67
|
-
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' });
|
|
68
65
|
pass = issued.pass;
|
|
69
66
|
}
|
|
70
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 config_1 = require("../config");
|
|
8
7
|
const errors_1 = require("../errors");
|
|
9
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,15 +24,14 @@ 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
32
|
const payload = (0, json_arg_1.jsonArg)(args, 'payload') ?? null;
|
|
37
|
-
const
|
|
33
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
34
|
+
const row = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/schedules`, {
|
|
38
35
|
method: 'POST',
|
|
39
36
|
body: { function: functionName, cron, payload },
|
|
40
37
|
});
|
|
@@ -42,8 +39,6 @@ async function schedulesSet(args) {
|
|
|
42
39
|
(0, ui_1.note)((0, ui_1.dim)(` The time is UTC. To check by hand: xflow functions invoke ${row.function}`));
|
|
43
40
|
}
|
|
44
41
|
async function schedulesRemove(args) {
|
|
45
|
-
const { config } = (0, config_1.requireProject)();
|
|
46
|
-
const client = (0, session_1.connect)(config);
|
|
47
42
|
const functionName = args.words[1];
|
|
48
43
|
if (!functionName) {
|
|
49
44
|
throw new errors_1.CliError('A function name is required', 'What runs on a timer: xflow schedules list');
|
|
@@ -52,7 +47,8 @@ async function schedulesRemove(args) {
|
|
|
52
47
|
const query = new URLSearchParams({ function: functionName });
|
|
53
48
|
if (cron)
|
|
54
49
|
query.set('cron', cron);
|
|
55
|
-
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' });
|
|
56
52
|
if (result.removed === 0) {
|
|
57
53
|
(0, ui_1.note)(`Function ${functionName} has no such schedule`);
|
|
58
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
CHANGED
|
@@ -15,6 +15,11 @@ const help_1 = require("./help");
|
|
|
15
15
|
* Keys are the command as it is typed. A command that is not here is not checked,
|
|
16
16
|
* so a new one keeps working until it is added, and the list also drives the hint:
|
|
17
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.
|
|
18
23
|
*/
|
|
19
24
|
const KNOWN = {
|
|
20
25
|
help: [],
|
|
@@ -23,6 +28,7 @@ const KNOWN = {
|
|
|
23
28
|
whoami: [],
|
|
24
29
|
org: [],
|
|
25
30
|
'org list': [],
|
|
31
|
+
'org members': [],
|
|
26
32
|
'org switch': [],
|
|
27
33
|
templates: [],
|
|
28
34
|
init: ['name', 'template', 'database'],
|
|
@@ -33,43 +39,44 @@ const KNOWN = {
|
|
|
33
39
|
skills: ['refresh', 'global', 'agent', 'yes'],
|
|
34
40
|
mcp: ['show-token'],
|
|
35
41
|
'mcp install': ['show-token'],
|
|
36
|
-
functions: [],
|
|
37
|
-
'functions list': [],
|
|
38
|
-
'functions invoke': ['data', 'data-file', 'method'],
|
|
39
|
-
'functions logs': ['limit'],
|
|
40
|
-
logs: ['limit'],
|
|
41
|
-
env: [],
|
|
42
|
-
'env list': [],
|
|
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'],
|
|
43
49
|
'env set': ['scope'],
|
|
44
|
-
'env rm': [],
|
|
45
|
-
'env remove': [],
|
|
50
|
+
'env rm': ['project'],
|
|
51
|
+
'env remove': ['project'],
|
|
46
52
|
'env check': [],
|
|
47
|
-
connections: [],
|
|
48
|
-
'connections list': [],
|
|
49
|
-
'connections link': ['as'],
|
|
53
|
+
connections: ['project'],
|
|
54
|
+
'connections list': ['project'],
|
|
55
|
+
'connections link': ['as', 'project'],
|
|
50
56
|
'connections unlink': ['force'],
|
|
51
|
-
schedules: [],
|
|
52
|
-
'schedules list': [],
|
|
53
|
-
'schedules set': ['payload', 'payload-file'],
|
|
54
|
-
'schedules rm': [],
|
|
55
|
-
'schedules remove': [],
|
|
57
|
+
schedules: ['project'],
|
|
58
|
+
'schedules list': ['project'],
|
|
59
|
+
'schedules set': ['payload', 'payload-file', 'project'],
|
|
60
|
+
'schedules rm': ['project'],
|
|
61
|
+
'schedules remove': ['project'],
|
|
56
62
|
db: [],
|
|
57
63
|
'db status': [],
|
|
58
|
-
'db
|
|
59
|
-
'db
|
|
64
|
+
'db list': [],
|
|
65
|
+
'db schema': ['project'],
|
|
66
|
+
'db query': ['limit', 'project'],
|
|
60
67
|
'db migrate': ['dry-run', 'allow-destructive'],
|
|
61
|
-
storage: ['json'],
|
|
62
|
-
'storage ls': ['json'],
|
|
63
|
-
'storage list': ['json'],
|
|
64
|
-
'storage push': ['to', 'replace', 'json'],
|
|
65
|
-
'storage rm': ['folder', 'yes'],
|
|
66
|
-
'storage remove': ['folder', 'yes'],
|
|
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'],
|
|
67
74
|
status: [],
|
|
68
75
|
pull: ['into', 'force', 'revision'],
|
|
69
76
|
deploy: ['no-push', 'force', 'allow-removals'],
|
|
70
|
-
publish: [],
|
|
71
|
-
rollback: [],
|
|
72
|
-
deployments: [],
|
|
77
|
+
publish: ['project'],
|
|
78
|
+
rollback: ['project'],
|
|
79
|
+
deployments: ['project'],
|
|
73
80
|
update: [],
|
|
74
81
|
};
|
|
75
82
|
/** Answered everywhere: both are handled before the command runs. */
|
package/dist/help.js
CHANGED
|
@@ -82,6 +82,7 @@ ${(0, ui_1.bold)('Functions')}
|
|
|
82
82
|
|
|
83
83
|
${(0, ui_1.bold)('Database')}
|
|
84
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
|
|
85
86
|
xflow db schema [table] tables of the project, or the columns of one
|
|
86
87
|
xflow db query "select ..." read data, in a read-only transaction
|
|
87
88
|
xflow db migrate [--dry-run] [--allow-destructive]
|
|
@@ -92,12 +93,17 @@ ${(0, ui_1.bold)('Reference')}
|
|
|
92
93
|
xflow projects list projects of the organization
|
|
93
94
|
xflow projects get [id] project card
|
|
94
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
|
|
95
97
|
xflow org switch <name|id> make another organization the active one
|
|
96
98
|
xflow whoami whose key this is and what it can do
|
|
97
99
|
xflow logout [--all] forget the key of the active organization (--all: every one)
|
|
98
100
|
xflow update update the CLI itself, and the skill that ships with it
|
|
99
101
|
xflow --version which version is installed
|
|
100
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
|
+
|
|
101
107
|
${(0, ui_1.bold)('Environment')}
|
|
102
108
|
XFLOW_TOKEN access key (for CI, instead of xflow login)
|
|
103
109
|
XFLOW_API_URL platform address when it is not app.getxflow.com
|
|
@@ -117,10 +123,14 @@ Which key a command then uses, in order:
|
|
|
117
123
|
3. the active organization
|
|
118
124
|
|
|
119
125
|
xflow org the stored organizations, the active one marked
|
|
126
|
+
xflow org members who is in this one, and with which role
|
|
120
127
|
xflow org switch <name|id> make another one active, no browser involved
|
|
121
128
|
xflow logout forget the key of the active organization
|
|
122
129
|
xflow logout --all forget every key of this platform address
|
|
123
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
|
+
|
|
124
134
|
The switch is local: it changes which stored key is used and nothing happens on
|
|
125
135
|
the platform. A folder bound to an organization is not affected, its commands
|
|
126
136
|
stay in its own organization: two projects of two organizations in two terminals
|
|
@@ -134,6 +144,37 @@ A project id is unique across the whole platform, so "project not found" under
|
|
|
134
144
|
the wrong organization can never touch somebody else's project. When that error
|
|
135
145
|
names a project you know exists, the key is simply from another organization:
|
|
136
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.`,
|
|
137
178
|
update: `${(0, ui_1.bold)('xflow update')}: bring the CLI up to date
|
|
138
179
|
|
|
139
180
|
Installs the published version and, if anything changed, rewrites the platform
|
|
@@ -160,10 +201,15 @@ will work.`,
|
|
|
160
201
|
db: `${(0, ui_1.bold)('xflow db')}: schema, data, migrations
|
|
161
202
|
|
|
162
203
|
xflow db status what is applied and what is waiting
|
|
204
|
+
xflow db list databases of the organization, and who is on them
|
|
163
205
|
xflow db schema [table] tables of the project, or the columns of one
|
|
164
206
|
xflow db query "select ..." read data (--limit N)
|
|
165
207
|
xflow db migrate apply migrations/*.sql
|
|
166
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
|
+
|
|
167
213
|
${(0, ui_1.bold)('schema')} answers a different question than the ${(0, ui_1.bold)('migrations/')} directory: one logical
|
|
168
214
|
database is shared by several projects, so the files say what you did, and the schema
|
|
169
215
|
says what is actually in there. ${(0, ui_1.bold)('query')} runs inside a READ ONLY transaction, so
|
|
@@ -475,7 +521,7 @@ is not a formality: your own palette on top of them looks foreign inside the pla
|
|
|
475
521
|
|
|
476
522
|
--name <name> project name (the folder name by default)
|
|
477
523
|
--template <id> a different template (the list: xflow templates)
|
|
478
|
-
--database <id> attach a logical database of the organization
|
|
524
|
+
--database <id> attach a logical database of the organization (the list: xflow db list)
|
|
479
525
|
|
|
480
526
|
The order: the project is created on the platform first, because without it there is
|
|
481
527
|
nowhere to get the token for .env. If writing the files fails, the project stays empty
|
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
|
|
|
@@ -398,7 +398,9 @@ place the affected tables are dumped first and kept for 7 days. Check with `--dr
|
|
|
398
398
|
|
|
399
399
|
One logical database can be shared by several projects, so your migration can break an app
|
|
400
400
|
you do not see, and `xflow db status` lists applied migrations that have no file in your
|
|
401
|
-
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
|
|
402
404
|
schema. It says what you did; `xflow db schema [table]` says what is in the database right
|
|
403
405
|
now, and `xflow db query "select ..."` reads it inside a READ ONLY transaction. Look before
|
|
404
406
|
you write a migration against a shared database.
|
|
@@ -494,7 +496,9 @@ One key per organization, stored side by side rather than replacing each other:
|
|
|
494
496
|
|
|
495
497
|
Inside a project folder there is nothing to switch: commands follow the organization the
|
|
496
498
|
folder is bound to, whatever the active one is. That is what lets two projects of two
|
|
497
|
-
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.
|
|
498
502
|
|
|
499
503
|
A "not found" on a project you know exists usually means the key belongs to another
|
|
500
504
|
organization, not that the project is gone: ids are unique across the platform, so a
|