@getxflow/cli 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApiError = void 0;
4
+ exports.isOlder = isOlder;
4
5
  exports.newerVersionSeen = newerVersionSeen;
5
6
  exports.apiJson = apiJson;
6
7
  exports.apiBinary = apiBinary;
package/dist/bin.js CHANGED
@@ -170,7 +170,7 @@ async function run(args) {
170
170
  return;
171
171
  }
172
172
  if (second === undefined || second === 'status') {
173
- await (0, db_1.dbStatus)();
173
+ await (0, db_1.dbStatus)({ ...args, words: args.words.slice(2) });
174
174
  return;
175
175
  }
176
176
  throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query and migrate');
@@ -227,13 +227,15 @@ function nudge() {
227
227
  }
228
228
  /**
229
229
  * The other staleness, and the one that costs more: the instructions an agent reads are
230
- * older than the CLI answering it, and nothing about that is visible in the text. Louder
231
- * than the notice above because it is wrong now rather than out of date soon, and it names
232
- * the file, because a machine can hold a dozen copies and the wrong one gets edited.
230
+ * not the ones the CLI answering it ships, and nothing about that is visible in the text.
231
+ * Louder than the notice above because it is wrong now rather than out of date soon, and
232
+ * it names the file, because a machine can hold a dozen copies and the wrong one gets edited.
233
233
  *
234
234
  * The command is skills --refresh, not update: what is out of step here is the copies, not
235
235
  * the package, and refreshing needs no network, no npm and no rights on a global folder.
236
- * The re-read is half the repair, so it is in the same breath as the command.
236
+ * The re-read is half the repair, so it is in the same breath as the command. Except when
237
+ * a newer CLI is known: then the package itself is behind, a refresh would rewrite the
238
+ * copies from it, and the update notice above is the whole advice.
237
239
  */
238
240
  function nudgeSkill() {
239
241
  const stale = (0, skills_1.staleSkillCopy)(process.cwd());
@@ -243,10 +245,17 @@ function nudgeSkill() {
243
245
  (0, state_1.clearSkillNudges)();
244
246
  return;
245
247
  }
248
+ // A newer CLI is published: the copy that differs may well be ahead of this package,
249
+ // and a refresh would "repair" it backwards. The update notice is the whole advice —
250
+ // xflow update brings the package and every copy along. Network-free commands see no
251
+ // headers, so the version an earlier notice named counts as known too.
252
+ const nudged = (0, state_1.lastNudgedVersion)();
253
+ if ((0, api_1.newerVersionSeen)() !== null || (nudged !== null && (0, api_1.isOlder)(version_1.CLI_VERSION, nudged)))
254
+ return;
246
255
  const key = `${stale} ${version_1.CLI_VERSION}`;
247
256
  if (!(0, state_1.shouldNudgeSkill)(key))
248
257
  return;
249
- (0, ui_1.warn)(`${stale} is older than this CLI`);
258
+ (0, ui_1.warn)(`${stale} does not match this CLI`);
250
259
  (0, ui_1.note)((0, ui_1.dim)(' Run xflow skills --refresh, then read that file again: the refresh only writes it to disk'));
251
260
  (0, state_1.markSkillNudged)(key);
252
261
  }
@@ -304,8 +313,8 @@ async function main() {
304
313
  if (!quiet)
305
314
  nudge();
306
315
  // Not behind the same flag: a copy out of step with the running package stays wrong
307
- // whatever the command was, and when the package itself is the problem the copies
308
- // match it and this says nothing anyway.
316
+ // whatever the command was, and when the package itself is the problem this steps
317
+ // aside on its own.
309
318
  nudgeSkill();
310
319
  }
311
320
  }
@@ -39,7 +39,22 @@ function requireMigrations(root) {
39
39
  }
40
40
  return migrations;
41
41
  }
42
- async function dbStatus() {
42
+ async function dbStatus(args) {
43
+ // Named by id there is no migrations/ directory to compare against: the answer
44
+ // is the applied history alone, which does not depend on any working copy.
45
+ if ((0, args_1.flagString)(args, 'project') !== undefined) {
46
+ const { projectId, client } = await (0, session_1.projectTarget)(args);
47
+ const history = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/migrate`);
48
+ (0, ui_1.out)(`Schema: ${(0, ui_1.bold)(history.schema)}`);
49
+ (0, ui_1.out)('');
50
+ if (history.applied.length === 0) {
51
+ (0, ui_1.note)('No migrations have been applied yet');
52
+ return;
53
+ }
54
+ (0, ui_1.table)(history.applied.map((row) => [row.name, (0, ui_1.formatAge)(row.applied_at)]));
55
+ (0, ui_1.note)((0, ui_1.dim)(' Applied history only: comparing against migrations/ needs the project folder'));
56
+ return;
57
+ }
43
58
  const { root, config } = (0, config_1.requireProject)();
44
59
  const client = (0, session_1.connect)(config);
45
60
  const local = readMigrations(root);
@@ -106,10 +121,13 @@ async function dbList() {
106
121
  }
107
122
  /** Long values would tear the columns apart; the cut is marked, never silent. */
108
123
  const CELL_LIMIT = 60;
109
- function cell(value) {
124
+ function plain(value) {
110
125
  if (value === null || value === undefined)
111
126
  return '-';
112
- const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
127
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
128
+ }
129
+ function cell(value) {
130
+ const text = plain(value);
113
131
  return text.length > CELL_LIMIT ? `${text.slice(0, CELL_LIMIT - 1)}…` : text;
114
132
  }
115
133
  /**
@@ -171,11 +189,28 @@ async function dbQuery(args) {
171
189
  body: { sql, limit: (0, args_1.flagNumber)(args, 'limit') },
172
190
  timeoutMs: 60_000,
173
191
  });
192
+ // The whole values, machine-readable, on stdout alone: the notes go to stderr.
193
+ if ((0, args_1.flagBool)(args, 'json')) {
194
+ (0, ui_1.out)(JSON.stringify({
195
+ schema: data.schema,
196
+ columns: data.columns,
197
+ rows: data.rows,
198
+ truncated: data.truncated,
199
+ limit: data.limit,
200
+ limit_max: data.limit_max,
201
+ }, null, 2));
202
+ return;
203
+ }
174
204
  if (data.rows.length === 0) {
175
205
  (0, ui_1.note)('No rows');
176
206
  return;
177
207
  }
178
208
  (0, ui_1.table)([data.columns, ...data.rows.map((row) => data.columns.map((column) => cell(row[column])))]);
209
+ // Two different cuts, named apart: this one is about the width of a cell, the
210
+ // one below is about the number of rows.
211
+ if (data.rows.some((row) => data.columns.some((column) => plain(row[column]).length > CELL_LIMIT))) {
212
+ (0, ui_1.note)((0, ui_1.dim)(` Values longer than ${CELL_LIMIT} characters are cut to keep the columns readable: --json prints them whole`));
213
+ }
179
214
  if (data.truncated) {
180
215
  // No exact total here: the platform stops reading at limit + 1 rows, so all
181
216
  // it knows is that more exist. The "raise --limit" advice shows only while
@@ -10,6 +10,7 @@ const node_path_1 = require("node:path");
10
10
  const api_1 = require("../api");
11
11
  const args_1 = require("../args");
12
12
  const config_1 = require("../config");
13
+ const credentials_1 = require("../credentials");
13
14
  const errors_1 = require("../errors");
14
15
  const session_1 = require("../session");
15
16
  const skills_1 = require("./skills");
@@ -123,6 +124,9 @@ async function link(args) {
123
124
  if (!projectId) {
124
125
  throw new errors_1.CliError('A project identifier is required', 'The list: xflow projects list');
125
126
  }
127
+ if (!(0, session_1.isProjectId)(projectId)) {
128
+ throw new errors_1.CliError(`link takes a project id, got "${projectId}"`, 'The ids: xflow projects list');
129
+ }
126
130
  let card;
127
131
  try {
128
132
  card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
@@ -192,25 +196,44 @@ async function list() {
192
196
  const root = (0, config_1.findProjectRoot)();
193
197
  const client = (0, session_1.connect)(root ? (0, config_1.readConfig)(root) : undefined);
194
198
  const { projects } = await (0, api_1.apiJson)(client, '/api/v1/projects');
195
- if (projects.length === 0) {
199
+ if (projects.length > 0) {
200
+ (0, ui_1.table)(projects.map((p) => [
201
+ p.id,
202
+ p.name,
203
+ p.live_deploy_id ? 'published' : p.dev_deploy_id ? 'dev only' : 'no build',
204
+ (0, ui_1.formatAge)(p.updated_at),
205
+ ]));
206
+ }
207
+ else {
196
208
  (0, ui_1.note)('No projects. To create one: xflow init');
197
- return;
198
209
  }
199
- (0, ui_1.table)(projects.map((p) => [
200
- p.id,
201
- p.name,
202
- p.live_deploy_id ? 'published' : p.dev_deploy_id ? 'dev only' : 'no build',
203
- (0, ui_1.formatAge)(p.updated_at),
204
- ]));
210
+ // One organization answers, and with several signed in that is worth saying:
211
+ // a project that "is not here" may simply live in another one. Not under an
212
+ // env key or a folder binding, where "active" would be the wrong word.
213
+ const orgs = (0, credentials_1.listOrgs)(client.apiUrl);
214
+ if (client.source === 'active' && orgs.length > 1) {
215
+ const name = orgs.find((org) => org.active)?.name;
216
+ (0, ui_1.note)((0, ui_1.dim)(` Projects of ${name ? `"${name}"` : 'the active organization'} alone: the rest of xflow org answer after org switch`));
217
+ }
205
218
  }
206
219
  async function get(args) {
207
220
  const root = (0, config_1.findProjectRoot)();
208
221
  const config = root ? (0, config_1.readConfig)(root) : undefined;
209
- const projectId = args.words[0] ?? config?.projectId;
222
+ const explicit = args.words[0];
223
+ if (explicit !== undefined && !(0, session_1.isProjectId)(explicit)) {
224
+ throw new errors_1.CliError(`projects get takes a project id, got "${explicit}"`, 'The ids: xflow projects list');
225
+ }
226
+ const projectId = explicit ?? config?.projectId;
210
227
  if (!projectId) {
211
228
  throw new errors_1.CliError('A project identifier is required', 'Or run the command inside the project folder');
212
229
  }
213
- const client = (0, session_1.connect)(config);
230
+ // An id typed by hand is looked for across every signed-in organization and
231
+ // ignores the folder whole, xflow.json included, the way --project does it.
232
+ // Named by the folder, the project of an unbound one gets probed and the
233
+ // binding written, the way publish and pull do it.
234
+ const client = explicit !== undefined
235
+ ? await (0, session_1.connectProject)(explicit, null)
236
+ : await (0, session_1.connectProject)(projectId, root, config);
214
237
  const card = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`);
215
238
  (0, ui_1.out)(`${(0, ui_1.bold)(card.name)} ${(0, ui_1.dim)(card.id)}`);
216
239
  if (card.description)
package/dist/flags.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KNOWN = void 0;
3
4
  exports.checkFlags = checkFlags;
4
5
  const args_1 = require("./args");
5
6
  const errors_1 = require("./errors");
@@ -17,11 +18,17 @@ const help_1 = require("./help");
17
18
  * whoever is at the keyboard gets the flags of this very command back.
18
19
  *
19
20
  * `--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
21
+ * only when that command can answer without the working copy. The ones that cannot
22
+ * (deploy, pull, status, db migrate, env check, env set, connections unlink) have
22
23
  * a folder to stand in, and naming a project they cannot read would be a lie.
24
+ * `db status` sits halfway: named by id it answers with the applied history alone,
25
+ * without the comparison against migrations/.
26
+ *
27
+ * flags-sync.test.ts holds this table against the code: a flag listed here has to
28
+ * be read by the handler of its command, and a flag the handler reads has to be
29
+ * listed here. A new command or a new way of reading flags shows up there first.
23
30
  */
24
- const KNOWN = {
31
+ exports.KNOWN = {
25
32
  help: [],
26
33
  login: [],
27
34
  logout: ['all'],
@@ -59,11 +66,11 @@ const KNOWN = {
59
66
  'schedules set': ['payload', 'payload-file', 'project'],
60
67
  'schedules rm': ['project'],
61
68
  'schedules remove': ['project'],
62
- db: [],
63
- 'db status': [],
69
+ db: ['project'],
70
+ 'db status': ['project'],
64
71
  'db list': [],
65
72
  'db schema': ['project'],
66
- 'db query': ['limit', 'project'],
73
+ 'db query': ['limit', 'json', 'project'],
67
74
  'db migrate': ['dry-run', 'allow-destructive'],
68
75
  storage: ['json', 'project'],
69
76
  'storage ls': ['json', 'project'],
@@ -103,18 +110,18 @@ function commandOf(words) {
103
110
  if (!first)
104
111
  return null;
105
112
  if (second !== undefined) {
106
- if (Object.hasOwn(KNOWN, `${first} ${second}`))
113
+ if (Object.hasOwn(exports.KNOWN, `${first} ${second}`))
107
114
  return `${first} ${second}`;
108
115
  if (NAMESPACES.has(first))
109
116
  return null;
110
117
  }
111
- return Object.hasOwn(KNOWN, first) ? first : null;
118
+ return Object.hasOwn(exports.KNOWN, first) ? first : null;
112
119
  }
113
120
  function checkFlags(args) {
114
121
  const command = commandOf(args.words);
115
122
  if (command === null)
116
123
  return;
117
- const allowed = KNOWN[command];
124
+ const allowed = exports.KNOWN[command];
118
125
  const topic = command.split(' ')[0];
119
126
  // Not every command has a page, and sending the reader to "No page for login"
120
127
  // would be a second dead end in the same breath as the first.
package/dist/help.js CHANGED
@@ -101,8 +101,8 @@ ${(0, ui_1.bold)('Reference')}
101
101
  xflow --version which version is installed
102
102
 
103
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)
104
+ --project <id> name the project by id, for every command that can
105
+ answer without the local files (xflow help project)
106
106
 
107
107
  ${(0, ui_1.bold)('Environment')}
108
108
  XFLOW_TOKEN access key (for CI, instead of xflow login)
@@ -152,21 +152,26 @@ no working copy of its own:
152
152
  xflow db query --project 3f2a… "select id, title from tasks where done = false"
153
153
  xflow functions invoke kanban --project 3f2a… --data-file card.json
154
154
 
155
- Which commands: ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('functions list')},
155
+ Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('functions list')},
156
156
  ${(0, ui_1.bold)('functions invoke')}, ${(0, ui_1.bold)('functions logs')}, ${(0, ui_1.bold)('logs')}, ${(0, ui_1.bold)('env')}, ${(0, ui_1.bold)('env rm')},
157
157
  ${(0, ui_1.bold)('connections')}, ${(0, ui_1.bold)('connections link')}, ${(0, ui_1.bold)('schedules')}, ${(0, ui_1.bold)('schedules set')},
158
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')}.
159
+ ${(0, ui_1.bold)('deployments')}, ${(0, ui_1.bold)('publish')}, ${(0, ui_1.bold)('rollback')}. ${(0, ui_1.bold)('db status')} answers with less
160
+ under the flag: the applied history alone, because comparing it against the local
161
+ ${(0, ui_1.bold)('migrations/')} directory needs the folder.
160
162
 
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.
163
+ The rest cannot answer without the working copy and have nothing to do with an id
164
+ instead: ${(0, ui_1.bold)('deploy')}, ${(0, ui_1.bold)('pull')}, ${(0, ui_1.bold)('status')}, ${(0, ui_1.bold)('db migrate')}, ${(0, ui_1.bold)('env check')},
165
+ ${(0, ui_1.bold)('env set')}, ${(0, ui_1.bold)('connections unlink')}. They answer about the files next to them, so
166
+ they keep asking for a folder. ${(0, ui_1.bold)('projects get')} and ${(0, ui_1.bold)('link')} take no flag either:
167
+ they name the project by its id as an argument.
165
168
 
166
169
  The folder is then ignored whole, and that includes its ${(0, ui_1.bold)('xflow.json')}: the address
167
170
  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
171
  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.
172
+ project, the flag therefore goes somewhere else than the folder does. An id handed to
173
+ ${(0, ui_1.bold)('projects get')} or ${(0, ui_1.bold)('link')} behaves the same way: typed by hand, it is not redirected
174
+ by a config lying around either.
170
175
 
171
176
  The id is echoed back on every run. It is the one place a project is named by hand,
172
177
  and a wrong id answers "not found" on a read but works on a write.
@@ -175,6 +180,24 @@ With several organizations signed in, the one holding the project is looked for
175
180
  every call: there is no folder to write the answer into, and a second place to
176
181
  remember it would be a second thing to keep true. One ${(0, ui_1.bold)('xflow org switch')} to the
177
182
  right organization spares that search.`,
183
+ projects: `${(0, ui_1.bold)('xflow projects')}: the projects of the organization
184
+
185
+ xflow projects list every project, its versions state and age
186
+ xflow projects get [id] one card: versions, database, functions, schedules
187
+
188
+ Both name the project positionally: the id is an argument, not a --project flag.
189
+ Without one, ${(0, ui_1.bold)('get')} answers about the project of the folder it stands in.
190
+
191
+ An id typed by hand is looked for across every signed-in organization, and the
192
+ folder around the command is ignored whole, the way the ${(0, ui_1.bold)('--project')} flag does it
193
+ (${(0, ui_1.bold)('xflow help project')}). So "not found" means the id is wrong, or its organization
194
+ is missing from ${(0, ui_1.bold)('xflow org')}: never that the active key was simply the wrong one.
195
+
196
+ ${(0, ui_1.bold)('list')} stays within one organization: the bound one inside a project folder, the
197
+ active one outside. With several signed in, a line under the table says whose
198
+ projects these are.
199
+
200
+ ${(0, ui_1.bold)('xflow link <id>')} binds the current folder to a project; ${(0, ui_1.bold)('xflow init')} makes a new one.`,
178
201
  update: `${(0, ui_1.bold)('xflow update')}: bring the CLI up to date
179
202
 
180
203
  Installs the published version and, if anything changed, rewrites the platform
@@ -203,7 +226,8 @@ will work.`,
203
226
  xflow db status what is applied and what is waiting
204
227
  xflow db list databases of the organization, and who is on them
205
228
  xflow db schema [table] tables of the project, or the columns of one
206
- xflow db query "select ..." read data (--limit N: 50 by default, 300 at most)
229
+ xflow db query "select ..." read data (--limit N: 50 by default, 300 at most;
230
+ --json prints the values whole)
207
231
  xflow db migrate apply migrations/*.sql
208
232
 
209
233
  ${(0, ui_1.bold)('list')} is about the organization, not about this project: it names the logical
@@ -213,7 +237,10 @@ attached to each one. That last column is the one to read before a migration.
213
237
  ${(0, ui_1.bold)('schema')} answers a different question than the ${(0, ui_1.bold)('migrations/')} directory: one logical
214
238
  database is shared by several projects, so the files say what you did, and the schema
215
239
  says what is actually in there. ${(0, ui_1.bold)('query')} runs inside a READ ONLY transaction, so
216
- writes are rejected by the database itself, not by us reading your SQL.
240
+ writes are rejected by the database itself, not by us reading your SQL. Its table cuts
241
+ long values to keep the columns readable; ${(0, ui_1.bold)('--json')} prints them whole, together with
242
+ the applied limit and its ceiling. ${(0, ui_1.bold)('status')} named by id (${(0, ui_1.bold)('--project')}) answers with
243
+ the applied history alone: comparing against ${(0, ui_1.bold)('migrations/')} needs the folder.
217
244
 
218
245
  ${(0, ui_1.bold)('migrate')}: the files live in the repository (${(0, ui_1.bold)('migrations/0001_init.sql')},
219
246
  ${(0, ui_1.bold)('migrations/0002_orders.sql')} and so on). The order comes from the file name, which
package/dist/state.js CHANGED
@@ -5,6 +5,7 @@ exports.shouldNudgeSkill = shouldNudgeSkill;
5
5
  exports.markSkillNudged = markSkillNudged;
6
6
  exports.clearSkillNudges = clearSkillNudges;
7
7
  exports.markNudged = markNudged;
8
+ exports.lastNudgedVersion = lastNudgedVersion;
8
9
  const node_fs_1 = require("node:fs");
9
10
  const node_os_1 = require("node:os");
10
11
  const node_path_1 = require("node:path");
@@ -78,6 +79,10 @@ function clearSkillNudges() {
78
79
  function markNudged(version) {
79
80
  write({ nudgedVersion: version, nudgedAt: new Date().toISOString() });
80
81
  }
82
+ /** What the update notice last named, for callers that outlive the process that saw it. */
83
+ function lastNudgedVersion() {
84
+ return read().nudgedVersion ?? null;
85
+ }
81
86
  /** Best effort: a read-only home folder must not break the command that just worked. */
82
87
  function write(patch) {
83
88
  try {
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.11.1';
5
+ exports.CLI_VERSION = '0.12.0';
6
6
  /** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
7
7
  exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getxflow/cli",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "CLI for the XFlow platform: source sync, deployment and publishing of applications",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -24,11 +24,11 @@ contains the fix.
24
24
 
25
25
  ## Keeping these instructions current
26
26
 
27
- These instructions ship with xflow CLI 0.11.1. They travel inside the package, so the copy
27
+ These instructions ship with xflow CLI 0.12.0. They travel inside the package, so the copy
28
28
  you are reading can be older than the CLI answering your commands, and nothing about that
29
29
  is visible in the text itself.
30
30
 
31
- When the CLI says this file is out of date, run `xflow skills --refresh` and then **read
31
+ When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
32
32
  again the file it names**. Refreshing only writes new text to disk; until you re-read it you
33
33
  are still working from the old rules, and the command will look like it did nothing. This
34
34
  repair is yours to make, no need to ask anyone.
@@ -424,7 +424,8 @@ organization and the projects on each, which is where you find out who else is o
424
424
  For the same reason `migrations/` is not the
425
425
  schema. It says what you did; `xflow db schema [table]` says what is in the database right
426
426
  now, and `xflow db query "select ..."` reads it inside a READ ONLY transaction (50 rows by
427
- default, `--limit` raises that to 300). Look before
427
+ default, `--limit` raises that to 300). The table cuts long values to keep the columns
428
+ readable; `--json` prints them whole. Look before
428
429
  you write a migration against a shared database.
429
430
 
430
431
  ## File storage
@@ -525,8 +526,9 @@ One key per organization, stored side by side rather than replacing each other:
525
526
  Inside a project folder there is nothing to switch: commands follow the organization the
526
527
  folder is bound to, whatever the active one is. That is what lets two projects of two
527
528
  organizations work side by side. The exception is `--project <id>`, which every command
528
- that does not read the working copy takes (`xflow help project` lists them): it ignores the
529
- folder whole, so a second project needs no second checkout.
529
+ that can answer without the working copy takes (`xflow help project` lists them): it ignores
530
+ the folder whole, so a second project needs no second checkout. `projects get` and `link`
531
+ take no flag and name the project by its id as an argument instead.
530
532
 
531
533
  A "not found" on a project you know exists usually means the key belongs to another
532
534
  organization, not that the project is gone: ids are unique across the platform, so a