@getxflow/cli 1.10.0 → 1.11.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.
@@ -295,6 +295,9 @@ async function migrate(client, projectId, migrations, dryRun, allowDestructive,
295
295
  (0, ui_1.note)((0, ui_1.dim)(` ${hit}`));
296
296
  }
297
297
  if (result.dry_run) {
298
+ if (result.failed) {
299
+ throw new errors_1.CliError(`${result.failed.name}: ${result.failed.error}`, 'Checked on a copy of the database: the database is unchanged');
300
+ }
298
301
  if (result.pending.length === 0) {
299
302
  (0, ui_1.ok)('Nothing to apply: every migration is already in the database');
300
303
  return;
@@ -303,12 +306,18 @@ async function migrate(client, projectId, migrations, dryRun, allowDestructive,
303
306
  (0, ui_1.out)(`Will be applied (${result.pending.length}):`);
304
307
  for (const name of result.pending)
305
308
  (0, ui_1.out)(` ${name}`);
306
- (0, ui_1.note)((0, ui_1.dim)(' Dry run: the database was not changed'));
309
+ (0, ui_1.note)((0, ui_1.dim)(result.checked
310
+ ? ' They ran on a copy of the database as it is now; the database was not changed'
311
+ : result.checked === false
312
+ ? ' Not checked: a copy of the database could not be made; the database was not changed'
313
+ : ' Dry run: the database was not changed'));
307
314
  return;
308
315
  }
309
316
  for (const dump of result.dumps) {
310
317
  (0, ui_1.note)((0, ui_1.dim)(` Dump of ${dump.table}: ${dump.rows} rows${dump.truncated ? ' (cut at the cap)' : ''}, kept for ${result.dump_retention_days} days`));
311
318
  }
319
+ if (result.checked)
320
+ (0, ui_1.note)((0, ui_1.dim)(' Checked on a copy of the database first'));
312
321
  if (result.applied.length > 0) {
313
322
  (0, ui_1.ok)(`Applied: ${result.applied.join(', ')}`);
314
323
  }
@@ -318,7 +327,7 @@ async function migrate(client, projectId, migrations, dryRun, allowDestructive,
318
327
  if (result.failed) {
319
328
  throw new errors_1.CliError(`${result.failed.name}: ${result.failed.error}`, result.applied.length > 0
320
329
  ? `What ran before it stays in the database (${result.applied.join(', ')}), the rest did not run`
321
- : 'The database is unchanged: the migration rolled back as a whole');
330
+ : 'The database is unchanged: nothing was applied');
322
331
  }
323
332
  }
324
333
  /** Time a branch has left, `5h` or `3d`. */
@@ -109,7 +109,7 @@ function printNotes(started) {
109
109
  (0, ui_1.note)(` Unfinished: ${item.message}${item.file ? (0, ui_1.dim)(` (${item.file})`) : ''}`);
110
110
  }
111
111
  }
112
- async function finishFunctions(client, base, started) {
112
+ async function finishFunctions(client, base, started, rollback) {
113
113
  printNotes(started);
114
114
  const build = await waitForBuild(client, base, started);
115
115
  if (build.phase === 'failed')
@@ -118,6 +118,9 @@ async function finishFunctions(client, base, started) {
118
118
  for (const line of openLines(build.log_tail))
119
119
  (0, ui_1.warn)(line);
120
120
  (0, ui_1.note)((0, ui_1.dim)(' Every app of the project calls the new code already: there is no publish step for functions'));
121
+ if (rollback) {
122
+ (0, ui_1.note)((0, ui_1.dim)(` The database is not rolled back. As it was when this revision first went live: xflow db branch create rev-${build.revision} --at r${build.revision}`));
123
+ }
121
124
  }
122
125
  async function finishApp(client, base, started) {
123
126
  printNotes(started);
@@ -165,6 +168,7 @@ function targetsHint(projectRoot) {
165
168
  async function deployTarget(target, args, comment, revision) {
166
169
  const base = (0, sources_1.streamPath)(target);
167
170
  const { client } = target;
171
+ const rollback = revision !== undefined;
168
172
  if (revision === undefined) {
169
173
  if (!target.root)
170
174
  throw new errors_1.CliError('Nothing to send without a folder', 'Build a revision the platform holds: --revision <n>');
@@ -189,7 +193,7 @@ async function deployTarget(target, args, comment, revision) {
189
193
  if (target.kind === 'app')
190
194
  await finishApp(client, base, started);
191
195
  else
192
- await finishFunctions(client, base, started);
196
+ await finishFunctions(client, base, started, rollback);
193
197
  }
194
198
  async function deploy(args) {
195
199
  const stated = statedRevision((0, args_1.flagNumber)(args, 'revision'), (0, args_1.flagBool)(args, 'force'));
package/dist/help.js CHANGED
@@ -296,7 +296,8 @@ will work.`,
296
296
  1 on the Free plan (--limit, --cursor for older)
297
297
  xflow db branch [list] branches of the database (--json)
298
298
  xflow db branch create <name> a branch as the database is now; --at 2h, --at 14:05 or
299
- --at 2026-10-30T14:05+03:00 for a moment of its history;
299
+ --at 2026-10-30T14:05+03:00 for a moment of its history,
300
+ --at r12 for when functions revision 12 went live;
300
301
  --ttl 12h or --ttl 3d to delete it sooner than the plan does
301
302
  xflow db branch extend <name> more time from now: the plan's longest, or --ttl
302
303
  xflow db branch delete <name> the branch goes, the database is unchanged
@@ -349,9 +350,10 @@ ${(0, ui_1.bold)('migrations/0002_orders.sql')} and so on). The order comes from
349
350
  is why the leading number is required. The command first stores the project root as a
350
351
  revision, so the migrations stay in its history and a clone gets them, then applies what is
351
352
  new. Every migration runs in its own transaction, the history is kept in the database
352
- itself, and anything already applied is not run again.
353
+ itself, and anything already applied is not run again. New migrations run first, all of them,
354
+ on a copy of the database as it is now: if one fails there, nothing is applied.
353
355
 
354
- --dry-run report what would be applied without touching the database
356
+ --dry-run run the new migrations on that copy only; the database is not touched
355
357
  --allow-destructive allow operations that destroy data
356
358
 
357
359
  About destructive ones. ${(0, ui_1.bold)('DROP TABLE')}, ${(0, ui_1.bold)('DROP COLUMN')}, ${(0, ui_1.bold)('TRUNCATE')} and deleting rows (judged by what
package/dist/moment.js CHANGED
@@ -4,8 +4,10 @@ exports.parseMoment = parseMoment;
4
4
  exports.parseTtl = parseTtl;
5
5
  const errors_1 = require("./errors");
6
6
  const UNIT_MS = { m: 60_000, h: 3_600_000, d: 86_400_000 };
7
- /** `30m`/`2h`/`1d` ago, `14:05` local (yesterday if still ahead), or an ISO date. */
7
+ /** `30m`/`2h`/`1d` ago, `14:05` local (yesterday if still ahead), an ISO date, or `r12` (the platform resolves it). */
8
8
  function parseMoment(text, now = new Date()) {
9
+ if (/^r\d{1,6}$/.test(text))
10
+ return text;
9
11
  const ago = /^(\d{1,4})([mhd])$/.exec(text);
10
12
  if (ago)
11
13
  return new Date(now.getTime() - Number(ago[1]) * UNIT_MS[ago[2]]).toISOString();
@@ -19,7 +21,7 @@ function parseMoment(text, now = new Date()) {
19
21
  }
20
22
  const at = new Date(text);
21
23
  if (!/^\d{4}-\d{2}-\d{2}/.test(text) || Number.isNaN(at.getTime())) {
22
- throw new errors_1.CliError(`--at ${text} is not a moment`, 'For example: --at 2h (two hours ago), --at 14:05 (today), --at 2026-10-30T14:05+03:00');
24
+ throw new errors_1.CliError(`--at ${text} is not a moment`, 'For example: --at 2h (two hours ago), --at 14:05 (today), --at 2026-10-30T14:05+03:00, --at r12 (when functions revision 12 went live)');
23
25
  }
24
26
  return at.toISOString();
25
27
  }
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 = '1.10.0';
5
+ exports.CLI_VERSION = '1.11.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": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -28,7 +28,7 @@ instead of guessing; give the person https://docs.getxflow.com when they ask how
28
28
 
29
29
  ## Keeping these instructions current
30
30
 
31
- These instructions ship with xflow CLI 1.10.0, so the copy you are reading can be older than
31
+ These instructions ship with xflow CLI 1.11.0, so the copy you are reading can be older than
32
32
  the CLI answering your commands.
33
33
 
34
34
  When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
@@ -553,7 +553,8 @@ There is no undo: read the rows with `db query` first.
553
553
 
554
554
  A branch is a database of its own, as the project database is now or was at a moment of its
555
555
  history (6 hours back on Free, 3 days on Start, 7 days on Business), made in seconds without
556
- copying: `xflow db branch create try-orders [--at 2h|14:05|<ISO date>]`. Try a migration or a fix
556
+ copying: `xflow db branch create try-orders [--at 2h|14:05|<ISO date>|r12]` (`r12`: when functions
557
+ revision 12 went live; rolling functions back with `--revision` leaves the database as it is). Try a migration or a fix
557
558
  there first (`db migrate`, `db exec`, `db query` take `--branch <name>`); nothing reaches the
558
559
  project database. After a data mistake, do not roll anything back: branch as of a moment before
559
560
  it, check the rows with `db query --branch`, then `xflow db restore --from <branch> --table <t>
@@ -562,8 +563,8 @@ it, check the rows with `db query --branch`, then `xflow db restore --from <bran
562
563
  5, 15 by plan; delete them when done (`xflow db branch delete <name>`). A branch is deleted on its
563
564
  own when its lifetime ends: at most 1, 7 or 30 days by plan, shorter with `--ttl 12h`, more from
564
565
  now with `xflow db branch extend <name>`. A branch sleeps after a quiet minute, has no functions,
565
- and a branch of a branch is not made. People see the branches in Databases, open them there and
566
- read their data.
566
+ and a branch of a branch is not made. People see the branches in Databases, open them there, read
567
+ their data and bring missing rows back.
567
568
 
568
569
  Anything that destroys data
569
570
  (`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE`) needs both the `--allow-destructive` flag and
@@ -573,7 +574,12 @@ or deleted rows is rolled back, however the SQL was written. `BEGIN`/`COMMIT` in
573
574
  refused: each one already runs in its own transaction. When the refusal is about the right, the flag changes nothing: say what
574
575
  needs deleting and why, and let the person decide. With both in place the affected tables are
575
576
  dumped first (up to 50 000 rows each) and kept for 7 days; getting a dump back is a request to
576
- the person, no command does it. Check with `--dry-run`.
577
+ the person, no command does it.
578
+
579
+ New migrations run first, all of them, on a copy of the database as it is now: if one fails
580
+ there, nothing is applied and the database is unchanged, so a half-applied set does not happen.
581
+ `--dry-run` runs only that check. A copy that could not be made is skipped, and the migration goes
582
+ straight to the database as before.
577
583
 
578
584
  Pass `DATABASE_URL` as is, without an `ssl` option. The database is PostgreSQL 18, so do not
579
585
  tune it from migrations: