@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 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' && (0, credentials_1.anyMultipleOrgs)()) {
271
- (0, ui_1.note)((0, ui_1.dim)(' Several organizations are signed in: the answer may live in another one, see xflow org'));
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 { config } = (0, config_1.requireProject)();
61
- const client = (0, session_1.connect)(config);
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 { config } = (0, config_1.requireProject)();
96
- const client = (0, session_1.connect)(config);
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(config.projectId), { method: 'POST', body: { connection_id: connectionId, alias } });
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(', ')}`));
@@ -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 { config } = (0, config_1.requireProject)();
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/${config.projectId}/db/schema${wanted ? `?table=${encodeURIComponent(wanted)}` : ''}`;
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.connect)(config);
143
- const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/query`, {
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,
@@ -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 { root, config } = (0, config_1.requireProject)();
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/${config.projectId}/publish`, { method: 'POST', timeoutMs: 120_000 });
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.connectProject)(root, config);
195
- const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/rollback`, { method: 'POST', body: { deploy_id: deployId } });
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 { root, config } = (0, config_1.requireProject)();
208
- const client = await (0, session_1.connectProject)(root, config);
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;
@@ -82,14 +82,17 @@ function referencedByFunctions(root) {
82
82
  }
83
83
  return { needed, provided, opaque };
84
84
  }
85
- async function fetchVariables() {
86
- const { root, config } = (0, config_1.requireProject)();
87
- const client = (0, session_1.connect)(config);
88
- const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env`);
89
- return { names: new Set(data.variables.map((row) => row.name)), rows: data.variables, root };
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 { rows } = await fetchVariables();
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 { names, root } = await fetchVariables();
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 result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/env?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
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 { config } = (0, config_1.requireProject)();
21
- const client = (0, session_1.connect)(config);
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 { config } = (0, config_1.requireProject)();
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/${config.projectId}`);
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/${config.projectId}/pass`, { method: 'POST' });
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 {
@@ -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 { config } = (0, config_1.requireProject)();
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/${config.projectId}/logs?${query.toString()}`);
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. */
@@ -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 { config } = (0, config_1.requireProject)();
14
- const client = (0, session_1.connect)(config);
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 row = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules`, {
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 result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/schedules?${query.toString()}`, { method: 'DELETE' });
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;
@@ -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([
@@ -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 { config } = (0, config_1.requireProject)();
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, config.projectId, folder);
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, config.projectId, target)).map((f) => [f.path, f]));
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/${config.projectId}/storage/upload-url`, {
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/${config.projectId}/storage/confirm`, {
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/${config.projectId}/storage?${query.toString()}`, { method: 'DELETE' });
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 schema': [],
59
- 'db query': ['limit'],
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
- /** Platform connection: address plus key. */
13
- function connect(config) {
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, config.projectId);
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 ${config.projectId}`, dead.length > 0
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
- : 'Check projectId in xflow.json, or sign in to the organization that owns it: xflow login');
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
- (0, config_1.writeState)(root, { organizationId: hit.organizationId });
81
- if (hit.organizationId !== session.organizationId) {
82
- (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`));
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
- return { apiUrl: session.apiUrl, token: hit.token, source: 'folder', organizationId: hit.organizationId };
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.3';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -24,7 +24,7 @@ contains the fix.
24
24
 
25
25
  ## Keeping these instructions current
26
26
 
27
- These instructions ship with xflow CLI 0.10.3. They travel inside the package, so the copy
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. For the same reason `migrations/` is not the
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