@getxflow/cli 1.2.1 → 1.3.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/bin.js +5 -1
- package/dist/commands/db.js +56 -8
- package/dist/flags.js +1 -0
- package/dist/help.js +16 -4
- package/dist/version.js +1 -1
- package/package.json +24 -24
- package/skills/xflow/SKILL.md +728 -722
package/dist/bin.js
CHANGED
|
@@ -176,6 +176,10 @@ async function run(args) {
|
|
|
176
176
|
await (0, db_1.dbQuery)({ ...args, words: args.words.slice(2) });
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
|
+
if (second === 'exec') {
|
|
180
|
+
await (0, db_1.dbExec)({ ...args, words: args.words.slice(2) });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
179
183
|
if (second === 'move') {
|
|
180
184
|
await (0, db_1.dbMove)(rest);
|
|
181
185
|
return;
|
|
@@ -184,7 +188,7 @@ async function run(args) {
|
|
|
184
188
|
await (0, db_1.dbStatus)({ ...args, words: args.words.slice(2) });
|
|
185
189
|
return;
|
|
186
190
|
}
|
|
187
|
-
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query, migrate and move');
|
|
191
|
+
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query, exec, migrate and move');
|
|
188
192
|
case 'storage':
|
|
189
193
|
if (second === 'push') {
|
|
190
194
|
await (0, storage_1.storagePush)(rest);
|
package/dist/commands/db.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.dbStatus = dbStatus;
|
|
|
4
4
|
exports.dbList = dbList;
|
|
5
5
|
exports.dbSchema = dbSchema;
|
|
6
6
|
exports.dbQuery = dbQuery;
|
|
7
|
+
exports.dbExec = dbExec;
|
|
7
8
|
exports.dbMove = dbMove;
|
|
8
9
|
exports.dbMigrate = dbMigrate;
|
|
9
10
|
exports.migrateBeforeDeploy = migrateBeforeDeploy;
|
|
@@ -227,6 +228,20 @@ async function dbSchema(args) {
|
|
|
227
228
|
]));
|
|
228
229
|
(0, ui_1.note)((0, ui_1.dim)(' Columns of one table: xflow db schema <table>'));
|
|
229
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* The statement typed after the command, or read from the .sql file named there.
|
|
233
|
+
* The hint is split in lines: an agent terminal cuts a long one at its width.
|
|
234
|
+
*/
|
|
235
|
+
function readSql(words, command, example, file) {
|
|
236
|
+
const typed = words.join(' ').trim();
|
|
237
|
+
const sql = /\.sql$/i.test(typed)
|
|
238
|
+
? (0, text_file_1.readTextFile)(typed, 'An argument ending in .sql is read as a file: check the path, or end the statement with ;')
|
|
239
|
+
: typed;
|
|
240
|
+
if (!sql) {
|
|
241
|
+
throw new errors_1.CliError(typed ? `${typed} is empty` : 'An SQL statement is required', `For example: xflow db ${command} '${example}'\n A long one from a file outside the project: xflow db ${command} ../${file}`);
|
|
242
|
+
}
|
|
243
|
+
return sql;
|
|
244
|
+
}
|
|
230
245
|
/**
|
|
231
246
|
* Reading data.
|
|
232
247
|
*
|
|
@@ -234,14 +249,7 @@ async function dbSchema(args) {
|
|
|
234
249
|
* database itself rather than by us guessing at the text of the SQL.
|
|
235
250
|
*/
|
|
236
251
|
async function dbQuery(args) {
|
|
237
|
-
const
|
|
238
|
-
const sql = /\.sql$/i.test(typed)
|
|
239
|
-
? (0, text_file_1.readTextFile)(typed, 'An argument ending in .sql is read as a file: check the path, or end the query with ;')
|
|
240
|
-
: typed;
|
|
241
|
-
if (!sql) {
|
|
242
|
-
throw new errors_1.CliError(typed ? `${typed} is empty` : 'A query is required', "For example: xflow db query 'select * from orders order by created_at desc limit 20', " +
|
|
243
|
-
'or a long one from a file outside the project: xflow db query ../report.sql');
|
|
244
|
-
}
|
|
252
|
+
const sql = readSql(args.words, 'query', 'select * from orders order by created_at desc limit 20', 'report.sql');
|
|
245
253
|
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
246
254
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/query`, {
|
|
247
255
|
method: 'POST',
|
|
@@ -280,6 +288,46 @@ async function dbQuery(args) {
|
|
|
280
288
|
: ` Shown the first ${data.rows.length} rows, more exist: narrow the query, with a where clause or an offset`));
|
|
281
289
|
}
|
|
282
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* A one-off data change: one INSERT, UPDATE, DELETE or MERGE, saved at once and
|
|
293
|
+
* kept out of the migration history. The platform judges it by what the database
|
|
294
|
+
* reports, not by the text: schema changes are refused, a mass change asks for
|
|
295
|
+
* --allow-destructive.
|
|
296
|
+
*/
|
|
297
|
+
async function dbExec(args) {
|
|
298
|
+
const sql = readSql(args.words, 'exec', "update orders set status = 'paid' where id = 42", 'fix.sql');
|
|
299
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
300
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/exec`, {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
body: { sql, allow_destructive: (0, args_1.flagBool)(args, 'allow-destructive') },
|
|
303
|
+
timeoutMs: 120_000,
|
|
304
|
+
});
|
|
305
|
+
if ((0, args_1.flagBool)(args, 'json')) {
|
|
306
|
+
(0, ui_1.out)(JSON.stringify(data, null, 2));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// A write inside WITH comes tagged SELECT: its count is the rows read, the tables below say what changed.
|
|
310
|
+
(0, ui_1.ok)(data.command === 'SELECT'
|
|
311
|
+
? 'The writes inside WITH are saved'
|
|
312
|
+
: `${data.command}: ${data.row_count} ${data.row_count === 1 ? 'row' : 'rows'}, saved`);
|
|
313
|
+
for (const t of data.tables) {
|
|
314
|
+
const parts = [
|
|
315
|
+
t.inserted > 0 ? `${t.inserted} inserted` : '',
|
|
316
|
+
t.updated > 0 ? `${t.updated} updated` : '',
|
|
317
|
+
t.deleted > 0 ? `${t.deleted} deleted` : '',
|
|
318
|
+
].filter(Boolean);
|
|
319
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${t.table}: ${parts.join(', ')}`));
|
|
320
|
+
}
|
|
321
|
+
for (const dump of data.dumps) {
|
|
322
|
+
(0, ui_1.note)((0, ui_1.dim)(` Copy of ${dump.table} before the change: ${dump.rows} rows, kept for ${data.dump_retention_days} days`));
|
|
323
|
+
}
|
|
324
|
+
if (data.rows.length > 0) {
|
|
325
|
+
(0, ui_1.out)('');
|
|
326
|
+
(0, ui_1.table)([data.columns, ...data.rows.map((row) => data.columns.map((column) => cell(row[column])))]);
|
|
327
|
+
if (data.truncated)
|
|
328
|
+
(0, ui_1.note)((0, ui_1.dim)(` The first ${data.rows.length} returned rows: the rest are saved but not sent back`));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
283
331
|
const MOVE_POLL_MS = 3_000;
|
|
284
332
|
/** Past this the command lets go: the move carries on, db status follows it. */
|
|
285
333
|
const MOVE_FOLLOW_MS = 10 * 60_000;
|
package/dist/flags.js
CHANGED
|
@@ -79,6 +79,7 @@ exports.KNOWN = {
|
|
|
79
79
|
'db list': [],
|
|
80
80
|
'db schema': ['project'],
|
|
81
81
|
'db query': ['json', 'project'],
|
|
82
|
+
'db exec': ['allow-destructive', 'json', 'project'],
|
|
82
83
|
'db migrate': ['dry-run', 'allow-destructive'],
|
|
83
84
|
'db move': ['to', 'yes', 'project'],
|
|
84
85
|
storage: ['json', 'project'],
|
package/dist/help.js
CHANGED
|
@@ -94,6 +94,7 @@ ${(0, ui_1.bold)('Database')}
|
|
|
94
94
|
xflow db list databases of the projects and the servers of the organization
|
|
95
95
|
xflow db schema [table] tables of the project, or the columns of one
|
|
96
96
|
xflow db query 'select ...' read data, in a read-only transaction
|
|
97
|
+
xflow db exec 'update ...' a one-off data change, outside the migration history
|
|
97
98
|
xflow db migrate [--dry-run] [--allow-destructive]
|
|
98
99
|
apply the migrations from migrations/*.sql
|
|
99
100
|
xflow db move --to <server> --yes
|
|
@@ -172,7 +173,7 @@ no working copy of its own:
|
|
|
172
173
|
xflow db query --project 3f2a… 'select id, title from tasks where done = false'
|
|
173
174
|
xflow functions invoke kanban --project 3f2a… --data-file card.json
|
|
174
175
|
|
|
175
|
-
Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('db move')}, ${(0, ui_1.bold)('functions list')},
|
|
176
|
+
Which commands: ${(0, ui_1.bold)('db status')}, ${(0, ui_1.bold)('db query')}, ${(0, ui_1.bold)('db exec')}, ${(0, ui_1.bold)('db schema')}, ${(0, ui_1.bold)('db move')}, ${(0, ui_1.bold)('functions list')},
|
|
176
177
|
${(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')},
|
|
177
178
|
${(0, ui_1.bold)('connections')}, ${(0, ui_1.bold)('connections link')}, ${(0, ui_1.bold)('connections unlink')}, ${(0, ui_1.bold)('schedules')},
|
|
178
179
|
${(0, ui_1.bold)('schedules set')}, ${(0, ui_1.bold)('schedules rm')}, ${(0, ui_1.bold)('storage ls')}, ${(0, ui_1.bold)('storage push')},
|
|
@@ -234,7 +235,7 @@ project, apps in subfolders:
|
|
|
234
235
|
|
|
235
236
|
The root and every app are separate histories: apps/** never travels with the root, the
|
|
236
237
|
root never travels with an app. The app is its appId, so a folder can be renamed freely.
|
|
237
|
-
Who opens an app (members of the project
|
|
238
|
+
Who opens an app (members of the project or anyone with the link) is set by a person
|
|
238
239
|
in the web interface, not here.
|
|
239
240
|
|
|
240
241
|
Both ${(0, ui_1.bold)('projects')} commands name the project positionally: the id is an argument, not a
|
|
@@ -341,6 +342,8 @@ will work.`,
|
|
|
341
342
|
xflow db query 'select ...' read data (rows: the query's own limit, 50 without
|
|
342
343
|
one, 300 at most; --json prints the values whole)
|
|
343
344
|
xflow db query <file>.sql the same, the query read from a file
|
|
345
|
+
xflow db exec 'update ...' a one-off data change: one INSERT, UPDATE, DELETE or MERGE
|
|
346
|
+
(or <file>.sql; --allow-destructive, --json)
|
|
344
347
|
xflow db migrate store the project root, then apply migrations/*.sql
|
|
345
348
|
xflow db move --to <server> --yes
|
|
346
349
|
move the project database to another server
|
|
@@ -374,6 +377,15 @@ long values to keep the columns readable; ${(0, ui_1.bold)('--json')} prints the
|
|
|
374
377
|
the applied limit and its ceiling. ${(0, ui_1.bold)('status')} named by id (${(0, ui_1.bold)('--project')}) answers with
|
|
375
378
|
the applied history alone: comparing against ${(0, ui_1.bold)('migrations/')} needs the folder.
|
|
376
379
|
|
|
380
|
+
${(0, ui_1.bold)('exec')} fixes data once: one INSERT, UPDATE, DELETE or MERGE, saved at once and kept out
|
|
381
|
+
of the migration history, so a one-off fix no longer becomes a migration forever. Schema
|
|
382
|
+
changes are refused: they stay with ${(0, ui_1.bold)('migrate')}. The platform judges the change by what the
|
|
383
|
+
database reports, cascades and triggers included. Deleting rows, or updating more than 100
|
|
384
|
+
in a table, asks for ${(0, ui_1.bold)('--allow-destructive')}, and those tables are copied for 7 days before it
|
|
385
|
+
is saved; deleting also needs the right to destroy data on the key, as in migrations (see
|
|
386
|
+
below). The rows it holds are locked while it runs, so a statement that waits on a running
|
|
387
|
+
app is cancelled after a few seconds and changes nothing. There is no undo.
|
|
388
|
+
|
|
377
389
|
${(0, ui_1.bold)('migrate')}: the files live in the project root (${(0, ui_1.bold)('migrations/0001_init.sql')},
|
|
378
390
|
${(0, ui_1.bold)('migrations/0002_orders.sql')} and so on). The order comes from the file name, which
|
|
379
391
|
is why the leading number is required. The command first stores the project root as a
|
|
@@ -385,8 +397,8 @@ itself, and anything already applied is not run again.
|
|
|
385
397
|
--allow-destructive allow operations that destroy data
|
|
386
398
|
|
|
387
399
|
About destructive ones. The platform keeps no database history and makes no backups,
|
|
388
|
-
so ${(0, ui_1.bold)('DROP TABLE')}, ${(0, ui_1.bold)('DROP COLUMN')}, ${(0, ui_1.bold)('TRUNCATE')} and
|
|
389
|
-
|
|
400
|
+
so ${(0, ui_1.bold)('DROP TABLE')}, ${(0, ui_1.bold)('DROP COLUMN')}, ${(0, ui_1.bold)('TRUNCATE')} and deleting rows (judged by what
|
|
401
|
+
the database did, not by the text) need two things at once: this flag, and the right to destroy data on the
|
|
390
402
|
access key. That right is off by default, and only its owner turns it on, in the
|
|
391
403
|
platform settings, where the key permissions live: a flag is something an agent adds by itself, a
|
|
392
404
|
right is not. With both in place, the contents of the affected tables are dumped and
|
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.3.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,24 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@getxflow/cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"engines": {
|
|
7
|
-
"node": ">=20"
|
|
8
|
-
},
|
|
9
|
-
"bin": {
|
|
10
|
-
"xflow": "dist/bin.js"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"dist",
|
|
14
|
-
"skills"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
18
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
20
|
-
},
|
|
21
|
-
"publishConfig": {
|
|
22
|
-
"access": "public"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@getxflow/cli",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"xflow": "dist/bin.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"skills"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
18
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|