@getxflow/cli 1.9.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.
- package/dist/commands/db.js +36 -6
- package/dist/commands/deploy.js +6 -2
- package/dist/flags.js +2 -2
- package/dist/help.js +10 -4
- package/dist/limits.js +1 -1
- package/dist/moment.js +12 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +16 -6
package/dist/commands/db.js
CHANGED
|
@@ -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)(
|
|
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,9 +327,14 @@ 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:
|
|
330
|
+
: 'The database is unchanged: nothing was applied');
|
|
322
331
|
}
|
|
323
332
|
}
|
|
333
|
+
/** Time a branch has left, `5h` or `3d`. */
|
|
334
|
+
function left(iso) {
|
|
335
|
+
const h = Math.max(0, Math.round((Date.parse(iso) - Date.now()) / 3_600_000));
|
|
336
|
+
return h < 48 ? `${h}h left` : `${Math.round(h / 24)}d left`;
|
|
337
|
+
}
|
|
324
338
|
/** Branches of the project database: list, create, delete. */
|
|
325
339
|
async function dbBranch(args) {
|
|
326
340
|
const [sub, name] = args.words;
|
|
@@ -329,7 +343,7 @@ async function dbBranch(args) {
|
|
|
329
343
|
if (sub === undefined || sub === 'list') {
|
|
330
344
|
const data = await (0, api_1.apiJson)(client, path);
|
|
331
345
|
if ((0, args_1.flagBool)(args, 'json')) {
|
|
332
|
-
(0, ui_1.out)(JSON.stringify(data
|
|
346
|
+
(0, ui_1.out)(JSON.stringify(data, null, 2));
|
|
333
347
|
return;
|
|
334
348
|
}
|
|
335
349
|
if (data.branches.length === 0) {
|
|
@@ -337,16 +351,18 @@ async function dbBranch(args) {
|
|
|
337
351
|
(0, ui_1.note)((0, ui_1.dim)(' A copy to try things on: xflow db branch create <name> [--at 2h]'));
|
|
338
352
|
return;
|
|
339
353
|
}
|
|
340
|
-
(0, ui_1.table)(data.branches.map((b) => [b.name, b.at ? `as of ${new Date(b.at).toLocaleString()}` : 'as made', (0, ui_1.formatAge)(b.created_at)]));
|
|
354
|
+
(0, ui_1.table)(data.branches.map((b) => [b.name, b.at ? `as of ${new Date(b.at).toLocaleString()}` : 'as made', (0, ui_1.formatAge)(b.created_at), left(b.expires_at)]));
|
|
355
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${data.branches.length} of ${data.plan.branches}; a branch lives up to ${data.plan.ttl_hours / 24}d on this plan (xflow db branch extend <name>)`));
|
|
341
356
|
return;
|
|
342
357
|
}
|
|
343
358
|
if (!name)
|
|
344
359
|
throw new errors_1.CliError(`xflow db branch ${sub} needs a name`, `For example: xflow db branch ${sub} fix-orders`);
|
|
345
360
|
if (sub === 'create') {
|
|
346
361
|
const at = (0, args_1.flagString)(args, 'at');
|
|
362
|
+
const ttl = (0, args_1.flagString)(args, 'ttl');
|
|
347
363
|
const data = await (0, api_1.apiJson)(client, path, {
|
|
348
364
|
method: 'POST',
|
|
349
|
-
body: { name, at: at ? (0, moment_1.parseMoment)(at) : undefined },
|
|
365
|
+
body: { name, at: at ? (0, moment_1.parseMoment)(at) : undefined, ttl_hours: ttl ? (0, moment_1.parseTtl)(ttl) : undefined },
|
|
350
366
|
timeoutMs: 180_000,
|
|
351
367
|
});
|
|
352
368
|
if ((0, args_1.flagBool)(args, 'json')) {
|
|
@@ -356,6 +372,20 @@ async function dbBranch(args) {
|
|
|
356
372
|
(0, ui_1.ok)(`Branch ${name}: the project database ${data.branch.at ? `as of ${new Date(data.branch.at).toLocaleString()}` : 'as it is now'}`);
|
|
357
373
|
(0, ui_1.note)((0, ui_1.dim)(` Work in it: xflow db query --branch ${name} '...', db exec and db migrate take --branch too`));
|
|
358
374
|
(0, ui_1.note)((0, ui_1.dim)(` Rows back into the project database: xflow db restore --from ${name} --table <table>`));
|
|
375
|
+
(0, ui_1.note)((0, ui_1.dim)(` Deleted on its own in ${left(data.branch.expires_at)}; more time: xflow db branch extend ${name}`));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (sub === 'extend') {
|
|
379
|
+
const ttl = (0, args_1.flagString)(args, 'ttl');
|
|
380
|
+
const data = await (0, api_1.apiJson)(client, `${path}/${encodeURIComponent(name)}`, {
|
|
381
|
+
method: 'PATCH',
|
|
382
|
+
body: { ttl_hours: ttl ? (0, moment_1.parseTtl)(ttl) : undefined },
|
|
383
|
+
});
|
|
384
|
+
if ((0, args_1.flagBool)(args, 'json')) {
|
|
385
|
+
(0, ui_1.out)(JSON.stringify(data.branch, null, 2));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
(0, ui_1.ok)(`Branch ${name}: ${left(data.branch.expires_at)}, until ${new Date(data.branch.expires_at).toLocaleString()}`);
|
|
359
389
|
return;
|
|
360
390
|
}
|
|
361
391
|
if (sub === 'delete' || sub === 'rm') {
|
|
@@ -363,7 +393,7 @@ async function dbBranch(args) {
|
|
|
363
393
|
(0, ui_1.ok)(`Branch ${name} deleted; the project database is unchanged`);
|
|
364
394
|
return;
|
|
365
395
|
}
|
|
366
|
-
throw new errors_1.CliError(`Unknown command: db branch ${sub}`, 'Available: list, create and delete');
|
|
396
|
+
throw new errors_1.CliError(`Unknown command: db branch ${sub}`, 'Available: list, create, extend and delete');
|
|
367
397
|
}
|
|
368
398
|
/** Rows of a table from a branch back into the project database, by primary key. */
|
|
369
399
|
async function dbRestore(args) {
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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/flags.js
CHANGED
|
@@ -82,8 +82,8 @@ exports.KNOWN = {
|
|
|
82
82
|
'db exec': ['allow-destructive', 'json', 'project', 'branch'],
|
|
83
83
|
'db logs': ['limit', 'project', 'cursor'],
|
|
84
84
|
'db migrate': ['dry-run', 'allow-destructive', 'branch'],
|
|
85
|
-
'db branch': ['at', 'json', 'project'],
|
|
86
|
-
'db branches': ['at', 'json', 'project'],
|
|
85
|
+
'db branch': ['at', 'json', 'project', 'ttl'],
|
|
86
|
+
'db branches': ['at', 'json', 'project', 'ttl'],
|
|
87
87
|
'db restore': ['from', 'table', 'where', 'overwrite', 'json', 'project'],
|
|
88
88
|
storage: ['json', 'project'],
|
|
89
89
|
'storage ls': ['json', 'project'],
|
package/dist/help.js
CHANGED
|
@@ -296,7 +296,10 @@ 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;
|
|
301
|
+
--ttl 12h or --ttl 3d to delete it sooner than the plan does
|
|
302
|
+
xflow db branch extend <name> more time from now: the plan's longest, or --ttl
|
|
300
303
|
xflow db branch delete <name> the branch goes, the database is unchanged
|
|
301
304
|
xflow db restore --from <branch> --table <table> [--where '...'] [--overwrite]
|
|
302
305
|
rows of a table from a branch back into the database
|
|
@@ -331,7 +334,9 @@ ${(0, ui_1.bold)('query')}, ${(0, ui_1.bold)('exec')} and ${(0, ui_1.bold)('migr
|
|
|
331
334
|
reaches the project database. A branch sleeps after a quiet minute, so its first query takes a
|
|
332
335
|
second or two; it has no functions and no history of its own, and a branch of a branch is not
|
|
333
336
|
made. Branches per project: 2 on Free, 5 on Start, 15 on Business; delete one you no longer need.
|
|
334
|
-
|
|
337
|
+
A branch is deleted on its own when its lifetime ends: at most 1 day on Free, 7 days on Start,
|
|
338
|
+
30 days on Business, and ${(0, ui_1.bold)('extend')} gives it more from now within that. The history reaches
|
|
339
|
+
back 6 hours on Free, 3 days on Start, 7 days on Business.
|
|
335
340
|
|
|
336
341
|
${(0, ui_1.bold)('restore')} puts rows back after a mistake without rolling the whole database back: make a
|
|
337
342
|
branch as of a moment before it, check the rows there with ${(0, ui_1.bold)('query --branch')}, then restore
|
|
@@ -345,9 +350,10 @@ ${(0, ui_1.bold)('migrations/0002_orders.sql')} and so on). The order comes from
|
|
|
345
350
|
is why the leading number is required. The command first stores the project root as a
|
|
346
351
|
revision, so the migrations stay in its history and a clone gets them, then applies what is
|
|
347
352
|
new. Every migration runs in its own transaction, the history is kept in the database
|
|
348
|
-
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.
|
|
349
355
|
|
|
350
|
-
--dry-run
|
|
356
|
+
--dry-run run the new migrations on that copy only; the database is not touched
|
|
351
357
|
--allow-destructive allow operations that destroy data
|
|
352
358
|
|
|
353
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/limits.js
CHANGED
|
@@ -54,7 +54,7 @@ const QUOTA_LABELS = {
|
|
|
54
54
|
people: 'People',
|
|
55
55
|
space_mb: 'Space (databases and files together)',
|
|
56
56
|
function_minutes: 'Function minutes',
|
|
57
|
-
db_hours: 'Database hours',
|
|
57
|
+
db_hours: 'Database GB-hours',
|
|
58
58
|
builds: 'Builds this month',
|
|
59
59
|
};
|
|
60
60
|
/** Decimal units, 1000 MB per GB: matches how volumes are sold. */
|
package/dist/moment.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseMoment = parseMoment;
|
|
4
|
+
exports.parseTtl = parseTtl;
|
|
4
5
|
const errors_1 = require("./errors");
|
|
5
6
|
const UNIT_MS = { m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
6
|
-
/** `30m`/`2h`/`1d` ago, `14:05` local (yesterday if still ahead),
|
|
7
|
+
/** `30m`/`2h`/`1d` ago, `14:05` local (yesterday if still ahead), an ISO date, or `r12` (the platform resolves it). */
|
|
7
8
|
function parseMoment(text, now = new Date()) {
|
|
9
|
+
if (/^r\d{1,6}$/.test(text))
|
|
10
|
+
return text;
|
|
8
11
|
const ago = /^(\d{1,4})([mhd])$/.exec(text);
|
|
9
12
|
if (ago)
|
|
10
13
|
return new Date(now.getTime() - Number(ago[1]) * UNIT_MS[ago[2]]).toISOString();
|
|
@@ -18,7 +21,14 @@ function parseMoment(text, now = new Date()) {
|
|
|
18
21
|
}
|
|
19
22
|
const at = new Date(text);
|
|
20
23
|
if (!/^\d{4}-\d{2}-\d{2}/.test(text) || Number.isNaN(at.getTime())) {
|
|
21
|
-
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)');
|
|
22
25
|
}
|
|
23
26
|
return at.toISOString();
|
|
24
27
|
}
|
|
28
|
+
/** A branch lifetime in whole hours: `12h` or `3d`. */
|
|
29
|
+
function parseTtl(text) {
|
|
30
|
+
const m = /^(\d{1,4})([hd])$/.exec(text);
|
|
31
|
+
if (!m || Number(m[1]) < 1)
|
|
32
|
+
throw new errors_1.CliError(`--ttl ${text} is not a lifetime`, 'For example: --ttl 12h or --ttl 3d');
|
|
33
|
+
return Number(m[1]) * (m[2] === 'd' ? 24 : 1);
|
|
34
|
+
}
|
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.
|
|
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
package/skills/xflow/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|
@@ -119,7 +119,8 @@ schedules may run, and which access levels an app may have. The number of functi
|
|
|
119
119
|
and projects is not capped; at most 15 new projects a day (UTC) per organization, deleted ones count. Most are finite; a plan
|
|
120
120
|
may leave one uncapped, and then `whoami` prints `no limit` instead of a ceiling.
|
|
121
121
|
A paid plan comes in packages (Start S/M/L, Business M/L/XL): the package sets the space,
|
|
122
|
-
database hours
|
|
122
|
+
database GB-hours (1 GB of memory a database actually uses for an hour awake; a sleeping
|
|
123
|
+
one uses none) and function minutes, the plan sets the rest. More space is a bigger package
|
|
123
124
|
chosen by a person in «Тариф» (Plan); there is no separate purchase of space.
|
|
124
125
|
`xflow whoami` prints every one of them next to what is used, and the rights of your own key
|
|
125
126
|
with them: destroying data (a migration or `db exec`), deleting files from storage, moving a database and
|
|
@@ -552,14 +553,18 @@ There is no undo: read the rows with `db query` first.
|
|
|
552
553
|
|
|
553
554
|
A branch is a database of its own, as the project database is now or was at a moment of its
|
|
554
555
|
history (6 hours back on Free, 3 days on Start, 7 days on Business), made in seconds without
|
|
555
|
-
copying: `xflow db branch create try-orders [--at 2h|14:05|<ISO date
|
|
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
|
|
556
558
|
there first (`db migrate`, `db exec`, `db query` take `--branch <name>`); nothing reaches the
|
|
557
559
|
project database. After a data mistake, do not roll anything back: branch as of a moment before
|
|
558
560
|
it, check the rows with `db query --branch`, then `xflow db restore --from <branch> --table <t>
|
|
559
561
|
[--where '...']` puts the missing rows back by primary key and keeps all newer data
|
|
560
562
|
(`--overwrite` replaces rows still there and needs the destroy right). Branches per project: 2,
|
|
561
|
-
5, 15 by plan; delete them when done (`xflow db branch delete <name>`). A branch
|
|
562
|
-
|
|
563
|
+
5, 15 by plan; delete them when done (`xflow db branch delete <name>`). A branch is deleted on its
|
|
564
|
+
own when its lifetime ends: at most 1, 7 or 30 days by plan, shorter with `--ttl 12h`, more from
|
|
565
|
+
now with `xflow db branch extend <name>`. A branch sleeps after a quiet minute, has no functions,
|
|
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.
|
|
563
568
|
|
|
564
569
|
Anything that destroys data
|
|
565
570
|
(`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE`) needs both the `--allow-destructive` flag and
|
|
@@ -569,7 +574,12 @@ or deleted rows is rolled back, however the SQL was written. `BEGIN`/`COMMIT` in
|
|
|
569
574
|
refused: each one already runs in its own transaction. When the refusal is about the right, the flag changes nothing: say what
|
|
570
575
|
needs deleting and why, and let the person decide. With both in place the affected tables are
|
|
571
576
|
dumped first (up to 50 000 rows each) and kept for 7 days; getting a dump back is a request to
|
|
572
|
-
the person, no command does it.
|
|
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.
|
|
573
583
|
|
|
574
584
|
Pass `DATABASE_URL` as is, without an `ssl` option. The database is PostgreSQL 18, so do not
|
|
575
585
|
tune it from migrations:
|